Skip to content

Do not skip constant-array guards when creating conditional expressions for subtype-absorbed targets#6012

Open
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-zr1y38v
Open

Do not skip constant-array guards when creating conditional expressions for subtype-absorbed targets#6012
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-zr1y38v

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

When a variable is derived from another variable across mutually exclusive branches (e.g. an if/elseif/else chain or a match), narrowing the derived value later should re-narrow the original variable back to the branch conditions. This regressed for the case where the derived value is a constant array: PHPStan inferred string|null instead of 'aa'|'bb'|'cc'.

Changes

  • src/Analyser/MutatingScope.php: removed the special-case skip in createConditionalExpressions() that dropped conditional expressions for subtype-absorbed targets whenever the guard's type is a constant array.
  • tests/PHPStan/Analyser/nsrt/bug-14926.php: regression test covering both the if/elseif/else form and the match form from the issue.

Root cause

Commit "Skip constant-array guards when creating conditional expressions for subtype-absorbed targets" added a performance optimization that skipped creating a conditional expression whenever a subtype-absorbed target was paired with a constant-array guard. Its premise was that a constant-array guard "represents a unique literal value that is not re-asserted as a condition later", so the conditional expression could never re-narrow anything.

That premise is wrong: the constant array is held by a derived variable ($class in the reproducer), and asserting a condition about that derived variable — if ($class === null) return; — re-selects the branch and must re-narrow the dependent variable ($x). Dropping the conditional expression removed exactly the link needed for this re-narrowing, so $x stayed at its widened string|null type.

The skip only ever fired when the guard's type was a constant array (isConstantArray()->yes()), which is why the bug disappeared when the derived value was not an array — reverting the skip therefore fixes the entire family at once. The #5876 behavior of keeping subtype-absorbed variables as conditional-expression targets is preserved; only the incorrect constant-array narrowing of that behavior is removed.

Test

tests/PHPStan/Analyser/nsrt/bug-14926.php asserts $x is 'aa'|'bb'|'cc' after the null check, in both the if/elseif/else and match forms. Both fail before the change (inferred string|null) and pass after. The full test suite and PHPStan self-analysis remain green.

Fixes phpstan/phpstan#14926

…ns for subtype-absorbed targets

- Revert the constant-array guard skip in MutatingScope::createConditionalExpressions() that dropped the conditional expression linking a subtype-absorbed target (e.g. a `?string` re-narrowed to a literal in a branch) to a constant-array guard.
- The skip assumed such a constant-array guard "is not re-asserted as a condition later", but it can be: narrowing a value derived from the guard (e.g. `if ($class === null) return;` where `$class` holds the derived constant array) re-selects the branch and re-narrows the dependent variable.
- Because the skip only ever fired on `isConstantArray()->yes()` guards, this restores dependent-type re-narrowing for the whole family; non-array derived values were never affected.
@staabm

staabm commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

@ondrejmirtes this PR reverts the perf optimization from #6002 to fix a bug

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.

Missing narrowing between dependent types (regression from 2.2.4)

2 participants