Fix phpstan/phpstan#8985: Expression result remembered on new()#5449
Fix phpstan/phpstan#8985: Expression result remembered on new()#5449phpstan-bot wants to merge 6 commits intophpstan:2.1.xfrom
Conversation
src/Analyser/MutatingScope.php
Outdated
| !$node instanceof Variable | ||
| && !$node instanceof Expr\Closure | ||
| && !$node instanceof Expr\ArrowFunction | ||
| && !$this->expressionHasNewInChain($node) |
There was a problem hiding this comment.
instead of skipping these expressions at type-resolving time, couldn't we instead not remember expressions which are based on a new-expression in the first place?
There was a problem hiding this comment.
Done. Here's a summary of the change addressing staabm's review:
What changed: Moved the expressionHasNewInChain() check from resolveType() (read-time) to specifyExpressionType() (write-time).
Before: Expressions with new in their chain were stored in expressionTypes but then ignored when looked up in resolveType().
After: Expressions with new in their chain are never stored in expressionTypes in the first place — specifyExpressionType() returns $this early, making the scope unchanged.
This is a cleaner approach as suggested by the reviewer: prevent the data from being stored rather than storing it and ignoring it later. All 11853 tests pass and PHPStan reports no errors.
| if ( | ||
| $expr instanceof MethodCall || | ||
| $expr instanceof Expr\NullsafeMethodCall || | ||
| $expr instanceof Expr\ArrayDimFetch || | ||
| $expr instanceof PropertyFetch || | ||
| $expr instanceof Expr\NullsafePropertyFetch | ||
| ) { | ||
| return $expr->var instanceof Expr\New_ || $this->expressionHasNewInChain($expr->var); | ||
| } | ||
| if ( | ||
| $expr instanceof Expr\StaticCall | ||
| || $expr instanceof Expr\StaticPropertyFetch | ||
| || $expr instanceof Expr\ClassConstFetch | ||
| ) { | ||
| return $expr->class instanceof Expr\New_ || $this->expressionHasNewInChain($expr->class); | ||
| } |
There was a problem hiding this comment.
add more tests to cover the relevant expression types
There was a problem hiding this comment.
Done. Here's a summary of the changes:
Bug fix (src/Analyser/MutatingScope.php):
expressionHasNewInChain()would crash with aTypeErrorwhen$expr->classwas aNamenode (e.g.,Foo::bar()) instead of anExprnode. Added an$expr->class instanceof Exprguard before the recursive call on line 1007.
Tests (tests/PHPStan/Analyser/nsrt/bug-8985.php):
Added test functions covering all expression types handled by expressionHasNewInChain():
testMethodCall-(new Foo())->method()(existing, reorganized)testNullsafeMethodCall-(new Foo())?->method()testPropertyFetch-(new Foo())->proptestNullsafePropertyFetch-(new Foo())?->proptestArrayDimFetch-(new Foo())->getArray()[0]->method()testStaticCall-(new Foo())::staticMethod()testChainedMethodCalls-(new Foo())->getEntity()->getValue()testChainedPropertyOnMethodCall-(new Foo())->getEntity()->value
All 11853 tests pass, PHPStan reports no errors.
src/Analyser/MutatingScope.php
Outdated
| if ( | ||
| $expr instanceof Expr\StaticCall | ||
| || $expr instanceof Expr\StaticPropertyFetch | ||
| || $expr instanceof Expr\ClassConstFetch |
There was a problem hiding this comment.
a const-fetch on a known class can still be remembered
There was a problem hiding this comment.
Pushed successfully. Here's a summary of what was done to address the review:
Changes made:
-
Removed
ClassConstFetchfromexpressionHasNewInChain()— Class constants are class-level, not instance-level.(new Foo())::MY_CONSTis deterministic based on the class, so narrowed types should still be remembered. Unlike method calls or property fetches which depend on instance state, a constant's value is fixed. -
Added test for
ClassConstFetch— AddedtestClassConstFetch()to verify that(new Repository())::MY_CONSTcorrectly remembers its narrowed type ('const_value').
All 11853 tests pass and PHPStan reports no errors.
- Added expressionHasNewInChain() check in MutatingScope::resolveType() to skip stored expression type lookup when the expression's receiver chain contains a New_ node - New regression test in tests/PHPStan/Analyser/nsrt/bug-8985.php - The root cause was that (new Foo())->method() produced the same expression key regardless of source location, so type narrowing from assert() on one new instance incorrectly applied to subsequent ones
…t resolve time Move the expressionHasNewInChain check from resolveType() to specifyExpressionType() so that expressions based on new are never remembered in the first place, rather than being stored and then ignored at lookup time. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…pression types The recursive call in expressionHasNewInChain() could receive a Name node (e.g. from Foo::bar()) instead of an Expr, causing a type error. Added an instanceof Expr guard before recursing on $expr->class. Also added test coverage for all expression types handled by expressionHasNewInChain(): nullsafe method calls, property fetches, nullsafe property fetches, array dim fetches, static calls, and chained expressions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ession types Class constants are class-level, not instance-level, so narrowed types from (new Foo())::MY_CONST should still be remembered. Also fix a type error where $expr->class could be a Name (not Expr) for static calls. Added tests covering: method calls, nullsafe method calls, property fetches, nullsafe property fetches, array dim fetches, static calls, static property fetches, class const fetches, and chained expressions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9b7fe1f to
e7850f1
Compare
|
add tests for https://phpstan.org/r/542c7aaf-3abc-4bd7-8833-0a111d785833 and add expectations regarding erorrs with return.type identifier |
Summary
When
assert((new Repository())->getAll() === [])narrowed the type of the method call result, the narrowed typearray{}was incorrectly remembered for subsequent(new Repository())->getAll()calls, even though eachnew Repository()creates a fresh object. This caused a false positive "Offset 0 does not exist on array{}".Changes
expressionHasNewInChain()helper method insrc/Analyser/MutatingScope.phpthat checks whether an expression's receiver chain contains aNew_noderesolveType()insrc/Analyser/MutatingScope.phpto skip the stored expression type lookup when the expression hasnewin its receiver chainnewexpression -- it does NOT skip the lookup fornew Foo()itself, preserving@varannotation support onnewexpressionsRoot cause
PHPStan uses expression string keys (like
(new \Foo())->getAll()) to store and retrieve narrowed types inMutatingScope::expressionTypes. Whenassert()narrows a type, it stores the narrowed type under this key. The problem is that two differentnew Foo()AST nodes at different source locations produce the same string key, so the narrowed type from oneassertincorrectly applies to a completely differentnewinstantiation.The fix skips the expression type lookup for expressions whose receiver chain contains
new, since eachnewcreates a fresh object and narrowed types from one instance should not apply to another.Test
Added
tests/PHPStan/Analyser/nsrt/bug-8985.php- an NSRT test that verifies(new Repository())->getAll()returnsarray<int, Entity>even after a priorassert((new Repository())->getAll() === []).Fixes phpstan/phpstan#8985