From 77d9a41c2664f893ab425ce714ca07f48fd05c87 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 03:42:31 +0000 Subject: [PATCH 1/4] Fix phpstan/phpstan#11314: Template of imported type breaks imported type - When a class has both @phpstan-import-type and @template with the imported type as bound, resolving the template bound triggered a cyclic dependency in FileTypeMapper - The cycle occurred because resolving the type alias went through ClassReflection::getTypeAliases() which needed the full resolved PHPDoc, but the PHPDoc was still being built - Fix: store a partial NameScope (with type aliases but before template resolution) and return it on cycle detection instead of throwing NameScopeAlreadyBeingCreatedException - New regression test in tests/PHPStan/Analyser/nsrt/bug-11314.php --- src/Type/FileTypeMapper.php | 9 ++++ tests/PHPStan/Analyser/nsrt/bug-11314.php | 51 +++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11314.php diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index ddd2032d62f..ddc6dc87af6 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -66,6 +66,9 @@ final class FileTypeMapper /** @var array */ private array $inProcess = []; + /** @var array */ + private array $inProcessNameScopes = []; + /** @var array */ private array $resolvedPhpDocBlockCache = []; @@ -200,6 +203,9 @@ public function getNameScope( { $nameScopeKey = $this->getNameScopeKey($fileName, $className, $traitName, $functionName); if (isset($this->inProcess[$nameScopeKey])) { + if (isset($this->inProcessNameScopes[$nameScopeKey])) { + return $this->inProcessNameScopes[$nameScopeKey]; + } throw new NameScopeAlreadyBeingCreatedException(); } @@ -288,6 +294,8 @@ public function getNameScope( continue; } + $this->inProcessNameScopes[$nameScopeKey] = $nameScope; + $templateTags = $this->phpDocNodeResolver->resolveTemplateTags($parent->getTemplatePhpDocNodes(), $nameScope); $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $templateTags)); $nameScope = $nameScope->withTemplateTypeMap($templateTypeMap, $templateTags); @@ -319,6 +327,7 @@ public function getNameScope( ); } finally { unset($this->inProcess[$nameScopeKey]); + unset($this->inProcessNameScopes[$nameScopeKey]); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11314.php b/tests/PHPStan/Analyser/nsrt/bug-11314.php new file mode 100644 index 00000000000..e541bb8691c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11314.php @@ -0,0 +1,51 @@ +breed); + + $cat2 = new Cat2(); + assertType("'British Shorthair'|'Maine Coon'|'Siamese'", $cat2->breed); + + $cat3 = new Cat3(); + assertType("'British Shorthair'|'Maine Coon'|'Siamese'", $cat3->breed); +}; From 5d34b4be102babf38bb62bcc3d9a1bf3a4c6d33b Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 25 Apr 2026 21:40:18 +0000 Subject: [PATCH 2/4] Add regression tests for phpstan/phpstan#7152 and phpstan/phpstan#13332 - bug-7152: @phpstan-type used as template bound with class inheritance - bug-13332: @phpstan-type used as template bound with generic return types Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-13332.php | 65 +++++++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-7152.php | 41 ++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13332.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-7152.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-13332.php b/tests/PHPStan/Analyser/nsrt/bug-13332.php new file mode 100644 index 00000000000..83224eb3ace --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13332.php @@ -0,0 +1,65 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug13332; + +use function PHPStan\Testing\assertType; + +enum TestEnum { + case A; + case B; +} + +/** + * @phpstan-type KeyType string|int|\UnitEnum|object + * + * @template K of KeyType + */ +class TestError +{ + /** @param K $key */ + public function __construct(private readonly mixed $key) + { + } + + /** @return self */ + public static function makeEnum(): self + { + return new self(TestEnum::A); + } + + /** @return self */ + public static function makeString(): self + { + return new self('foo'); + } +} + +/** + * @template K of string|int|\UnitEnum|object + */ +class TestOk +{ + /** @param K $key */ + public function __construct(private readonly mixed $key) + { + } + + /** @return self */ + public static function makeEnum(): self + { + return new self(TestEnum::A); + } +} + +function () { + $error = TestError::makeEnum(); + assertType('Bug13332\TestError', $error); + + $errorStr = TestError::makeString(); + assertType('Bug13332\TestError', $errorStr); + + $ok = TestOk::makeEnum(); + assertType('Bug13332\TestOk', $ok); +}; diff --git a/tests/PHPStan/Analyser/nsrt/bug-7152.php b/tests/PHPStan/Analyser/nsrt/bug-7152.php new file mode 100644 index 00000000000..f14035fc411 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-7152.php @@ -0,0 +1,41 @@ + + */ +class Root +{ + /** @var T */ + public array $value; +} + +/** + * @phpstan-type Foo array + * @template T of Foo + * @extends Root + */ +class Middle extends Root +{ +} + +/** + * @template T of array + * @extends Root + */ +class Middle2 extends Root +{ +} + +function () { + /** @var Middle> $m */ + $m = new Middle(); + assertType('array', $m->value); + + /** @var Middle2> $m2 */ + $m2 = new Middle2(); + assertType('array', $m2->value); +}; From 98071b054da383e04092e1d39ee54e44b9518a6f Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sun, 26 Apr 2026 06:46:24 +0000 Subject: [PATCH 3/4] Add regression tests for phpstan/phpstan#7152 and phpstan/phpstan#13332 Co-Authored-By: Claude Opus 4.6 --- .../Rules/Generics/ClassAncestorsRuleTest.php | 5 ++ .../Generics/ClassTemplateTypeRuleTest.php | 16 ++++++ .../PHPStan/Rules/Generics/data/bug-11314.php | 38 ++++++++++++++ .../PHPStan/Rules/Generics/data/bug-13332.php | 52 +++++++++++++++++++ .../PHPStan/Rules/Generics/data/bug-7152.php | 29 +++++++++++ 5 files changed, 140 insertions(+) create mode 100644 tests/PHPStan/Rules/Generics/data/bug-11314.php create mode 100644 tests/PHPStan/Rules/Generics/data/bug-13332.php create mode 100644 tests/PHPStan/Rules/Generics/data/bug-7152.php diff --git a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php index d98f5438694..90b368011b3 100644 --- a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php @@ -290,4 +290,9 @@ public function testBug7021(): void $this->analyse([__DIR__ . '/data/bug-7021.php'], []); } + public function testBug7152(): void + { + $this->analyse([__DIR__ . '/data/bug-7152.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php index 7c231876c93..73b14e7521e 100644 --- a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php @@ -151,4 +151,20 @@ public function testBug10049(): void ]); } + public function testBug11314(): void + { + $this->analyse([__DIR__ . '/data/bug-11314.php'], []); + } + + #[RequiresPhp('>= 8.1.0')] + public function testBug13332(): void + { + $this->analyse([__DIR__ . '/data/bug-13332.php'], []); + } + + public function testBug7152(): void + { + $this->analyse([__DIR__ . '/data/bug-7152.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Generics/data/bug-11314.php b/tests/PHPStan/Rules/Generics/data/bug-11314.php new file mode 100644 index 00000000000..2e3f4cfd01e --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-11314.php @@ -0,0 +1,38 @@ += 8.1 + +declare(strict_types = 1); + +namespace Bug13332Generics; + +enum TestEnum { + case A; + case B; +} + +/** + * @phpstan-type KeyType string|int|\UnitEnum|object + * + * @template K of KeyType + */ +class TestError +{ + /** @param K $key */ + public function __construct(private readonly mixed $key) + { + } + + /** @return self */ + public static function makeEnum(): self + { + return new self(TestEnum::A); + } + + /** @return self */ + public static function makeString(): self + { + return new self('foo'); + } +} + +/** + * @template K of string|int|\UnitEnum|object + */ +class TestOk +{ + /** @param K $key */ + public function __construct(private readonly mixed $key) + { + } + + /** @return self */ + public static function makeEnum(): self + { + return new self(TestEnum::A); + } +} diff --git a/tests/PHPStan/Rules/Generics/data/bug-7152.php b/tests/PHPStan/Rules/Generics/data/bug-7152.php new file mode 100644 index 00000000000..b937cd71c9e --- /dev/null +++ b/tests/PHPStan/Rules/Generics/data/bug-7152.php @@ -0,0 +1,29 @@ + + */ +class Root +{ + /** @var T */ + public array $value; +} + +/** + * @phpstan-type Foo array + * @template T of Foo + * @extends Root + */ +class Middle extends Root +{ +} + +/** + * @template T of array + * @extends Root + */ +class Middle2 extends Root +{ +} From 00599a8131ff99b7fc094aac1cd069355166f348 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sun, 26 Apr 2026 06:56:07 +0000 Subject: [PATCH 4/4] Remove duplicate test fixtures, reference nsrt files from rule tests Co-Authored-By: Claude Opus 4.6 --- .../Rules/Generics/ClassAncestorsRuleTest.php | 2 +- .../Generics/ClassTemplateTypeRuleTest.php | 6 +-- .../PHPStan/Rules/Generics/data/bug-11314.php | 38 -------------- .../PHPStan/Rules/Generics/data/bug-13332.php | 52 ------------------- .../PHPStan/Rules/Generics/data/bug-7152.php | 29 ----------- 5 files changed, 4 insertions(+), 123 deletions(-) delete mode 100644 tests/PHPStan/Rules/Generics/data/bug-11314.php delete mode 100644 tests/PHPStan/Rules/Generics/data/bug-13332.php delete mode 100644 tests/PHPStan/Rules/Generics/data/bug-7152.php diff --git a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php index 90b368011b3..85120fe0d2f 100644 --- a/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassAncestorsRuleTest.php @@ -292,7 +292,7 @@ public function testBug7021(): void public function testBug7152(): void { - $this->analyse([__DIR__ . '/data/bug-7152.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-7152.php'], []); } } diff --git a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php index 73b14e7521e..2632e3334df 100644 --- a/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generics/ClassTemplateTypeRuleTest.php @@ -153,18 +153,18 @@ public function testBug10049(): void public function testBug11314(): void { - $this->analyse([__DIR__ . '/data/bug-11314.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-11314.php'], []); } #[RequiresPhp('>= 8.1.0')] public function testBug13332(): void { - $this->analyse([__DIR__ . '/data/bug-13332.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13332.php'], []); } public function testBug7152(): void { - $this->analyse([__DIR__ . '/data/bug-7152.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-7152.php'], []); } } diff --git a/tests/PHPStan/Rules/Generics/data/bug-11314.php b/tests/PHPStan/Rules/Generics/data/bug-11314.php deleted file mode 100644 index 2e3f4cfd01e..00000000000 --- a/tests/PHPStan/Rules/Generics/data/bug-11314.php +++ /dev/null @@ -1,38 +0,0 @@ -= 8.1 - -declare(strict_types = 1); - -namespace Bug13332Generics; - -enum TestEnum { - case A; - case B; -} - -/** - * @phpstan-type KeyType string|int|\UnitEnum|object - * - * @template K of KeyType - */ -class TestError -{ - /** @param K $key */ - public function __construct(private readonly mixed $key) - { - } - - /** @return self */ - public static function makeEnum(): self - { - return new self(TestEnum::A); - } - - /** @return self */ - public static function makeString(): self - { - return new self('foo'); - } -} - -/** - * @template K of string|int|\UnitEnum|object - */ -class TestOk -{ - /** @param K $key */ - public function __construct(private readonly mixed $key) - { - } - - /** @return self */ - public static function makeEnum(): self - { - return new self(TestEnum::A); - } -} diff --git a/tests/PHPStan/Rules/Generics/data/bug-7152.php b/tests/PHPStan/Rules/Generics/data/bug-7152.php deleted file mode 100644 index b937cd71c9e..00000000000 --- a/tests/PHPStan/Rules/Generics/data/bug-7152.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ -class Root -{ - /** @var T */ - public array $value; -} - -/** - * @phpstan-type Foo array - * @template T of Foo - * @extends Root - */ -class Middle extends Root -{ -} - -/** - * @template T of array - * @extends Root - */ -class Middle2 extends Root -{ -}