Skip to content

Fix phpstan/phpstan#9519: Moving (instanceof A || instanceof B) from if statement causes error#5372

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

Fix phpstan/phpstan#9519: Moving (instanceof A || instanceof B) from if statement causes error#5372
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-pbsoop9

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When instanceof results were stored in variables and then used in if conditions (especially OR conditions), PHPStan failed to narrow the target variable's type. For example:

$isA = $obj instanceof ClassA;
$isB = $obj instanceof ClassB;
if ($isA || $isB) {
    $obj->sayHello(); // Error: Cannot call method on mixed
}

This worked correctly when instanceof was used inline: if ($obj instanceof ClassA || $obj instanceof ClassB).

Changes

  • src/Analyser/MutatingScope.php: Changed addConditionalExpressions() to merge new conditional expression holders with existing ones instead of replacing them. This was the root cause of the second instanceof assignment overwriting the first's type narrowing information. Also added a public getConditionalExpressions() getter.
  • src/Analyser/TypeSpecifier.php: Added resolveConditionalExpressions() method that resolves a scope's conditional expressions for a given set of specified types. Used in the BooleanOr handler to augment each branch's specified types with resolved conditional expressions before intersecting, enabling proper type narrowing through variable-stored conditions.
  • tests/PHPStan/Analyser/nsrt/bug-9519.php: New regression test covering the reported scenarios.
  • tests/PHPStan/Analyser/nsrt/multi-assign.php: Updated expectations to reflect now-correct transitive type narrowing through chained assignments ($foo = $bar = $baz = $b).
  • tests/PHPStan/Analyser/nsrt/bug-7716.php: Updated expectations to reflect now-correct narrowing of optional array keys when their isset() results are stored in variables.

Root cause

Two issues combined to cause this bug:

  1. addConditionalExpressions used replacement instead of merge: When processing $isB = $obj instanceof ClassB after $isA = $obj instanceof ClassA, the conditional expression holders for $obj from the first assignment were completely replaced by the second's. This meant only the last instanceof assignment's type narrowing was preserved.

  2. BooleanOr handler didn't resolve conditional expressions: Even with the merge fix, $isA || $isB in truthy context produced empty sure types (because intersectWith only keeps expressions present in both branches, and $isA/$isB are different expression keys). The scope's conditional expressions mapping $isA$obj:ClassA and $isB$obj:ClassB were never resolved during the BooleanOr processing. The new resolveConditionalExpressions() method resolves these before the intersection, so $obj:ClassA from the left branch and $obj:ClassB from the right branch properly intersect to produce $obj:ClassA|ClassB.

Test

The regression test (tests/PHPStan/Analyser/nsrt/bug-9519.php) covers:

  • instanceof results stored in variables used in OR conditions (test1)
  • Inline instanceof in OR conditions as control (test2)
  • Individual variable-stored instanceof checks (test3)
  • instanceof with template types where second assignment doesn't clobber first (test4, test5)

Fixes phpstan/phpstan#9519

- Changed MutatingScope::addConditionalExpressions to merge instead of
  replace, so multiple instanceof assignments on the same target don't
  overwrite each other's conditional expression holders
- Added resolveConditionalExpressions in TypeSpecifier to resolve
  scope conditional expressions during BooleanOr processing, enabling
  type narrowing to flow through variable-stored instanceof results
- Added getConditionalExpressions() public getter to MutatingScope
- Updated test expectations in multi-assign.php and bug-7716.php to
  reflect the now-correct transitive type narrowing
- New regression test in tests/PHPStan/Analyser/nsrt/bug-9519.php

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@staabm staabm deleted the create-pull-request/patch-pbsoop9 branch April 1, 2026 05:45
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