Skip to content

Do not use cached expression types for member access chains rooted in new/clone#5497

Closed
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-68g0603
Closed

Do not use cached expression types for member access chains rooted in new/clone#5497
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-68g0603

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When assert() (or any type-narrowing construct) was applied to an expression rooted in new — e.g. assert((new Repository())->getAll() === []) — the narrowed type was cached in expressionTypes under the expression's string key. A subsequent occurrence of the same expression (new Repository())->getAll() would look up the cached type and incorrectly return the narrowed type (array{}), even though new creates a completely different object each time.

Changes

  • Added isExpressionBasedOnNew() private method to src/Analyser/MutatingScope.php that walks the receiver chain of an expression (MethodCall, PropertyFetch, NullsafeMethodCall, NullsafePropertyFetch, ArrayDimFetch, StaticCall, StaticPropertyFetch, ClassConstFetch) to detect whether the root is an Expr\New_ or Expr\Clone_
  • Modified resolveType() in MutatingScope to skip the expressionTypes cache lookup when isExpressionBasedOnNew() returns true
  • The check intentionally does NOT match bare new Foo() or clone $obj at the top level — only chains built on top of them — so that @var annotations on new expressions (like /** @var Foo<string> */ return new Foo()) continue to work correctly

Analogous cases probed and fixed

All of these were confirmed broken without the fix and passing with it:

  • Method call on new: (new Foo())->method() — the original bug report
  • Property access on new: (new Foo())->property
  • Nullsafe method call on new: (new Foo())->method()?->other()
  • Chained method calls on new: (new Factory())->create()->build()
  • Array dim fetch after method on new: (new Foo())->getItems() (via variable assignment + assert)
  • Clone expression: (clone $obj)->method()

Cases probed and found already correct

  • Bare new Foo() with @var annotation — correctly uses cache (tested via existing testBug7218)
  • StaticCall/StaticPropertyFetch/ClassConstFetch with expression class — covered in the helper but not practically triggerable with new as the class expression

Root cause

MutatingScope::resolveType() unconditionally used the expressionTypes cache for non-Variable, non-Closure expressions. Since new creates a fresh object on every evaluation, two textually identical expressions like (new Repository())->getAll() refer to method calls on different objects. But the cache keyed them identically, causing type narrowing from one evaluation to leak into the next.

Test

  • tests/PHPStan/Analyser/nsrt/bug-8985.php — regression test with 7 test functions covering the original bug and all analogous cases listed above

Fixes phpstan/phpstan#8985

… `new`/`clone`

- Add `isExpressionBasedOnNew()` helper in `MutatingScope` that detects
  expressions whose root object is a `new`/`clone` (e.g. `(new Foo())->method()`)
- Skip the `expressionTypes` cache in `resolveType()` for such expressions,
  because each `new`/`clone` creates a fresh object and type narrowing from a
  previous evaluation (e.g. via `assert()`) must not carry over
- Only skip cache for chains on `new`/`clone` (method calls, property fetches,
  array dim fetches, etc.), not for bare `new Foo()` itself, so `@var`
  annotations on `new` expressions continue to work
- Covers analogous cases: property access, nullsafe method calls, chained
  method calls, array dim fetch, and clone expressions
@staabm staabm closed this Apr 19, 2026
@staabm staabm deleted the create-pull-request/patch-68g0603 branch April 19, 2026 11:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants