From 5b343a81ec98592102fe9edb5903efd8c5ab956a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 23:18:53 +0000 Subject: [PATCH 1/8] Use declared property type instead of scope-narrowed type when inferring generic `new` expression types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - In `NewHandler::exactInstantiation()`, when `new GenericClass()` is assigned to a property and the constructor cannot resolve template types, the code infers template types from the property's declared type - Previously used `$scope->getType($assignedToProperty)` which returns the scope-narrowed type — inside `if ($x === null)`, this returns `null`, and after `removeNull` becomes `never`, causing incorrect type inference - Now uses `PropertyReflectionFinder::findPropertyReflectionFromNode()` to get the property's declared writable type, which is not affected by scope narrowing - Affects both static and instance properties with nullable generic types like `WeakMap|null` and `SplObjectStorage|null` --- src/Analyser/ExprHandler/NewHandler.php | 13 ++-- tests/PHPStan/Analyser/nsrt/bug-11844.php | 36 +++++++++++ .../TooWidePropertyTypeRuleTest.php | 6 ++ .../Rules/TooWideTypehints/data/bug-11844.php | 62 +++++++++++++++++++ 4 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-11844.php create mode 100644 tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index 67cc17797d5..01ad19f32ee 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -35,6 +35,7 @@ use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Rules\Properties\PropertyReflectionFinder; use PHPStan\ShouldNotHappenException; use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\GenericObjectType; @@ -66,6 +67,7 @@ public function __construct( private ReflectionProvider $reflectionProvider, private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider, private DynamicReturnTypeExtensionRegistryProvider $dynamicReturnTypeExtensionRegistryProvider, + private PropertyReflectionFinder $propertyReflectionFinder, #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] private bool $implicitThrows, ) @@ -416,10 +418,13 @@ private function exactInstantiation(MutatingScope $scope, New_ $node, Name $clas $classTemplateTypes = $traverser->getClassTemplateTypes(); if (count($classTemplateTypes) === count($originalClassTemplateTypes)) { - $propertyType = TypeCombinator::removeNull($scope->getType($assignedToProperty)); - $nonFinalObjectType = $isStatic ? new StaticType($nonFinalClassReflection) : new ObjectType($resolvedClassName, classReflection: $nonFinalClassReflection); - if ($nonFinalObjectType->isSuperTypeOf($propertyType)->yes()) { - return $propertyType; + $foundProperty = $this->propertyReflectionFinder->findPropertyReflectionFromNode($assignedToProperty, $scope); + if ($foundProperty !== null) { + $propertyType = TypeCombinator::removeNull($foundProperty->getWritableType()); + $nonFinalObjectType = $isStatic ? new StaticType($nonFinalClassReflection) : new ObjectType($resolvedClassName, classReflection: $nonFinalClassReflection); + if ($nonFinalObjectType->isSuperTypeOf($propertyType)->yes()) { + return $propertyType; + } } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11844.php b/tests/PHPStan/Analyser/nsrt/bug-11844.php new file mode 100644 index 00000000000..3d363336082 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-11844.php @@ -0,0 +1,36 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug11844Nsrt; + +use function PHPStan\Testing\assertType; + +class Foo +{ + /** + * @var \WeakMap|null + */ + private static ?\WeakMap $staticMap = null; + + /** + * @var \WeakMap|null + */ + private ?\WeakMap $instanceMap = null; + + public static function initStatic(): void + { + if (self::$staticMap === null) { + self::$staticMap = new \WeakMap(); + assertType('WeakMap', self::$staticMap); + } + } + + public function initInstance(): void + { + if ($this->instanceMap === null) { + $this->instanceMap = new \WeakMap(); + assertType('WeakMap', $this->instanceMap); + } + } +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php index 4ddf860b4de..cab35b9a6ae 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php @@ -138,4 +138,10 @@ public function testBug13624(): void $this->analyse([__DIR__ . '/data/bug-13624.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testBug11844(): void + { + $this->analyse([__DIR__ . '/data/bug-11844.php'], []); + } + } diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php new file mode 100644 index 00000000000..569e99c23aa --- /dev/null +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php @@ -0,0 +1,62 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug11844; + +class StaticPropertyCase +{ + /** + * @var \WeakMap|null + */ + private static ?\WeakMap $map = null; + + public static function init(): void + { + if (self::$map === null) { + self::$map = new \WeakMap(); + } + } +} + +class InstancePropertyCase +{ + /** + * @var \WeakMap|null + */ + private ?\WeakMap $map = null; + + public function init(): void + { + if ($this->map === null) { + $this->map = new \WeakMap(); + } + } +} + +/** @template T */ +class GenericContainer +{ + /** @var T */ + private $value; + + /** @param T $value */ + public function __construct($value) { + $this->value = $value; + } +} + +class OtherGenericCase +{ + /** + * @var \SplObjectStorage|null + */ + private static ?\SplObjectStorage $storage = null; + + public static function init(): void + { + if (self::$storage === null) { + self::$storage = new \SplObjectStorage(); + } + } +} From 5417fdd31515e58c825b650bb1affbd654327554 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 15 Apr 2026 09:10:20 +0000 Subject: [PATCH 2/8] Use TypeCombinator::intersect() to extract matching type from property union When the property type is a union like `WeakMap|null|false`, `TypeCombinator::removeNull()` only strips `null`, leaving `WeakMap|false`. The subsequent `isSuperTypeOf()` check then fails because `false` is not a subtype of `WeakMap`. Using `TypeCombinator::intersect()` with the class type properly extracts only the matching part of the union (e.g. `WeakMap`), handling any combination of non-object types in the property's declared type. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ExprHandler/NewHandler.php | 4 +- tests/PHPStan/Analyser/nsrt/bug-11844.php | 33 ++++++++++++++ .../Rules/TooWideTypehints/data/bug-11844.php | 44 +++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index 01ad19f32ee..76fc219807b 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -420,9 +420,9 @@ private function exactInstantiation(MutatingScope $scope, New_ $node, Name $clas if (count($classTemplateTypes) === count($originalClassTemplateTypes)) { $foundProperty = $this->propertyReflectionFinder->findPropertyReflectionFromNode($assignedToProperty, $scope); if ($foundProperty !== null) { - $propertyType = TypeCombinator::removeNull($foundProperty->getWritableType()); $nonFinalObjectType = $isStatic ? new StaticType($nonFinalClassReflection) : new ObjectType($resolvedClassName, classReflection: $nonFinalClassReflection); - if ($nonFinalObjectType->isSuperTypeOf($propertyType)->yes()) { + $propertyType = TypeCombinator::intersect($foundProperty->getWritableType(), $nonFinalObjectType); + if (!($propertyType instanceof NeverType) && $nonFinalObjectType->isSuperTypeOf($propertyType)->yes()) { return $propertyType; } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11844.php b/tests/PHPStan/Analyser/nsrt/bug-11844.php index 3d363336082..1d853ca9c6b 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11844.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11844.php @@ -34,3 +34,36 @@ public function initInstance(): void } } } + +class Bar +{ + /** + * @var \WeakMap|null|false + */ + private \WeakMap|null|false $instanceMap = false; + + /** + * @var \WeakMap|null|false + */ + private static \WeakMap|null|false $staticMap = false; + + public function initInstance(): void + { + if ($this->instanceMap !== false) { + if ($this->instanceMap === null) { + $this->instanceMap = new \WeakMap(); + assertType('WeakMap', $this->instanceMap); + } + } + } + + public static function initStatic(): void + { + if (self::$staticMap !== false) { + if (self::$staticMap === null) { + self::$staticMap = new \WeakMap(); + assertType('WeakMap', self::$staticMap); + } + } + } +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php index 569e99c23aa..db6d0afd3f3 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php @@ -46,6 +46,50 @@ public function __construct($value) { } } +class NullOrFalsePropertyCase +{ + /** + * @var \WeakMap|null|false + */ + private \WeakMap|null|false $map = false; + + public function init(): void + { + if ($this->map !== false) { + if ($this->map === null) { + $this->map = new \WeakMap(); + } + } + } + + public function reset(): void + { + $this->map = null; + } +} + +class StaticNullOrFalsePropertyCase +{ + /** + * @var \WeakMap|null|false + */ + private static \WeakMap|null|false $map = false; + + public static function init(): void + { + if (self::$map !== false) { + if (self::$map === null) { + self::$map = new \WeakMap(); + } + } + } + + public static function reset(): void + { + self::$map = null; + } +} + class OtherGenericCase { /** From fcde102a955a35ebc014766a0df5c0c261cb7048 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 15 Apr 2026 09:26:40 +0000 Subject: [PATCH 3/8] Add test cases for template property types to verify TypeCombinator::intersect() preserves generics Adds test coverage for properties typed with class-level template parameters (e.g. `@var \WeakMap|null`) to ensure the intersect call in NewHandler does not lose generic type information when template types are involved. Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-11844.php | 33 ++++++++++++++++ .../Rules/TooWideTypehints/data/bug-11844.php | 38 +++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-11844.php b/tests/PHPStan/Analyser/nsrt/bug-11844.php index 1d853ca9c6b..01dbc6b0961 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11844.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11844.php @@ -67,3 +67,36 @@ public static function initStatic(): void } } } + +/** + * @template T of object + * @template U + */ +class Baz +{ + /** + * @var \WeakMap|null + */ + private ?\WeakMap $instanceMap = null; + + /** + * @var \WeakMap|null + */ + private static ?\WeakMap $staticMap = null; + + public function initInstance(): void + { + if ($this->instanceMap === null) { + $this->instanceMap = new \WeakMap(); + assertType('WeakMap', $this->instanceMap); + } + } + + public static function initStatic(): void + { + if (self::$staticMap === null) { + self::$staticMap = new \WeakMap(); + assertType('WeakMap', self::$staticMap); + } + } +} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php index db6d0afd3f3..01133e829c4 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php @@ -104,3 +104,41 @@ public static function init(): void } } } + +/** + * @template T of object + * @template U + */ +class TemplatePropertyCase +{ + /** + * @var \WeakMap|null + */ + private ?\WeakMap $map = null; + + public function init(): void + { + if ($this->map === null) { + $this->map = new \WeakMap(); + } + } +} + +/** + * @template T of object + * @template U + */ +class StaticTemplatePropertyCase +{ + /** + * @var \WeakMap|null + */ + private static ?\WeakMap $map = null; + + public static function init(): void + { + if (self::$map === null) { + self::$map = new \WeakMap(); + } + } +} From cba17ba6af68b437261e2a9b362fac2122a895fc Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 15 Apr 2026 09:37:58 +0000 Subject: [PATCH 4/8] Merge NSRT and TooWidePropertyTypeRule test files for bug-11844 into a single shared file Co-Authored-By: Claude Opus 4.6 --- .../Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Analyser/nsrt/bug-11844.php | 102 ------------------ .../Rules/TooWideTypehints/data/bug-11844.php | 8 ++ 3 files changed, 9 insertions(+), 102 deletions(-) delete mode 100644 tests/PHPStan/Analyser/nsrt/bug-11844.php diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 13ca1bdc2e7..b89374b7c81 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -263,6 +263,7 @@ private static function findTestFiles(): iterable yield __DIR__ . '/../Rules/Variables/data/bug-14124.php'; yield __DIR__ . '/../Rules/Variables/data/bug-14124b.php'; yield __DIR__ . '/../Rules/Arrays/data/bug-14308.php'; + yield __DIR__ . '/../Rules/TooWideTypehints/data/bug-11844.php'; } /** diff --git a/tests/PHPStan/Analyser/nsrt/bug-11844.php b/tests/PHPStan/Analyser/nsrt/bug-11844.php deleted file mode 100644 index 01dbc6b0961..00000000000 --- a/tests/PHPStan/Analyser/nsrt/bug-11844.php +++ /dev/null @@ -1,102 +0,0 @@ -= 8.0 - -declare(strict_types = 1); - -namespace Bug11844Nsrt; - -use function PHPStan\Testing\assertType; - -class Foo -{ - /** - * @var \WeakMap|null - */ - private static ?\WeakMap $staticMap = null; - - /** - * @var \WeakMap|null - */ - private ?\WeakMap $instanceMap = null; - - public static function initStatic(): void - { - if (self::$staticMap === null) { - self::$staticMap = new \WeakMap(); - assertType('WeakMap', self::$staticMap); - } - } - - public function initInstance(): void - { - if ($this->instanceMap === null) { - $this->instanceMap = new \WeakMap(); - assertType('WeakMap', $this->instanceMap); - } - } -} - -class Bar -{ - /** - * @var \WeakMap|null|false - */ - private \WeakMap|null|false $instanceMap = false; - - /** - * @var \WeakMap|null|false - */ - private static \WeakMap|null|false $staticMap = false; - - public function initInstance(): void - { - if ($this->instanceMap !== false) { - if ($this->instanceMap === null) { - $this->instanceMap = new \WeakMap(); - assertType('WeakMap', $this->instanceMap); - } - } - } - - public static function initStatic(): void - { - if (self::$staticMap !== false) { - if (self::$staticMap === null) { - self::$staticMap = new \WeakMap(); - assertType('WeakMap', self::$staticMap); - } - } - } -} - -/** - * @template T of object - * @template U - */ -class Baz -{ - /** - * @var \WeakMap|null - */ - private ?\WeakMap $instanceMap = null; - - /** - * @var \WeakMap|null - */ - private static ?\WeakMap $staticMap = null; - - public function initInstance(): void - { - if ($this->instanceMap === null) { - $this->instanceMap = new \WeakMap(); - assertType('WeakMap', $this->instanceMap); - } - } - - public static function initStatic(): void - { - if (self::$staticMap === null) { - self::$staticMap = new \WeakMap(); - assertType('WeakMap', self::$staticMap); - } - } -} diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php index 01133e829c4..1268e56de9a 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php +++ b/tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php @@ -4,6 +4,8 @@ namespace Bug11844; +use function PHPStan\Testing\assertType; + class StaticPropertyCase { /** @@ -15,6 +17,7 @@ public static function init(): void { if (self::$map === null) { self::$map = new \WeakMap(); + assertType('WeakMap', self::$map); } } } @@ -30,6 +33,7 @@ public function init(): void { if ($this->map === null) { $this->map = new \WeakMap(); + assertType('WeakMap', $this->map); } } } @@ -58,6 +62,7 @@ public function init(): void if ($this->map !== false) { if ($this->map === null) { $this->map = new \WeakMap(); + assertType('WeakMap', $this->map); } } } @@ -80,6 +85,7 @@ public static function init(): void if (self::$map !== false) { if (self::$map === null) { self::$map = new \WeakMap(); + assertType('WeakMap', self::$map); } } } @@ -120,6 +126,7 @@ public function init(): void { if ($this->map === null) { $this->map = new \WeakMap(); + assertType('WeakMap', $this->map); } } } @@ -139,6 +146,7 @@ public static function init(): void { if (self::$map === null) { self::$map = new \WeakMap(); + assertType('WeakMap', self::$map); } } } From 64f3fe3fd6a877a5cc26db543791a588fc171c7d Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 15 Apr 2026 11:12:05 +0000 Subject: [PATCH 5/8] Remove redundant isSuperTypeOf check after TypeCombinator::intersect() The result of TypeCombinator::intersect($writableType, $nonFinalObjectType) is by definition a subtype of $nonFinalObjectType, so the isSuperTypeOf check is always true when the result is not NeverType. Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ExprHandler/NewHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index 76fc219807b..3331cb18eb4 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -422,7 +422,7 @@ private function exactInstantiation(MutatingScope $scope, New_ $node, Name $clas if ($foundProperty !== null) { $nonFinalObjectType = $isStatic ? new StaticType($nonFinalClassReflection) : new ObjectType($resolvedClassName, classReflection: $nonFinalClassReflection); $propertyType = TypeCombinator::intersect($foundProperty->getWritableType(), $nonFinalObjectType); - if (!($propertyType instanceof NeverType) && $nonFinalObjectType->isSuperTypeOf($propertyType)->yes()) { + if (!($propertyType instanceof NeverType)) { return $propertyType; } } From b642c042bd25095c19638f2770d6c45d97f9f07f Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 15 Apr 2026 11:18:25 +0000 Subject: [PATCH 6/8] Remove redundant parentheses around instanceof check Co-Authored-By: Claude Opus 4.6 --- src/Analyser/ExprHandler/NewHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index 3331cb18eb4..2e57336cb9e 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -422,7 +422,7 @@ private function exactInstantiation(MutatingScope $scope, New_ $node, Name $clas if ($foundProperty !== null) { $nonFinalObjectType = $isStatic ? new StaticType($nonFinalClassReflection) : new ObjectType($resolvedClassName, classReflection: $nonFinalClassReflection); $propertyType = TypeCombinator::intersect($foundProperty->getWritableType(), $nonFinalObjectType); - if (!($propertyType instanceof NeverType)) { + if (!$propertyType instanceof NeverType) { return $propertyType; } } From 8439cfe063a68524d5cdec4a27d944efd6d07d44 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 15 Apr 2026 19:22:07 +0000 Subject: [PATCH 7/8] Move bug-11844 test file to tests/PHPStan/Analyser/nsrt/ directory The file is auto-discovered in the nsrt/ directory by NodeScopeResolverTest, so the explicit yield was also removed to avoid duplicate key errors. Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/NodeScopeResolverTest.php | 1 - .../TooWideTypehints/data => Analyser/nsrt}/bug-11844.php | 0 .../Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php | 2 +- 3 files changed, 1 insertion(+), 2 deletions(-) rename tests/PHPStan/{Rules/TooWideTypehints/data => Analyser/nsrt}/bug-11844.php (100%) diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index b89374b7c81..13ca1bdc2e7 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -263,7 +263,6 @@ private static function findTestFiles(): iterable yield __DIR__ . '/../Rules/Variables/data/bug-14124.php'; yield __DIR__ . '/../Rules/Variables/data/bug-14124b.php'; yield __DIR__ . '/../Rules/Arrays/data/bug-14308.php'; - yield __DIR__ . '/../Rules/TooWideTypehints/data/bug-11844.php'; } /** diff --git a/tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php b/tests/PHPStan/Analyser/nsrt/bug-11844.php similarity index 100% rename from tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php rename to tests/PHPStan/Analyser/nsrt/bug-11844.php diff --git a/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php b/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php index cab35b9a6ae..dbfb5258b84 100644 --- a/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php +++ b/tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php @@ -141,7 +141,7 @@ public function testBug13624(): void #[RequiresPhp('>= 8.0')] public function testBug11844(): void { - $this->analyse([__DIR__ . '/data/bug-11844.php'], []); + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-11844.php'], []); } } From 9051e36104f387ab32f5e2ecc2d64aa4a49b39e9 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 15 Apr 2026 19:50:24 +0000 Subject: [PATCH 8/8] Add custom generic class test to demonstrate fix is not WeakMap-specific The fix applies to any generic class whose constructor does not reference its template types (e.g. WeakMap, SplObjectStorage, or custom classes with no-arg constructors). Added CustomGenericCache test cases to prove this. Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-11844.php | 59 +++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-11844.php b/tests/PHPStan/Analyser/nsrt/bug-11844.php index 1268e56de9a..0db69f91639 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11844.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11844.php @@ -107,6 +107,65 @@ public static function init(): void { if (self::$storage === null) { self::$storage = new \SplObjectStorage(); + assertType('SplObjectStorage', self::$storage); + } + } +} + +/** + * Custom generic class whose constructor does NOT reference template types. + * This proves the fix is general, not WeakMap-specific. + * + * @template TKey of string + * @template TValue + */ +class CustomGenericCache +{ + /** @var array */ + private array $data = []; + + public function __construct() + { + } + + /** + * @param TKey $key + * @param TValue $value + */ + public function set(string $key, mixed $value): void + { + $this->data[$key] = $value; + } +} + +class CustomGenericPropertyCase +{ + /** + * @var CustomGenericCache|null + */ + private ?CustomGenericCache $cache = null; + + public function init(): void + { + if ($this->cache === null) { + $this->cache = new CustomGenericCache(); + assertType('Bug11844\CustomGenericCache', $this->cache); + } + } +} + +class StaticCustomGenericPropertyCase +{ + /** + * @var CustomGenericCache|null + */ + private static ?CustomGenericCache $cache = null; + + public static function init(): void + { + if (self::$cache === null) { + self::$cache = new CustomGenericCache(); + assertType('Bug11844\CustomGenericCache', self::$cache); } } }