From f059229a97af970e00367772b36f16539672e8ed Mon Sep 17 00:00:00 2001 From: codebymikey <9484406+codebymikey@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:35:50 +0000 Subject: [PATCH 1/2] Fix deprecated string interpolation syntax --- UPGRADING.md | 2 + src/Analyser/FileAnalyser.php | 3 +- src/Analyser/MutatingScope.php | 6 ++ src/Analyser/Scope.php | 6 ++ src/Analyser/ScopeContext.php | 35 +++++++-- src/Parser/CachedParser.php | 28 +++++++ src/Parser/CleaningParser.php | 5 ++ src/Parser/Parser.php | 8 ++ src/Parser/PathRoutingParser.php | 19 +++++ src/Parser/RichParser.php | 8 ++ src/Parser/SimpleParser.php | 8 ++ src/Parser/StubParser.php | 8 ++ src/Php/PhpVersion.php | 5 ++ src/Rules/String/InterpolatedStringRule.php | 73 +++++++++++++++++++ .../String/InterpolatedStringRuleTest.php | 55 ++++++++++++++ .../Rules/String/data/interpolated-string.php | 23 ++++++ .../String/data/interpolated-string.php.fixed | 23 ++++++ 17 files changed, 308 insertions(+), 7 deletions(-) create mode 100644 src/Rules/String/InterpolatedStringRule.php create mode 100644 tests/PHPStan/Rules/String/InterpolatedStringRuleTest.php create mode 100644 tests/PHPStan/Rules/String/data/interpolated-string.php create mode 100644 tests/PHPStan/Rules/String/data/interpolated-string.php.fixed diff --git a/UPGRADING.md b/UPGRADING.md index 1b76768ec4..88fe6c536d 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -336,3 +336,5 @@ Remove `Type::isSuperTypeOfWithReason()`, `Type:isSuperTypeOf()` return type cha * `ClassPropertyNode::getNativeType()` return type changed from AST node to `Type|null` * Class `PHPStan\Node\ClassMethod` (accessible from `ClassMethodsNode`) is no longer an AST node * Call `PHPStan\Node\ClassMethod::getNode()` to access the original AST node +* Interface `PHPStan\Analyser\Scope` introduces a new `getTokens()` method which returns the tokens for the current file. +* Interface `PHPStan\Parser\Parser` introduces a new `getTokens()` method which returns the tokens for the parsed file. diff --git a/src/Analyser/FileAnalyser.php b/src/Analyser/FileAnalyser.php index 945f67b6b6..cf6c0ebe78 100644 --- a/src/Analyser/FileAnalyser.php +++ b/src/Analyser/FileAnalyser.php @@ -99,6 +99,7 @@ public function analyseFile( try { $this->collectErrors($analysedFiles); $parserNodes = $this->parser->parseFile($file); + $parserTokens = $this->parser->getTokens(); $processedFiles[] = $file; $nodeCallback = new FileAnalyserCallback( @@ -114,7 +115,7 @@ public function analyseFile( $this->ruleErrorTransformer, $processedFiles, ); - $scope = $this->scopeFactory->create(ScopeContext::create($file), $nodeCallback); + $scope = $this->scopeFactory->create(ScopeContext::create($file, $parserTokens), $nodeCallback); $nodeCallback(new FileNode($parserNodes), $scope); $this->nodeScopeResolver->processNodes( $parserNodes, diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 296e3f1e7e..657cadc533 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -300,6 +300,12 @@ public function getFile(): string return $this->context->getFile(); } + /** @api */ + public function getTokens(): array + { + return $this->context->getTokens(); + } + /** @api */ public function getFileDescription(): string { diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index 626e9db7ff..882947ec77 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -6,6 +6,7 @@ use PhpParser\Node\Expr; use PhpParser\Node\Name; use PhpParser\Node\Param; +use PhpParser\Token; use PHPStan\Php\PhpVersions; use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; @@ -65,6 +66,11 @@ interface Scope extends ClassMemberAccessAnswerer, NamespaceAnswerer */ public function getFile(): string; + /** + * @return Token[] + */ + public function getTokens(): array; + /** * For traits, returns the trait file path with the using class context, * e.g. "TraitFile.php (in context of class MyClass)". diff --git a/src/Analyser/ScopeContext.php b/src/Analyser/ScopeContext.php index fd23625611..1900b8bdf2 100644 --- a/src/Analyser/ScopeContext.php +++ b/src/Analyser/ScopeContext.php @@ -2,29 +2,44 @@ namespace PHPStan\Analyser; +use PhpParser\Token; use PHPStan\Reflection\ClassReflection; use PHPStan\ShouldNotHappenException; final class ScopeContext { + /** + * @param string $file + * @param \PHPStan\Reflection\ClassReflection|null $classReflection + * @param \PHPStan\Reflection\ClassReflection|null $traitReflection + * @param Token[] $tokens + */ private function __construct( private string $file, private ?ClassReflection $classReflection, private ?ClassReflection $traitReflection, + private array $tokens, ) { } - /** @api */ - public static function create(string $file): self + /** + * @param string $file + * @param Token[] $tokens + * + * @return self + * + * @api + */ + public static function create(string $file, array $tokens = []): self { - return new self($file, classReflection: null, traitReflection: null); + return new self($file, classReflection: null, traitReflection: null, tokens: $tokens); } public function beginFile(): self { - return new self($this->file, classReflection: null, traitReflection: null); + return new self($this->file, classReflection: null, traitReflection: null, tokens: $this->tokens); } public function enterClass(ClassReflection $classReflection): self @@ -35,7 +50,7 @@ public function enterClass(ClassReflection $classReflection): self if ($classReflection->isTrait()) { throw new ShouldNotHappenException(); } - return new self($this->file, $classReflection, traitReflection: null); + return new self($this->file, $classReflection, traitReflection: null, tokens: $this->tokens); } public function enterTrait(ClassReflection $traitReflection): self @@ -47,7 +62,7 @@ public function enterTrait(ClassReflection $traitReflection): self throw new ShouldNotHappenException(); } - return new self($this->file, $this->classReflection, $traitReflection); + return new self($this->file, $this->classReflection, $traitReflection, $this->tokens); } public function equals(self $otherContext): bool @@ -90,4 +105,12 @@ public function getTraitReflection(): ?ClassReflection return $this->traitReflection; } + /** + * @return Token[] + */ + public function getTokens(): array + { + return $this->tokens; + } + } diff --git a/src/Parser/CachedParser.php b/src/Parser/CachedParser.php index 400c21bf5a..c0f3de7dd3 100644 --- a/src/Parser/CachedParser.php +++ b/src/Parser/CachedParser.php @@ -3,6 +3,7 @@ namespace PHPStan\Parser; use PhpParser\Node; +use PhpParser\Token; use PHPStan\File\FileReader; use function array_slice; @@ -12,6 +13,11 @@ final class CachedParser implements Parser /** @var array*/ private array $cachedNodesByString = []; + /** @var array*/ + private array $cachedTokensByString = []; + + private ?string $lastParsedSourceCode = null; + private int $cachedNodesByStringCount = 0; /** @var array */ @@ -36,6 +42,11 @@ public function parseFile(string $file): array 1, preserve_keys: true, ); + $this->cachedTokensByString = array_slice( + $this->cachedTokensByString, + 1, + preserve_keys: true, + ); --$this->cachedNodesByStringCount; } @@ -43,10 +54,12 @@ public function parseFile(string $file): array $sourceCode = FileReader::read($file); if (!isset($this->cachedNodesByString[$sourceCode]) || isset($this->parsedByString[$sourceCode])) { $this->cachedNodesByString[$sourceCode] = $this->originalParser->parseFile($file); + $this->cachedTokensByString[$sourceCode] = $this->originalParser->getTokens(); $this->cachedNodesByStringCount++; unset($this->parsedByString[$sourceCode]); } + $this->lastParsedSourceCode = $sourceCode; return $this->cachedNodesByString[$sourceCode]; } @@ -61,19 +74,34 @@ public function parseString(string $sourceCode): array 1, preserve_keys: true, ); + $this->cachedTokensByString = array_slice( + $this->cachedTokensByString, + 1, + preserve_keys: true, + ); --$this->cachedNodesByStringCount; } if (!isset($this->cachedNodesByString[$sourceCode])) { $this->cachedNodesByString[$sourceCode] = $this->originalParser->parseString($sourceCode); + $this->cachedTokensByString[$sourceCode] = $this->originalParser->getTokens(); $this->cachedNodesByStringCount++; $this->parsedByString[$sourceCode] = true; } + $this->lastParsedSourceCode = $sourceCode; return $this->cachedNodesByString[$sourceCode]; } + public function getTokens(): array + { + if (isset($this->lastParsedSourceCode, $this->cachedTokensByString[$this->lastParsedSourceCode])) { + return $this->cachedTokensByString[$this->lastParsedSourceCode]; + } + return []; + } + public function getCachedNodesByStringCount(): int { return $this->cachedNodesByStringCount; diff --git a/src/Parser/CleaningParser.php b/src/Parser/CleaningParser.php index 0f874eafbf..2ec2aefbe5 100644 --- a/src/Parser/CleaningParser.php +++ b/src/Parser/CleaningParser.php @@ -28,6 +28,11 @@ public function parseString(string $sourceCode): array return $this->clean($this->wrappedParser->parseString($sourceCode)); } + public function getTokens(): array + { + return $this->wrappedParser->getTokens(); + } + /** * @param Stmt[] $ast * @return Stmt[] diff --git a/src/Parser/Parser.php b/src/Parser/Parser.php index 187d6875c6..a0ac26c92f 100644 --- a/src/Parser/Parser.php +++ b/src/Parser/Parser.php @@ -3,6 +3,7 @@ namespace PHPStan\Parser; use PhpParser\Node; +use PhpParser\Token; /** @api */ interface Parser @@ -21,4 +22,11 @@ public function parseFile(string $file): array; */ public function parseString(string $sourceCode): array; + /** + * Return tokens for the last parse. + * + * @return Token[] + */ + public function getTokens(): array; + } diff --git a/src/Parser/PathRoutingParser.php b/src/Parser/PathRoutingParser.php index 53040aa27f..94317a9b5b 100644 --- a/src/Parser/PathRoutingParser.php +++ b/src/Parser/PathRoutingParser.php @@ -18,6 +18,8 @@ final class PathRoutingParser implements Parser private ?string $singleReflectionFile; + private ?Parser $usedParser = null; + /** @var array filePath(string) => bool(true) */ private array $analysedFiles = []; @@ -44,9 +46,11 @@ public function parseFile(string $file): array { $normalizedPath = $this->fileHelper->normalizePath($file, '/'); if (str_contains($normalizedPath, 'vendor/jetbrains/phpstorm-stubs')) { + $this->usedParser = $this->php8Parser; return $this->php8Parser->parseFile($file); } if (str_contains($normalizedPath, 'vendor/phpstan/php-8-stubs/stubs')) { + $this->usedParser = $this->php8Parser; return $this->php8Parser->parseFile($file); } @@ -64,21 +68,36 @@ public function parseFile(string $file): array if ($realFilePath !== false) { $normalizedRealFilePath = $this->fileHelper->normalizePath($realFilePath); if (isset($this->analysedFiles[$normalizedRealFilePath])) { + $this->usedParser = $this->currentPhpVersionRichParser; return $this->currentPhpVersionRichParser->parseFile($file); } } break; } + $this->usedParser = $this->currentPhpVersionSimpleParser; return $this->currentPhpVersionSimpleParser->parseFile($file); } + $this->usedParser = $this->currentPhpVersionRichParser; return $this->currentPhpVersionRichParser->parseFile($file); } public function parseString(string $sourceCode): array { + $this->usedParser = $this->currentPhpVersionSimpleParser; return $this->currentPhpVersionSimpleParser->parseString($sourceCode); } + /** + * {@inheritdoc} + */ + public function getTokens(): array + { + if ($this->usedParser !== null) { + return $this->usedParser->getTokens(); + } + return []; + } + } diff --git a/src/Parser/RichParser.php b/src/Parser/RichParser.php index 4d3f0bdb4b..d5cacf3598 100644 --- a/src/Parser/RichParser.php +++ b/src/Parser/RichParser.php @@ -123,6 +123,14 @@ public function parseString(string $sourceCode): array return $nodes; } + /** + * {@inheritdoc} + */ + public function getTokens(): array + { + return $this->parser->getTokens(); + } + /** * @param Token[] $tokens * @return array{lines: array|null>, errors: array>} diff --git a/src/Parser/SimpleParser.php b/src/Parser/SimpleParser.php index 713c1502ef..e59a8153e8 100644 --- a/src/Parser/SimpleParser.php +++ b/src/Parser/SimpleParser.php @@ -53,4 +53,12 @@ public function parseString(string $sourceCode): array return $nodeTraverser->traverse($nodes); } + /** + * {@inheritdoc} + */ + public function getTokens(): array + { + return $this->parser->getTokens(); + } + } diff --git a/src/Parser/StubParser.php b/src/Parser/StubParser.php index d98a2cc721..4ea7f58b67 100644 --- a/src/Parser/StubParser.php +++ b/src/Parser/StubParser.php @@ -53,4 +53,12 @@ public function parseString(string $sourceCode): array return $nodeTraverser->traverse($nodes); } + /** + * {@inheritdoc} + */ + public function getTokens(): array + { + return $this->parser->getTokens(); + } + } diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index 21945b2b38..868557443e 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -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; diff --git a/src/Rules/String/InterpolatedStringRule.php b/src/Rules/String/InterpolatedStringRule.php new file mode 100644 index 0000000000..c142efc087 --- /dev/null +++ b/src/Rules/String/InterpolatedStringRule.php @@ -0,0 +1,73 @@ + + */ +#[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 []; + } + + $sourceTokens = $scope->getTokens(); + foreach ($node->parts as $part) { + if (!$part instanceof Variable && !($part instanceof ArrayDimFetch && $part->var instanceof Variable)) { + continue; + } + $startTokenPos = $part->getStartTokenPos(); + if (!isset($sourceTokens[$startTokenPos])) { + continue; + } + $startToken = (string) $sourceTokens[$startTokenPos]; + if ($startToken !== '${') { + 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()}}"); From 4f209d66763dfe0683988c2a279add58c6814857 Mon Sep 17 00:00:00 2001 From: codebymikey <9484406+codebymikey@users.noreply.github.com> Date: Wed, 25 Feb 2026 09:13:12 +0000 Subject: [PATCH 2/2] Use a node visitor to flag the deprecated interpolated strings --- UPGRADING.md | 2 - src/Analyser/FileAnalyser.php | 3 +- src/Analyser/MutatingScope.php | 6 -- src/Analyser/Scope.php | 6 -- src/Analyser/ScopeContext.php | 35 ++---------- src/Parser/CachedParser.php | 28 ---------- src/Parser/CleaningParser.php | 5 -- .../DeprecatedInterpolatedStringVisitor.php | 56 +++++++++++++++++++ src/Parser/Parser.php | 8 --- src/Parser/PathRoutingParser.php | 19 ------- src/Parser/RichParser.php | 11 +--- src/Parser/SimpleParser.php | 8 --- src/Parser/StubParser.php | 8 --- src/Parser/TokenAwareVisitor.php | 20 +++++++ src/Rules/String/InterpolatedStringRule.php | 15 ++--- 15 files changed, 91 insertions(+), 139 deletions(-) create mode 100644 src/Parser/DeprecatedInterpolatedStringVisitor.php create mode 100644 src/Parser/TokenAwareVisitor.php diff --git a/UPGRADING.md b/UPGRADING.md index 88fe6c536d..1b76768ec4 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -336,5 +336,3 @@ Remove `Type::isSuperTypeOfWithReason()`, `Type:isSuperTypeOf()` return type cha * `ClassPropertyNode::getNativeType()` return type changed from AST node to `Type|null` * Class `PHPStan\Node\ClassMethod` (accessible from `ClassMethodsNode`) is no longer an AST node * Call `PHPStan\Node\ClassMethod::getNode()` to access the original AST node -* Interface `PHPStan\Analyser\Scope` introduces a new `getTokens()` method which returns the tokens for the current file. -* Interface `PHPStan\Parser\Parser` introduces a new `getTokens()` method which returns the tokens for the parsed file. diff --git a/src/Analyser/FileAnalyser.php b/src/Analyser/FileAnalyser.php index cf6c0ebe78..945f67b6b6 100644 --- a/src/Analyser/FileAnalyser.php +++ b/src/Analyser/FileAnalyser.php @@ -99,7 +99,6 @@ public function analyseFile( try { $this->collectErrors($analysedFiles); $parserNodes = $this->parser->parseFile($file); - $parserTokens = $this->parser->getTokens(); $processedFiles[] = $file; $nodeCallback = new FileAnalyserCallback( @@ -115,7 +114,7 @@ public function analyseFile( $this->ruleErrorTransformer, $processedFiles, ); - $scope = $this->scopeFactory->create(ScopeContext::create($file, $parserTokens), $nodeCallback); + $scope = $this->scopeFactory->create(ScopeContext::create($file), $nodeCallback); $nodeCallback(new FileNode($parserNodes), $scope); $this->nodeScopeResolver->processNodes( $parserNodes, diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 657cadc533..296e3f1e7e 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -300,12 +300,6 @@ public function getFile(): string return $this->context->getFile(); } - /** @api */ - public function getTokens(): array - { - return $this->context->getTokens(); - } - /** @api */ public function getFileDescription(): string { diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index 882947ec77..626e9db7ff 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -6,7 +6,6 @@ use PhpParser\Node\Expr; use PhpParser\Node\Name; use PhpParser\Node\Param; -use PhpParser\Token; use PHPStan\Php\PhpVersions; use PHPStan\Reflection\ClassConstantReflection; use PHPStan\Reflection\ClassMemberAccessAnswerer; @@ -66,11 +65,6 @@ interface Scope extends ClassMemberAccessAnswerer, NamespaceAnswerer */ public function getFile(): string; - /** - * @return Token[] - */ - public function getTokens(): array; - /** * For traits, returns the trait file path with the using class context, * e.g. "TraitFile.php (in context of class MyClass)". diff --git a/src/Analyser/ScopeContext.php b/src/Analyser/ScopeContext.php index 1900b8bdf2..fd23625611 100644 --- a/src/Analyser/ScopeContext.php +++ b/src/Analyser/ScopeContext.php @@ -2,44 +2,29 @@ namespace PHPStan\Analyser; -use PhpParser\Token; use PHPStan\Reflection\ClassReflection; use PHPStan\ShouldNotHappenException; final class ScopeContext { - /** - * @param string $file - * @param \PHPStan\Reflection\ClassReflection|null $classReflection - * @param \PHPStan\Reflection\ClassReflection|null $traitReflection - * @param Token[] $tokens - */ private function __construct( private string $file, private ?ClassReflection $classReflection, private ?ClassReflection $traitReflection, - private array $tokens, ) { } - /** - * @param string $file - * @param Token[] $tokens - * - * @return self - * - * @api - */ - public static function create(string $file, array $tokens = []): self + /** @api */ + public static function create(string $file): self { - return new self($file, classReflection: null, traitReflection: null, tokens: $tokens); + return new self($file, classReflection: null, traitReflection: null); } public function beginFile(): self { - return new self($this->file, classReflection: null, traitReflection: null, tokens: $this->tokens); + return new self($this->file, classReflection: null, traitReflection: null); } public function enterClass(ClassReflection $classReflection): self @@ -50,7 +35,7 @@ public function enterClass(ClassReflection $classReflection): self if ($classReflection->isTrait()) { throw new ShouldNotHappenException(); } - return new self($this->file, $classReflection, traitReflection: null, tokens: $this->tokens); + return new self($this->file, $classReflection, traitReflection: null); } public function enterTrait(ClassReflection $traitReflection): self @@ -62,7 +47,7 @@ public function enterTrait(ClassReflection $traitReflection): self throw new ShouldNotHappenException(); } - return new self($this->file, $this->classReflection, $traitReflection, $this->tokens); + return new self($this->file, $this->classReflection, $traitReflection); } public function equals(self $otherContext): bool @@ -105,12 +90,4 @@ public function getTraitReflection(): ?ClassReflection return $this->traitReflection; } - /** - * @return Token[] - */ - public function getTokens(): array - { - return $this->tokens; - } - } diff --git a/src/Parser/CachedParser.php b/src/Parser/CachedParser.php index c0f3de7dd3..400c21bf5a 100644 --- a/src/Parser/CachedParser.php +++ b/src/Parser/CachedParser.php @@ -3,7 +3,6 @@ namespace PHPStan\Parser; use PhpParser\Node; -use PhpParser\Token; use PHPStan\File\FileReader; use function array_slice; @@ -13,11 +12,6 @@ final class CachedParser implements Parser /** @var array*/ private array $cachedNodesByString = []; - /** @var array*/ - private array $cachedTokensByString = []; - - private ?string $lastParsedSourceCode = null; - private int $cachedNodesByStringCount = 0; /** @var array */ @@ -42,11 +36,6 @@ public function parseFile(string $file): array 1, preserve_keys: true, ); - $this->cachedTokensByString = array_slice( - $this->cachedTokensByString, - 1, - preserve_keys: true, - ); --$this->cachedNodesByStringCount; } @@ -54,12 +43,10 @@ public function parseFile(string $file): array $sourceCode = FileReader::read($file); if (!isset($this->cachedNodesByString[$sourceCode]) || isset($this->parsedByString[$sourceCode])) { $this->cachedNodesByString[$sourceCode] = $this->originalParser->parseFile($file); - $this->cachedTokensByString[$sourceCode] = $this->originalParser->getTokens(); $this->cachedNodesByStringCount++; unset($this->parsedByString[$sourceCode]); } - $this->lastParsedSourceCode = $sourceCode; return $this->cachedNodesByString[$sourceCode]; } @@ -74,34 +61,19 @@ public function parseString(string $sourceCode): array 1, preserve_keys: true, ); - $this->cachedTokensByString = array_slice( - $this->cachedTokensByString, - 1, - preserve_keys: true, - ); --$this->cachedNodesByStringCount; } if (!isset($this->cachedNodesByString[$sourceCode])) { $this->cachedNodesByString[$sourceCode] = $this->originalParser->parseString($sourceCode); - $this->cachedTokensByString[$sourceCode] = $this->originalParser->getTokens(); $this->cachedNodesByStringCount++; $this->parsedByString[$sourceCode] = true; } - $this->lastParsedSourceCode = $sourceCode; return $this->cachedNodesByString[$sourceCode]; } - public function getTokens(): array - { - if (isset($this->lastParsedSourceCode, $this->cachedTokensByString[$this->lastParsedSourceCode])) { - return $this->cachedTokensByString[$this->lastParsedSourceCode]; - } - return []; - } - public function getCachedNodesByStringCount(): int { return $this->cachedNodesByStringCount; diff --git a/src/Parser/CleaningParser.php b/src/Parser/CleaningParser.php index 2ec2aefbe5..0f874eafbf 100644 --- a/src/Parser/CleaningParser.php +++ b/src/Parser/CleaningParser.php @@ -28,11 +28,6 @@ public function parseString(string $sourceCode): array return $this->clean($this->wrappedParser->parseString($sourceCode)); } - public function getTokens(): array - { - return $this->wrappedParser->getTokens(); - } - /** * @param Stmt[] $ast * @return Stmt[] 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/Parser.php b/src/Parser/Parser.php index a0ac26c92f..187d6875c6 100644 --- a/src/Parser/Parser.php +++ b/src/Parser/Parser.php @@ -3,7 +3,6 @@ namespace PHPStan\Parser; use PhpParser\Node; -use PhpParser\Token; /** @api */ interface Parser @@ -22,11 +21,4 @@ public function parseFile(string $file): array; */ public function parseString(string $sourceCode): array; - /** - * Return tokens for the last parse. - * - * @return Token[] - */ - public function getTokens(): array; - } diff --git a/src/Parser/PathRoutingParser.php b/src/Parser/PathRoutingParser.php index 94317a9b5b..53040aa27f 100644 --- a/src/Parser/PathRoutingParser.php +++ b/src/Parser/PathRoutingParser.php @@ -18,8 +18,6 @@ final class PathRoutingParser implements Parser private ?string $singleReflectionFile; - private ?Parser $usedParser = null; - /** @var array filePath(string) => bool(true) */ private array $analysedFiles = []; @@ -46,11 +44,9 @@ public function parseFile(string $file): array { $normalizedPath = $this->fileHelper->normalizePath($file, '/'); if (str_contains($normalizedPath, 'vendor/jetbrains/phpstorm-stubs')) { - $this->usedParser = $this->php8Parser; return $this->php8Parser->parseFile($file); } if (str_contains($normalizedPath, 'vendor/phpstan/php-8-stubs/stubs')) { - $this->usedParser = $this->php8Parser; return $this->php8Parser->parseFile($file); } @@ -68,36 +64,21 @@ public function parseFile(string $file): array if ($realFilePath !== false) { $normalizedRealFilePath = $this->fileHelper->normalizePath($realFilePath); if (isset($this->analysedFiles[$normalizedRealFilePath])) { - $this->usedParser = $this->currentPhpVersionRichParser; return $this->currentPhpVersionRichParser->parseFile($file); } } break; } - $this->usedParser = $this->currentPhpVersionSimpleParser; return $this->currentPhpVersionSimpleParser->parseFile($file); } - $this->usedParser = $this->currentPhpVersionRichParser; return $this->currentPhpVersionRichParser->parseFile($file); } public function parseString(string $sourceCode): array { - $this->usedParser = $this->currentPhpVersionSimpleParser; return $this->currentPhpVersionSimpleParser->parseString($sourceCode); } - /** - * {@inheritdoc} - */ - public function getTokens(): array - { - if ($this->usedParser !== null) { - return $this->usedParser->getTokens(); - } - return []; - } - } diff --git a/src/Parser/RichParser.php b/src/Parser/RichParser.php index d5cacf3598..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); } @@ -123,14 +126,6 @@ public function parseString(string $sourceCode): array return $nodes; } - /** - * {@inheritdoc} - */ - public function getTokens(): array - { - return $this->parser->getTokens(); - } - /** * @param Token[] $tokens * @return array{lines: array|null>, errors: array>} diff --git a/src/Parser/SimpleParser.php b/src/Parser/SimpleParser.php index e59a8153e8..713c1502ef 100644 --- a/src/Parser/SimpleParser.php +++ b/src/Parser/SimpleParser.php @@ -53,12 +53,4 @@ public function parseString(string $sourceCode): array return $nodeTraverser->traverse($nodes); } - /** - * {@inheritdoc} - */ - public function getTokens(): array - { - return $this->parser->getTokens(); - } - } diff --git a/src/Parser/StubParser.php b/src/Parser/StubParser.php index 4ea7f58b67..d98a2cc721 100644 --- a/src/Parser/StubParser.php +++ b/src/Parser/StubParser.php @@ -53,12 +53,4 @@ public function parseString(string $sourceCode): array return $nodeTraverser->traverse($nodes); } - /** - * {@inheritdoc} - */ - public function getTokens(): array - { - return $this->parser->getTokens(); - } - } 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 @@ +getTokens(); + 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; } - $startTokenPos = $part->getStartTokenPos(); - if (!isset($sourceTokens[$startTokenPos])) { - continue; - } - $startToken = (string) $sourceTokens[$startTokenPos]; - if ($startToken !== '${') { - continue; - } if ($part instanceof ArrayDimFetch || (is_string($part->name))) { $deprecatedMessage = 'Using ${var} in strings is deprecated in PHP 8.2. Use {$var} instead.';