Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/Parser/DeprecatedInterpolatedStringVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php declare(strict_types = 1);

namespace PHPStan\Parser;

use Override;
use PhpParser\Node;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Scalar\InterpolatedString;
use PhpParser\NodeVisitorAbstract;
use PhpParser\Token;
use PHPStan\DependencyInjection\AutowiredService;

#[AutowiredService]
final class DeprecatedInterpolatedStringVisitor extends NodeVisitorAbstract implements TokenAwareVisitor
{
public const ATTRIBUTE_NAME = 'isDeprecatedInterpolatedString';

/**
* @var Token[]
*/
protected array $tokens = [];

#[Override]
public function setTokens(array $tokens): void
{
$this->tokens = $tokens;
}

#[Override]
public function enterNode(Node $node): ?Node
{
if (!$node instanceof InterpolatedString) {
return null;
}

foreach ($node->parts as $part) {
if (!$part instanceof Variable && !($part instanceof ArrayDimFetch && $part->var instanceof Variable)) {
continue;
}
$startTokenPos = $part->getStartTokenPos();
if (!isset($this->tokens[$startTokenPos])) {
continue;
}
$startToken = (string) $this->tokens[$startTokenPos];

Check failure on line 45 in src/Parser/DeprecatedInterpolatedStringVisitor.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, windows-latest)

Call to method __toString() of internal class Symfony\Polyfill\Php80\PhpToken from outside its root namespace Symfony.

Check failure on line 45 in src/Parser/DeprecatedInterpolatedStringVisitor.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, ubuntu-latest)

Call to method __toString() of internal class Symfony\Polyfill\Php80\PhpToken from outside its root namespace Symfony.
if ($startToken !== '${') {
continue;
}

$node->setAttribute(self::ATTRIBUTE_NAME, true);
}

return null;
}

}
3 changes: 3 additions & 0 deletions src/Parser/RichParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ public function parseString(string $sourceCode): array
$nodeTraverser->addVisitor($traitCollectingVisitor);

foreach ($this->container->getServicesByTag(self::VISITOR_SERVICE_TAG) as $visitor) {
if ($visitor instanceof TokenAwareVisitor) {
$visitor->setTokens($tokens);
}
$nodeTraverser->addVisitor($visitor);
}

Expand Down
20 changes: 20 additions & 0 deletions src/Parser/TokenAwareVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types = 1);

namespace PHPStan\Parser;

use PhpParser\Token;

/**
* A node visitor that is aware of the parser tokens.
*
* @api
*/
interface TokenAwareVisitor
{

/**
* @param Token[] $tokens
*/
public function setTokens(array $tokens): void;

}
5 changes: 5 additions & 0 deletions src/Php/PhpVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,11 @@ public function deprecatesDynamicProperties(): bool
return $this->versionId >= 80200;
}

public function deprecatesStringInterpolation(): bool
{
return $this->versionId >= 80200;
}

public function strSplitReturnsEmptyArray(): bool
{
return $this->versionId >= 80200;
Expand Down
68 changes: 68 additions & 0 deletions src/Rules/String/InterpolatedStringRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\String;

use PhpParser\Node;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Scalar\InterpolatedString;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\RegisteredRule;
use PHPStan\Parser\DeprecatedInterpolatedStringVisitor;
use PHPStan\Php\PhpVersion;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use function is_string;

/**
* @implements Rule<InterpolatedString>
*/
#[RegisteredRule(level: 0)]
final class InterpolatedStringRule implements Rule
{

public function __construct(
private PhpVersion $phpVersion,
)
{
}

public function getNodeType(): string
{
return InterpolatedString::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!$this->phpVersion->deprecatesStringInterpolation()) {
return [];
}

if ($node->getAttribute(DeprecatedInterpolatedStringVisitor::ATTRIBUTE_NAME) !== true) {
return [];
}

foreach ($node->parts as $part) {
if (!$part instanceof Variable && !($part instanceof ArrayDimFetch && $part->var instanceof Variable)) {
continue;
}

if ($part instanceof ArrayDimFetch || (is_string($part->name))) {
$deprecatedMessage = 'Using ${var} in strings is deprecated in PHP 8.2. Use {$var} instead.';
} else {
$deprecatedMessage = 'Using ${expr} (variable variables) in strings is deprecated in PHP 8.2. Use {${expr}} instead.';
}

return [
RuleErrorBuilder::message($deprecatedMessage)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's a deprecation, and not a real error, should it be in deprecation-rules instead ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially, my finding was that the rule needs access to the parser tokens in some form to check the original syntax.

So as long as that API is exposed in the main phpstan library, then the rule could be moved into deprecation-rules if that's the most sensible part for it.

->identifier('interpolatedstring.deprecated')
->line($node->getStartLine())
->fixNode($node, static fn () => new InterpolatedString($node->parts, $node->getAttributes()))
->build(),
];
}

return [];
}

}
55 changes: 55 additions & 0 deletions tests/PHPStan/Rules/String/InterpolatedStringRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\String;

use PHPStan\Php\PhpVersion;
use PHPStan\Rules\Rule as TRule;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\RequiresPhp;
use const PHP_VERSION_ID;

/**
* @extends RuleTestCase<InterpolatedStringRule>
*/
class InterpolatedStringRuleTest extends RuleTestCase
{

protected function getRule(): TRule
{
return new InterpolatedStringRule(new PhpVersion(PHP_VERSION_ID));
}

#[RequiresPhp('>= 8.2')]
public function testRule(): void
{
$this->analyse([__DIR__ . '/data/interpolated-string.php'], [
[
'Using ${var} in strings is deprecated in PHP 8.2. Use {$var} instead.',
17,
],
[
'Using ${expr} (variable variables) in strings is deprecated in PHP 8.2. Use {${expr}} instead.',
18,
],
[
'Using ${expr} (variable variables) in strings is deprecated in PHP 8.2. Use {${expr}} instead.',
19,
],
[
'Using ${var} in strings is deprecated in PHP 8.2. Use {$var} instead.',
20,
],
[
'Using ${expr} (variable variables) in strings is deprecated in PHP 8.2. Use {${expr}} instead.',
23,
],
]);
}

#[RequiresPhp('>= 8.2')]
public function testFix(): void
{
$this->fix(__DIR__ . '/data/interpolated-string.php', __DIR__ . '/data/interpolated-string.php.fixed');
}

}
23 changes: 23 additions & 0 deletions tests/PHPStan/Rules/String/data/interpolated-string.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php // lint >= 8.2

namespace Bug8744Traits;

class Test
{
public function getMethod()
{
return 'foo';
}
}

const foo = 'bar';
$foo = 'foo';
$bar = 'bar';
$array = ['baz'];
var_dump("${foo}");
var_dump("${(foo)}");
var_dump("${$foo}");
var_dump("${array[0]}");

$object = new Test();
var_dump("${$object->getMethod()}");
23 changes: 23 additions & 0 deletions tests/PHPStan/Rules/String/data/interpolated-string.php.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php // lint >= 8.2

namespace Bug8744Traits;

class Test
{
public function getMethod()
{
return 'foo';
}
}

const foo = 'bar';
$foo = 'foo';
$bar = 'bar';
$array = ['baz'];
var_dump("{$foo}");
var_dump("{${foo}}");
var_dump("{${$foo}}");
var_dump("{$array[0]}");

$object = new Test();
var_dump("{${$object->getMethod()}}");
Loading