diff --git a/src/Parser/DeprecatedInterpolatedStringVisitor.php b/src/Parser/DeprecatedInterpolatedStringVisitor.php new file mode 100644 index 0000000000..9f1d0f3f46 --- /dev/null +++ b/src/Parser/DeprecatedInterpolatedStringVisitor.php @@ -0,0 +1,56 @@ +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]; + if ($startToken !== '${') { + continue; + } + + $node->setAttribute(self::ATTRIBUTE_NAME, true); + } + + return null; + } + +} diff --git a/src/Parser/RichParser.php b/src/Parser/RichParser.php index 4d3f0bdb4b..4284395678 100644 --- a/src/Parser/RichParser.php +++ b/src/Parser/RichParser.php @@ -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); } diff --git a/src/Parser/TokenAwareVisitor.php b/src/Parser/TokenAwareVisitor.php new file mode 100644 index 0000000000..b23f99d046 --- /dev/null +++ b/src/Parser/TokenAwareVisitor.php @@ -0,0 +1,20 @@ +versionId >= 80200; } + public function deprecatesStringInterpolation(): bool + { + return $this->versionId >= 80200; + } + public function strSplitReturnsEmptyArray(): bool { return $this->versionId >= 80200; diff --git a/src/Rules/String/InterpolatedStringRule.php b/src/Rules/String/InterpolatedStringRule.php new file mode 100644 index 0000000000..c599dc679c --- /dev/null +++ b/src/Rules/String/InterpolatedStringRule.php @@ -0,0 +1,68 @@ + + */ +#[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) + ->identifier('interpolatedstring.deprecated') + ->line($node->getStartLine()) + ->fixNode($node, static fn () => new InterpolatedString($node->parts, $node->getAttributes())) + ->build(), + ]; + } + + return []; + } + +} diff --git a/tests/PHPStan/Rules/String/InterpolatedStringRuleTest.php b/tests/PHPStan/Rules/String/InterpolatedStringRuleTest.php new file mode 100644 index 0000000000..c4375b879a --- /dev/null +++ b/tests/PHPStan/Rules/String/InterpolatedStringRuleTest.php @@ -0,0 +1,55 @@ + + */ +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'); + } + +} diff --git a/tests/PHPStan/Rules/String/data/interpolated-string.php b/tests/PHPStan/Rules/String/data/interpolated-string.php new file mode 100644 index 0000000000..49d6a010f8 --- /dev/null +++ b/tests/PHPStan/Rules/String/data/interpolated-string.php @@ -0,0 +1,23 @@ += 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()}"); diff --git a/tests/PHPStan/Rules/String/data/interpolated-string.php.fixed b/tests/PHPStan/Rules/String/data/interpolated-string.php.fixed new file mode 100644 index 0000000000..7869e189f1 --- /dev/null +++ b/tests/PHPStan/Rules/String/data/interpolated-string.php.fixed @@ -0,0 +1,23 @@ += 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()}}");