Use declared property type instead of scope-narrowed type when inferring generic new expression types#5467
Conversation
VincentLanglet
left a comment
There was a problem hiding this comment.
What about a property \WeakMap|null|false
with a code
if ($this->instanceMap !== false) {
if ($this->instanceMap === null) {
$this->instanceMap = new \WeakMap();
assertType('WeakMap<object, string>', $this->instanceMap);
}
}
VincentLanglet
left a comment
There was a problem hiding this comment.
Add test with a property
@var \WeakMap<T, U>|null
to be sure the TypeCombinator::intersect call is not loosing generic type with template types.
|
Done. I've addressed the review by adding test cases with template property types (
Both |
VincentLanglet
left a comment
There was a problem hiding this comment.
Merge the nsrt type and the TooWideTypehints/data file into a single one used for both nsrt and TooWidePropertyTypeRule test
|
Done. I merged the two test files into a single
The standalone |
|
I processed this review but have nothing to report. |
| @@ -0,0 +1,152 @@ | |||
| <?php // lint >= 8.0 | |||
There was a problem hiding this comment.
move this file into tests/PHPStan/Analyser/nsrt and adjust paths to point to the moved path
There was a problem hiding this comment.
Done. Moved tests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php to tests/PHPStan/Analyser/nsrt/bug-11844.php as requested by staabm:
- The file is now in the
nsrt/directory where it's auto-discovered byNodeScopeResolverTest, so the explicityieldwas removed to avoid a duplicate key error - Updated the path in
TooWidePropertyTypeRuleTest::testBug11844()to reference the new location - All 11857 tests pass and
make phpstanreports no errors
| $nonFinalObjectType = $isStatic ? new StaticType($nonFinalClassReflection) : new ObjectType($resolvedClassName, classReflection: $nonFinalClassReflection); | ||
| if ($nonFinalObjectType->isSuperTypeOf($propertyType)->yes()) { | ||
| return $propertyType; | ||
| $foundProperty = $this->propertyReflectionFinder->findPropertyReflectionFromNode($assignedToProperty, $scope); |
There was a problem hiding this comment.
why is this fix required for WeakMap but not for other generic objects?
There was a problem hiding this comment.
Pushed. To answer staabm's question:
The fix is not WeakMap-specific — it's needed for any generic class whose constructor doesn't reference its template types. The code path at line 420 is entered when count($classTemplateTypes) === count($originalClassTemplateTypes), which means the ConstructorClassTemplateTraverser found no template types consumed by constructor parameters. This is the case for:
WeakMap(empty constructor, but hasTKey/TValuetemplates)SplObjectStorage(same pattern)- Any custom generic class with a no-arg constructor
For generic classes whose constructors do reference template types (e.g. new GenericContainer($value) where $value: T), the template types are resolved from the constructor arguments instead, so this code path is never reached — and the bug doesn't apply.
I added test cases with a custom CustomGenericCache<TKey, TValue> class (with an explicit no-arg constructor) to demonstrate the fix is general. Both instance and static property variants are tested. All 11857 tests pass and make phpstan reports no errors.
…ing 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`
…y union When the property type is a union like `WeakMap<object,string>|null|false`, `TypeCombinator::removeNull()` only strips `null`, leaving `WeakMap<object,string>|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<object,string>`), handling any combination of non-object types in the property's declared type. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…intersect() preserves generics Adds test coverage for properties typed with class-level template parameters (e.g. `@var \WeakMap<T, U>|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 <noreply@anthropic.com>
…a single shared file Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
67382a8 to
9051e36
Compare
Summary
When a nullable generic property (e.g.
?WeakMap $mapwith@var WeakMap<object, string>|null) was assignednew WeakMap()inside a null-check guard (if ($x === null)), PHPStan incorrectly reportedproperty.unusedTypesaying theWeakMap<object, string>type was never assigned. This was a false positive introduced in the commit that added static property support toNewAssignedToPropertyVisitor.Changes
src/Analyser/ExprHandler/NewHandler.php:PropertyReflectionFinderto resolve property reflections from AST nodes$scope->getType($assignedToProperty)with$foundProperty->getWritableType()to use the declared property type instead of the scope-narrowed typetests/PHPStan/Rules/TooWideTypehints/data/bug-11844.php— rule test covering static properties, instance properties, andSplObjectStoragetests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php— test methodtestBug11844tests/PHPStan/Analyser/nsrt/bug-11844.php— type inference test verifying correct types after assignmentRoot cause
NewHandler::exactInstantiation()infers template types fornew GenericClass()from the declared type of the property being assigned to. It used$scope->getType($assignedToProperty)to get this type, but inside a scope where the property has been narrowed (e.g. afterif (self::$map === null)),getType()returns the narrowed type (null). AfterTypeCombinator::removeNull(), this becomesnever, causing theTooWideTypeCheckto conclude the generic type was never assigned.The fix uses
PropertyReflectionFinder::findPropertyReflectionFromNode()to get the property's declared writable type, which reflects the PHPDoc/native type annotation and is not affected by scope narrowing.Test
testBug11844inTooWidePropertyTypeRuleTestverifies no false positiveproperty.unusedTypeerrors for nullableWeakMapandSplObjectStorageproperties (both static and instance)bug-11844.phpverifiesnew WeakMap()is correctly inferred asWeakMap<object, string>when assigned to a property typedWeakMap<object, string>|nullFixes phpstan/phpstan#11844