From 7a786a41ff9c747650a9d3176cef6b8d713bde4b Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Tue, 17 Feb 2026 05:02:10 +0000 Subject: [PATCH 1/4] Fix circular @phpstan-import-type between classes making aliases unresolvable - When two classes have mutual @phpstan-import-type tags (FooType imports Bar from BarType, BarType imports Foo from FooType), the circular detection in ClassReflection::getTypeAliases() was too aggressive - The fix returns local type aliases (defined via @phpstan-type) when circular import resolution is detected, since local aliases don't depend on imports - Added regression test in tests/PHPStan/Rules/PhpDoc/data/bug-11463.php Closes https://github.com/phpstan/phpstan/issues/11463 --- src/Reflection/ClassReflection.php | 7 +++++ .../PhpDoc/IncompatiblePhpDocTypeRuleTest.php | 5 ++++ tests/PHPStan/Rules/PhpDoc/data/bug-11463.php | 28 +++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 tests/PHPStan/Rules/PhpDoc/data/bug-11463.php diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index a8c7913674..b67064b5ec 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -1376,6 +1376,13 @@ public function getTypeAliases(): array // prevent circular imports if (array_key_exists($this->getName(), self::$resolvingTypeAliasImports)) { + // Return only local type aliases to break the cycle. + // Imported aliases are not available yet, but local ones + // don't depend on other classes and can be returned safely. + $localAliases = array_map(static fn (TypeAliasTag $typeAliasTag): TypeAlias => $typeAliasTag->getTypeAlias(), $typeAliasTags); + if ($localAliases !== []) { + return $localAliases; + } throw new CircularTypeAliasDefinitionException(); } diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php index 44d03ede8a..5c2b248296 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php @@ -487,4 +487,9 @@ public function testBug9708(): void $this->analyse([__DIR__ . '/data/bug-9708.php'], []); } + public function testBug11463(): void + { + $this->analyse([__DIR__ . '/data/bug-11463.php'], []); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-11463.php b/tests/PHPStan/Rules/PhpDoc/data/bug-11463.php new file mode 100644 index 0000000000..09235324b2 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-11463.php @@ -0,0 +1,28 @@ + Date: Thu, 19 Feb 2026 16:06:31 +0100 Subject: [PATCH 2/4] Update ClassReflection.php --- src/Reflection/ClassReflection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index b67064b5ec..a41dc11827 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -1381,7 +1381,7 @@ public function getTypeAliases(): array // don't depend on other classes and can be returned safely. $localAliases = array_map(static fn (TypeAliasTag $typeAliasTag): TypeAlias => $typeAliasTag->getTypeAlias(), $typeAliasTags); if ($localAliases !== []) { - return $localAliases; + return $this->typeAliases = $localAliases; } throw new CircularTypeAliasDefinitionException(); } From a58a08ab94f1da264358d620060472370a04c41b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 19 Feb 2026 16:18:51 +0100 Subject: [PATCH 3/4] more tests --- src/Reflection/ClassReflection.php | 7 +-- .../PhpDoc/IncompatiblePhpDocTypeRuleTest.php | 5 ++ .../PHPStan/Rules/PhpDoc/data/bug-11463b.php | 46 +++++++++++++++++++ 3 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 tests/PHPStan/Rules/PhpDoc/data/bug-11463b.php diff --git a/src/Reflection/ClassReflection.php b/src/Reflection/ClassReflection.php index a41dc11827..e2a9d32d27 100644 --- a/src/Reflection/ClassReflection.php +++ b/src/Reflection/ClassReflection.php @@ -1373,13 +1373,10 @@ public function getTypeAliases(): array $typeAliasImportTags = $resolvedPhpDoc->getTypeAliasImportTags(); $typeAliasTags = $resolvedPhpDoc->getTypeAliasTags(); + $localAliases = array_map(static fn (TypeAliasTag $typeAliasTag): TypeAlias => $typeAliasTag->getTypeAlias(), $typeAliasTags); // prevent circular imports if (array_key_exists($this->getName(), self::$resolvingTypeAliasImports)) { - // Return only local type aliases to break the cycle. - // Imported aliases are not available yet, but local ones - // don't depend on other classes and can be returned safely. - $localAliases = array_map(static fn (TypeAliasTag $typeAliasTag): TypeAlias => $typeAliasTag->getTypeAlias(), $typeAliasTags); if ($localAliases !== []) { return $this->typeAliases = $localAliases; } @@ -1413,8 +1410,6 @@ public function getTypeAliases(): array unset(self::$resolvingTypeAliasImports[$this->getName()]); - $localAliases = array_map(static fn (TypeAliasTag $typeAliasTag): TypeAlias => $typeAliasTag->getTypeAlias(), $typeAliasTags); - $this->typeAliases = array_filter( array_merge($importedAliases, $localAliases), static fn (?TypeAlias $typeAlias): bool => $typeAlias !== null, diff --git a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php index 5c2b248296..247dcce92b 100644 --- a/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php @@ -492,4 +492,9 @@ public function testBug11463(): void $this->analyse([__DIR__ . '/data/bug-11463.php'], []); } + public function testBug11463b(): void + { + $this->analyse([__DIR__ . '/data/bug-11463b.php'], []); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-11463b.php b/tests/PHPStan/Rules/PhpDoc/data/bug-11463b.php new file mode 100644 index 0000000000..e19b034689 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-11463b.php @@ -0,0 +1,46 @@ + Date: Thu, 19 Feb 2026 16:42:16 +0100 Subject: [PATCH 4/4] another test --- .../Rules/Methods/CallMethodsRuleTest.php | 17 +++++++++++++++++ tests/PHPStan/Rules/PhpDoc/data/bug-11463.php | 8 ++++++++ 2 files changed, 25 insertions(+) diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index b7e36dbd96..2f5ce4e226 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3900,4 +3900,21 @@ public function testBug6120(): void $this->analyse([__DIR__ . '/data/bug-6120.php'], []); } + public function testBug11463(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/../PhpDoc/data/bug-11463.php'], [ + [ + "Parameter #1 \$bar of method Bug11463\FooType::foo() expects 'bar', 'bla' given.", + 32, + ], + [ + "Parameter #1 \$foo of method Bug11463\BarType::bar() expects 'foo', 'bla' given.", + 35, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-11463.php b/tests/PHPStan/Rules/PhpDoc/data/bug-11463.php index 09235324b2..2f5fc2e700 100644 --- a/tests/PHPStan/Rules/PhpDoc/data/bug-11463.php +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-11463.php @@ -26,3 +26,11 @@ class BarType { */ public function bar($foo): string { return $foo; } } + +function doFoo(FooType $foo, BarType $bar): void { + $foo->foo('bar'); + $foo->foo('bla'); + + $bar->bar('foo'); + $bar->bar('bla'); +}