From 27476be04d1c04efea0b74d9ee649f4385d8a9ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tinjo=20Sch=C3=B6ni?= <32767367+tscni@users.noreply.github.com> Date: Sat, 17 Aug 2024 04:00:02 +0200 Subject: [PATCH 1/4] Support multiple anonymous class definitions on the same line --- src/Broker/AnonymousClassNameHelper.php | 2 +- .../Analyser/AnalyserIntegrationTest.php | 21 +++++++++ .../Analyser/LegacyNodeScopeResolverTest.php | 44 +++++++++++++++++-- .../data/anonymous-class-name-same-line.php | 5 +++ tests/PHPStan/Analyser/data/bug-11511.php | 10 +++++ tests/PHPStan/Analyser/data/bug-5597.php | 25 +++++++++++ 6 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/anonymous-class-name-same-line.php create mode 100644 tests/PHPStan/Analyser/data/bug-11511.php create mode 100644 tests/PHPStan/Analyser/data/bug-5597.php diff --git a/src/Broker/AnonymousClassNameHelper.php b/src/Broker/AnonymousClassNameHelper.php index 20e3087b00..bda7cd1a5c 100644 --- a/src/Broker/AnonymousClassNameHelper.php +++ b/src/Broker/AnonymousClassNameHelper.php @@ -34,7 +34,7 @@ public function getAnonymousClassName( return sprintf( 'AnonymousClass%s', - md5(sprintf('%s:%s', $filename, $classNode->getStartLine())), + md5(sprintf('%s:%s', $filename, $classNode->getStartFilePos())), ); } diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 442e5863fe..b84ff5feb0 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1416,6 +1416,27 @@ public function testBug11297(): void $this->assertNoErrors($errors); } + public function testBug5597(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-5597.php'); + $this->assertNoErrors($errors); + } + + public function testBug11511(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Test requires PHP 8.0.'); + } + + $errors = $this->runAnalyse(__DIR__ . '/data/bug-11511.php'); + $this->assertCount(1, $errors); + $this->assertSame('Access to an undefined property object::$bar.', $errors[0]->getMessage()); + } + /** * @param string[]|null $allAnalysedFiles * @return Error[] diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 08cbf6d7a5..e5f4f120bf 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -8335,12 +8335,12 @@ public function dataAnonymousClass(): array { return [ [ - '$this(AnonymousClass3301acd9e9d13ba9bbce9581cdb00699)', + '$this(AnonymousClass6a0687bc4f876de22e6d370597168d67)', '$this', "'inside'", ], [ - 'AnonymousClass3301acd9e9d13ba9bbce9581cdb00699', + 'AnonymousClass6a0687bc4f876de22e6d370597168d67', '$foo', "'outside'", ], @@ -8388,7 +8388,7 @@ public function dataAnonymousClassInTrait(): array { return [ [ - '$this(AnonymousClass3de0a9734314db9dec21ba308363ff9a)', + '$this(AnonymousClassa90f7ae5a3564e08aca97d6fbb39c2b2)', '$this', ], ]; @@ -8409,6 +8409,44 @@ public function testAnonymousClassNameInTrait( ); } + public function dataAnonymousClassNameSameLine(): array + { + return [ + [ + 'AnonymousClass6540444db24e3b8821f292cc08bb9b6c', + '$foo', + '$bar', + ], + [ + 'AnonymousClass7c37a0e958f6b76cfeb23acfa3259ff8', + '$bar', + '$baz', + ], + [ + 'AnonymousClassca84fbe02c775710b2735bfe3cbfbb7e', + '$baz', + 'die', + ], + ]; + } + + /** + * @dataProvider dataAnonymousClassNameSameLine + */ + public function testAnonymousClassNameSameLine( + string $description, + string $expression, + string $evaluatedPointExpression, + ): void + { + $this->assertTypes( + __DIR__ . '/data/anonymous-class-name-same-line.php', + $description, + $expression, + $evaluatedPointExpression, + ); + } + public function dataDynamicConstants(): array { return [ diff --git a/tests/PHPStan/Analyser/data/anonymous-class-name-same-line.php b/tests/PHPStan/Analyser/data/anonymous-class-name-same-line.php new file mode 100644 index 0000000000..16a95e7b5f --- /dev/null +++ b/tests/PHPStan/Analyser/data/anonymous-class-name-same-line.php @@ -0,0 +1,5 @@ += 8.0 + +namespace Bug11511; + +$myObject = new class (new class { public string $bar = 'test'; }) { + public function __construct(public object $foo) + { + } +}; +echo $myObject->foo->bar; diff --git a/tests/PHPStan/Analyser/data/bug-5597.php b/tests/PHPStan/Analyser/data/bug-5597.php new file mode 100644 index 0000000000..19720c8a17 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5597.php @@ -0,0 +1,25 @@ += 8.0 + +namespace Bug5597; + +interface InterfaceA {} + +class ClassA implements InterfaceA {} + +class ClassB +{ + public function __construct( + private InterfaceA $parameterA, + ) { + } + + public function test() : InterfaceA + { + return $this->parameterA; + } +} + +$classA = new class() extends ClassA {}; +$thisWorks = new class($classA) extends ClassB {}; + +$thisFailsWithTwoErrors = new class(new class() extends ClassA {}) extends ClassB {}; From d04bd26f466a439c5bec1f8f74351a134757d1bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tinjo=20Sch=C3=B6ni?= <32767367+tscni@users.noreply.github.com> Date: Mon, 19 Aug 2024 17:20:02 +0200 Subject: [PATCH 2/4] Use a line-based index to disambiguate anonymous classes --- conf/config.neon | 5 ++ src/Broker/AnonymousClassNameHelper.php | 11 ++++- src/Parser/AnonymousClassVisitor.php | 49 +++++++++++++++++++ .../BetterReflectionProvider.php | 11 ++++- .../Analyser/LegacyNodeScopeResolverTest.php | 12 ++--- 5 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 src/Parser/AnonymousClassVisitor.php diff --git a/conf/config.neon b/conf/config.neon index c85cce945d..291572b81d 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -311,6 +311,11 @@ services: options: preserveOriginalNames: true + - + class: PHPStan\Parser\AnonymousClassVisitor + tags: + - phpstan.parser.richParserNodeVisitor + - class: PHPStan\Parser\ArrayFilterArgVisitor tags: diff --git a/src/Broker/AnonymousClassNameHelper.php b/src/Broker/AnonymousClassNameHelper.php index bda7cd1a5c..a09a15e760 100644 --- a/src/Broker/AnonymousClassNameHelper.php +++ b/src/Broker/AnonymousClassNameHelper.php @@ -5,6 +5,7 @@ use PhpParser\Node; use PHPStan\File\FileHelper; use PHPStan\File\RelativePathHelper; +use PHPStan\Parser\AnonymousClassVisitor; use PHPStan\ShouldNotHappenException; use function md5; use function sprintf; @@ -32,9 +33,17 @@ public function getAnonymousClassName( $this->fileHelper->normalizePath($filename, '/'), ); + /** @var int|null $lineIndex */ + $lineIndex = $classNode->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX); + if ($lineIndex === null) { + $hash = md5(sprintf('%s:%s', $filename, $classNode->getStartLine())); + } else { + $hash = md5(sprintf('%s:%s:%d', $filename, $classNode->getStartLine(), $lineIndex)); + } + return sprintf( 'AnonymousClass%s', - md5(sprintf('%s:%s', $filename, $classNode->getStartFilePos())), + $hash, ); } diff --git a/src/Parser/AnonymousClassVisitor.php b/src/Parser/AnonymousClassVisitor.php new file mode 100644 index 0000000000..403bb40d01 --- /dev/null +++ b/src/Parser/AnonymousClassVisitor.php @@ -0,0 +1,49 @@ +> */ + private array $nodesPerLine = []; + + public function beforeTraverse(array $nodes): ?array + { + $this->nodesPerLine = []; + return null; + } + + public function enterNode(Node $node): ?Node + { + if (!$node instanceof Node\Stmt\Class_ || !$node->isAnonymous()) { + return null; + } + + $this->nodesPerLine[$node->getStartLine()][] = $node; + + return null; + } + + public function afterTraverse(array $nodes): ?array + { + foreach ($this->nodesPerLine as $nodesOnLine) { + if (count($nodesOnLine) === 1) { + continue; + } + for ($i = 0; $i < count($nodesOnLine); $i++) { + $nodesOnLine[$i]->setAttribute(self::ATTRIBUTE_LINE_INDEX, $i + 1); + } + } + + $this->nodesPerLine = []; + return null; + } + +} diff --git a/src/Reflection/BetterReflection/BetterReflectionProvider.php b/src/Reflection/BetterReflection/BetterReflectionProvider.php index 67166c39d1..cafa64d1b7 100644 --- a/src/Reflection/BetterReflection/BetterReflectionProvider.php +++ b/src/Reflection/BetterReflection/BetterReflectionProvider.php @@ -24,6 +24,7 @@ use PHPStan\File\FileHelper; use PHPStan\File\FileReader; use PHPStan\File\RelativePathHelper; +use PHPStan\Parser\AnonymousClassVisitor; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\StubPhpDocProvider; @@ -214,6 +215,14 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $ null, ); + /** @var int|null $classLineIndex */ + $classLineIndex = $classNode->getAttribute(AnonymousClassVisitor::ATTRIBUTE_LINE_INDEX); + if ($classLineIndex === null) { + $displayName = sprintf('class@anonymous/%s:%s', $filename, $classNode->getStartLine()); + } else { + $displayName = sprintf('class@anonymous/%s:%s:%d', $filename, $classNode->getStartLine(), $classLineIndex); + } + self::$anonymousClasses[$className] = new ClassReflection( $this->reflectionProviderProvider->getReflectionProvider(), $this->initializerExprTypeResolver, @@ -227,7 +236,7 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $ $this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsPropertyClassReflectionExtension(), $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsMethodsClassReflectionExtension(), - sprintf('class@anonymous/%s:%s', $filename, $classNode->getStartLine()), + $displayName, new ReflectionClass($reflectionClass), $scopeFile, null, diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index e5f4f120bf..cf6c1c1b3b 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -8335,12 +8335,12 @@ public function dataAnonymousClass(): array { return [ [ - '$this(AnonymousClass6a0687bc4f876de22e6d370597168d67)', + '$this(AnonymousClass3301acd9e9d13ba9bbce9581cdb00699)', '$this', "'inside'", ], [ - 'AnonymousClass6a0687bc4f876de22e6d370597168d67', + 'AnonymousClass3301acd9e9d13ba9bbce9581cdb00699', '$foo', "'outside'", ], @@ -8388,7 +8388,7 @@ public function dataAnonymousClassInTrait(): array { return [ [ - '$this(AnonymousClassa90f7ae5a3564e08aca97d6fbb39c2b2)', + '$this(AnonymousClass3de0a9734314db9dec21ba308363ff9a)', '$this', ], ]; @@ -8413,17 +8413,17 @@ public function dataAnonymousClassNameSameLine(): array { return [ [ - 'AnonymousClass6540444db24e3b8821f292cc08bb9b6c', + 'AnonymousClass0d7d08272ba2f0a6ef324bb65c679e02', '$foo', '$bar', ], [ - 'AnonymousClass7c37a0e958f6b76cfeb23acfa3259ff8', + 'AnonymousClass464f64cbdca25b4af842cae65615bca9', '$bar', '$baz', ], [ - 'AnonymousClassca84fbe02c775710b2735bfe3cbfbb7e', + 'AnonymousClassa9fb472ec9acc5cae3bee4355c296bfa', '$baz', 'die', ], From ab3d1bdc05903a40a4dde51ca9dc1e354a2445d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tinjo=20Sch=C3=B6ni?= <32767367+tscni@users.noreply.github.com> Date: Mon, 19 Aug 2024 17:24:27 +0200 Subject: [PATCH 3/4] Move anonymous class node attribute handling into AnonymousClassVisitor --- src/Analyser/NodeScopeResolver.php | 3 ++- src/Parser/AnonymousClassVisitor.php | 2 ++ src/Reflection/BetterReflection/BetterReflectionProvider.php | 1 - src/Type/FileTypeMapper.php | 5 +++-- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index dbd1418a1d..140dc7e02b 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -117,6 +117,7 @@ use PHPStan\Node\UnreachableStatementNode; use PHPStan\Node\VariableAssignNode; use PHPStan\Node\VarTagChangedExpressionTypeNode; +use PHPStan\Parser\AnonymousClassVisitor; use PHPStan\Parser\ArrowFunctionArgVisitor; use PHPStan\Parser\ClosureArgVisitor; use PHPStan\Parser\Parser; @@ -855,7 +856,7 @@ private function processStmtNode( if ($stmt->name === null) { throw new ShouldNotHappenException(); } - if ($stmt->getAttribute('anonymousClass', false) === false) { + if ($stmt->getAttribute(AnonymousClassVisitor::ATTRIBUTE_ANONYMOUS_CLASS, false) === false) { $classReflection = $this->reflectionProvider->getClass($stmt->name->toString()); } else { $classReflection = $this->reflectionProvider->getAnonymousClassReflection($stmt, $scope); diff --git a/src/Parser/AnonymousClassVisitor.php b/src/Parser/AnonymousClassVisitor.php index 403bb40d01..3a179206ba 100644 --- a/src/Parser/AnonymousClassVisitor.php +++ b/src/Parser/AnonymousClassVisitor.php @@ -9,6 +9,7 @@ class AnonymousClassVisitor extends NodeVisitorAbstract { + public const ATTRIBUTE_ANONYMOUS_CLASS = 'anonymousClass'; public const ATTRIBUTE_LINE_INDEX = 'anonymousClassLineIndex'; /** @var array> */ @@ -26,6 +27,7 @@ public function enterNode(Node $node): ?Node return null; } + $node->setAttribute(self::ATTRIBUTE_ANONYMOUS_CLASS, true); $this->nodesPerLine[$node->getStartLine()][] = $node; return null; diff --git a/src/Reflection/BetterReflection/BetterReflectionProvider.php b/src/Reflection/BetterReflection/BetterReflectionProvider.php index cafa64d1b7..cd18c1bfe3 100644 --- a/src/Reflection/BetterReflection/BetterReflectionProvider.php +++ b/src/Reflection/BetterReflection/BetterReflectionProvider.php @@ -202,7 +202,6 @@ public function getAnonymousClassReflection(Node\Stmt\Class_ $classNode, Scope $ $scopeFile, ); $classNode->name = new Node\Identifier($className); - $classNode->setAttribute('anonymousClass', true); if (isset(self::$anonymousClasses[$className])) { return self::$anonymousClasses[$className]; diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index 307be3446f..07df40e471 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -8,6 +8,7 @@ use PHPStan\BetterReflection\Util\GetLastDocComment; use PHPStan\Broker\AnonymousClassNameHelper; use PHPStan\File\FileHelper; +use PHPStan\Parser\AnonymousClassVisitor; use PHPStan\Parser\Parser; use PHPStan\PhpDoc\PhpDocNodeResolver; use PHPStan\PhpDoc\PhpDocStringResolver; @@ -260,7 +261,7 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA } $className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName); - } elseif ((bool) $node->getAttribute('anonymousClass', false)) { + } elseif ((bool) $node->getAttribute(AnonymousClassVisitor::ATTRIBUTE_ANONYMOUS_CLASS, false)) { $className = $node->name->name; } else { if ($traitFound) { @@ -451,7 +452,7 @@ function (Node $node) use ($fileName, $lookForTrait, $phpDocNodeMap, &$traitFoun } $className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName); - } elseif ((bool) $node->getAttribute('anonymousClass', false)) { + } elseif ((bool) $node->getAttribute(AnonymousClassVisitor::ATTRIBUTE_ANONYMOUS_CLASS, false)) { $className = $node->name->name; } else { if ($traitFound) { From d76492591a0172d59f0d72bc630ebea7d157a8ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tinjo=20Sch=C3=B6ni?= <32767367+tscni@users.noreply.github.com> Date: Thu, 22 Aug 2024 03:28:02 +0200 Subject: [PATCH 4/4] Test anonymous class reflection names --- .../AnonymousClassReflectionTest.php | 105 ++++++++++++++++++ .../Reflection/data/anonymous-classes.php | 13 +++ 2 files changed, 118 insertions(+) create mode 100644 tests/PHPStan/Reflection/AnonymousClassReflectionTest.php create mode 100644 tests/PHPStan/Reflection/data/anonymous-classes.php diff --git a/tests/PHPStan/Reflection/AnonymousClassReflectionTest.php b/tests/PHPStan/Reflection/AnonymousClassReflectionTest.php new file mode 100644 index 0000000000..6a07961d4e --- /dev/null +++ b/tests/PHPStan/Reflection/AnonymousClassReflectionTest.php @@ -0,0 +1,105 @@ +> + */ +class AnonymousClassReflectionTest extends RuleTestCase +{ + + /** + * @return Rule + */ + protected function getRule(): Rule + { + return new /** @implements Rule */ class (self::createReflectionProvider()) implements Rule { + + public function __construct(private ReflectionProvider $reflectionProvider) + { + } + + public function getNodeType(): string + { + return Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!(bool) $node->getAttribute(AnonymousClassVisitor::ATTRIBUTE_ANONYMOUS_CLASS)) { + return []; + } + + $classReflection = $this->reflectionProvider->getAnonymousClassReflection($node, $scope); + + return [ + RuleErrorBuilder::message(sprintf( + "name: %s\ndisplay name: %s", + $classReflection->getName(), + $classReflection->getDisplayName(), + ))->identifier('test.anonymousClassReflection')->build(), + ]; + } + + }; + } + + public function testReflection(): void + { + $this->analyse([__DIR__ . '/data/anonymous-classes.php'], [ + [ + implode("\n", [ + 'name: AnonymousClass0c307d7b8501323d1d30b0afea7e0578', + 'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:5', + ]), + 5, + ], + [ + implode("\n", [ + 'name: AnonymousClassa16017c480192f8fbf3c03e17840e99c', + 'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:7:1', + ]), + 7, + ], + [ + implode("\n", [ + 'name: AnonymousClassd68d75f1cdac379350e3027c09a7c5a0', + 'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:7:2', + ]), + 7, + ], + [ + implode("\n", [ + 'name: AnonymousClass75aa798fed4f30306c14dcf03a50878c', + 'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:7:3', + ]), + 7, + ], + [ + implode("\n", [ + 'name: AnonymousClass4fcabdc52bfed5f8c101f3f89b2180bd', + 'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:9:1', + ]), + 9, + ], + [ + implode("\n", [ + 'name: AnonymousClass0e77d7995f4c47dcd5402817970fd7e0', + 'display name: class@anonymous/tests/PHPStan/Reflection/data/anonymous-classes.php:9:2', + ]), + 9, + ], + ]); + } + +} diff --git a/tests/PHPStan/Reflection/data/anonymous-classes.php b/tests/PHPStan/Reflection/data/anonymous-classes.php new file mode 100644 index 0000000000..9336316ff9 --- /dev/null +++ b/tests/PHPStan/Reflection/data/anonymous-classes.php @@ -0,0 +1,13 @@ +