Skip to content

Preserve conditional expressions in invalidateExpression when requireMoreCharacters is true#5560

Merged
ondrejmirtes merged 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-rm0d862
Apr 28, 2026
Merged

Preserve conditional expressions in invalidateExpression when requireMoreCharacters is true#5560
ondrejmirtes merged 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-rm0d862

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When an instanceof check result is stored in a variable (e.g. $is_interface = $obj instanceof SomeInterface) and used in multiple consecutive if blocks, the type narrowing was lost after the first check when the if-block body contained impure calls (method calls, var_dump, etc.). This happened because invalidateExpression() incorrectly removed the conditional expressions that encode the stored type guard.

Changes

  • Modified src/Analyser/MutatingScope.php line 2872: changed the requireMoreCharacters parameter from hardcoded false to $requireMoreCharacters when checking whether to invalidate conditional expression targets
  • Added regression test tests/PHPStan/Analyser/nsrt/bug-14545.php covering:
    • Stored instanceof with generic return type + method call in body
    • Stored instanceof with generic return type + var_dump() in body
    • Stored instanceof with a different concrete class
    • Stored instanceof with abstract object return type (already worked)
    • Three consecutive checks of the same stored boolean
    • Stored is_array() result with impure call in body

Root cause

In MutatingScope::invalidateExpression(), when a method call like $obj->test() triggers invalidation with requireMoreCharacters=true, the expression type for $obj is correctly preserved (because shouldInvalidateExpression returns false for exact matches when requireMoreCharacters=true). However, the conditional expression check for $obj used false instead of $requireMoreCharacters, causing it to remove the conditional expressions that link $is_interface to $obj's narrowed type.

After the conditional was removed from the truthy branch scope, intersectConditionalExpressions() during the if-block merge would find no matching conditional in the other scope, so the conditional was dropped entirely. For abstract object types, createConditionalExpressions() could reconstruct the conditional from type differences (because object~SomeInterfaceobject), masking the bug. For concrete types like ObjectClass, ObjectClass minus SomeInterface is still ObjectClass, so reconstruction was impossible.

Analogous cases probed

  • Guard expression check (line 2881): Also uses hardcoded false, but this is correct — guard expressions that reference sub-expressions of the invalidated variable (e.g. $obj->prop) should be invalidated because the property value may have changed
  • Stored is_array() results: Same mechanism, tested and works with the fix
  • Stored instanceof with different concrete classes: Tested, works
  • Three+ consecutive checks: Tested, works
  • Abstract object return type: Already worked before the fix via createConditionalExpressions reconstruction

Test

Regression test at tests/PHPStan/Analyser/nsrt/bug-14545.php with 6 test functions covering the reported bug and analogous cases. All tests pass with the fix and the failing ones fail without it.

Fixes phpstan/phpstan#14545

…ireMoreCharacters` is true

- In `MutatingScope::invalidateExpression()`, the target expression check for
  conditional expressions was hardcoded to `requireMoreCharacters=false`, causing
  conditional expressions to be removed even when the expression type itself was
  preserved (e.g. after a method call on the variable)
- Pass the caller's `$requireMoreCharacters` flag to the `shouldInvalidateExpression`
  check for conditional expression targets, consistent with the expression type check
- This fixes stored `instanceof` results losing their type narrowing after the first
  use when the if-block body contains impure calls (method calls, var_dump, etc.)
- The bug only manifested for concrete class types (e.g. `ObjectClass`) where
  `createConditionalExpressions` could not reconstruct the conditional from type
  differences, unlike abstract `object` types where it could
@ondrejmirtes ondrejmirtes merged commit dc2650e into phpstan:2.1.x Apr 28, 2026
656 of 657 checks passed
@ondrejmirtes ondrejmirtes deleted the create-pull-request/patch-rm0d862 branch April 28, 2026 17:51
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