Skip to content

Commit 29d20a0

Browse files
github-actions[bot]phpstan-bot
authored andcommitted
Use declared property type instead of scope-narrowed type when inferring generic new expression types
- 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<object, string>|null` and `SplObjectStorage<object, string>|null`
1 parent 04a99c1 commit 29d20a0

4 files changed

Lines changed: 113 additions & 4 deletions

File tree

src/Analyser/ExprHandler/NewHandler.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
use PHPStan\Reflection\ParametersAcceptor;
3636
use PHPStan\Reflection\ParametersAcceptorSelector;
3737
use PHPStan\Reflection\ReflectionProvider;
38+
use PHPStan\Rules\Properties\PropertyReflectionFinder;
3839
use PHPStan\ShouldNotHappenException;
3940
use PHPStan\Type\ErrorType;
4041
use PHPStan\Type\Generic\GenericObjectType;
@@ -66,6 +67,7 @@ public function __construct(
6667
private ReflectionProvider $reflectionProvider,
6768
private DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
6869
private DynamicReturnTypeExtensionRegistryProvider $dynamicReturnTypeExtensionRegistryProvider,
70+
private PropertyReflectionFinder $propertyReflectionFinder,
6971
#[AutowiredParameter(ref: '%exceptions.implicitThrows%')]
7072
private bool $implicitThrows,
7173
)
@@ -416,10 +418,13 @@ private function exactInstantiation(MutatingScope $scope, New_ $node, Name $clas
416418
$classTemplateTypes = $traverser->getClassTemplateTypes();
417419

418420
if (count($classTemplateTypes) === count($originalClassTemplateTypes)) {
419-
$propertyType = TypeCombinator::removeNull($scope->getType($assignedToProperty));
420-
$nonFinalObjectType = $isStatic ? new StaticType($nonFinalClassReflection) : new ObjectType($resolvedClassName, classReflection: $nonFinalClassReflection);
421-
if ($nonFinalObjectType->isSuperTypeOf($propertyType)->yes()) {
422-
return $propertyType;
421+
$foundProperty = $this->propertyReflectionFinder->findPropertyReflectionFromNode($assignedToProperty, $scope);
422+
if ($foundProperty !== null) {
423+
$propertyType = TypeCombinator::removeNull($foundProperty->getWritableType());
424+
$nonFinalObjectType = $isStatic ? new StaticType($nonFinalClassReflection) : new ObjectType($resolvedClassName, classReflection: $nonFinalClassReflection);
425+
if ($nonFinalObjectType->isSuperTypeOf($propertyType)->yes()) {
426+
return $propertyType;
427+
}
423428
}
424429
}
425430
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug11844Nsrt;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
class Foo
10+
{
11+
/**
12+
* @var \WeakMap<object, string>|null
13+
*/
14+
private static ?\WeakMap $staticMap = null;
15+
16+
/**
17+
* @var \WeakMap<object, string>|null
18+
*/
19+
private ?\WeakMap $instanceMap = null;
20+
21+
public static function initStatic(): void
22+
{
23+
if (self::$staticMap === null) {
24+
self::$staticMap = new \WeakMap();
25+
assertType('WeakMap<object, string>', self::$staticMap);
26+
}
27+
}
28+
29+
public function initInstance(): void
30+
{
31+
if ($this->instanceMap === null) {
32+
$this->instanceMap = new \WeakMap();
33+
assertType('WeakMap<object, string>', $this->instanceMap);
34+
}
35+
}
36+
}

tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,10 @@ public function testBug13624(): void
138138
$this->analyse([__DIR__ . '/data/bug-13624.php'], []);
139139
}
140140

141+
#[RequiresPhp('>= 8.0')]
142+
public function testBug11844(): void
143+
{
144+
$this->analyse([__DIR__ . '/data/bug-11844.php'], []);
145+
}
146+
141147
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug11844;
6+
7+
class StaticPropertyCase
8+
{
9+
/**
10+
* @var \WeakMap<object, string>|null
11+
*/
12+
private static ?\WeakMap $map = null;
13+
14+
public static function init(): void
15+
{
16+
if (self::$map === null) {
17+
self::$map = new \WeakMap();
18+
}
19+
}
20+
}
21+
22+
class InstancePropertyCase
23+
{
24+
/**
25+
* @var \WeakMap<object, string>|null
26+
*/
27+
private ?\WeakMap $map = null;
28+
29+
public function init(): void
30+
{
31+
if ($this->map === null) {
32+
$this->map = new \WeakMap();
33+
}
34+
}
35+
}
36+
37+
/** @template T */
38+
class GenericContainer
39+
{
40+
/** @var T */
41+
private $value;
42+
43+
/** @param T $value */
44+
public function __construct($value) {
45+
$this->value = $value;
46+
}
47+
}
48+
49+
class OtherGenericCase
50+
{
51+
/**
52+
* @var \SplObjectStorage<object, string>|null
53+
*/
54+
private static ?\SplObjectStorage $storage = null;
55+
56+
public static function init(): void
57+
{
58+
if (self::$storage === null) {
59+
self::$storage = new \SplObjectStorage();
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)