Skip to content

Fix #5051: Execution branch confusion#5058

Merged
staabm merged 5 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-q6icc61
Feb 27, 2026
Merged

Fix #5051: Execution branch confusion#5058
staabm merged 5 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-q6icc61

Conversation

@phpstan-bot
Copy link
Collaborator

@phpstan-bot phpstan-bot commented Feb 27, 2026

Summary

When an if/else block assigned multiple variables alongside a boolean flag correlated with a null check, PHPStan failed to narrow the nullable variable through the boolean flag. For example:

if ($data === null) {
    $foo = 'bar';
    $update = false;
} else {
    $foo = 'baz';
    $update = true;
}
if ($update) {
    // $data should be narrowed to `object` here, but was incorrectly `object|null`
    get_class($data); // false positive error
}

Without the $foo assignments, the narrowing worked correctly. The issue was that extra variable assignments in both branches created multi-condition guards that couldn't be resolved.

Changes

  • Modified MutatingScope::createConditionalExpressions() in src/Analyser/MutatingScope.php to create pairwise conditional expression holders instead of one holder per variable with all other type guards as conditions
  • This applies to both the type narrowing section (lines ~4129-4146) and the undefined variable section (lines ~4148-4155)

Root cause

When merging two branches of an if/else, createConditionalExpressions() identified variables that differed between branches as "type guards" and created ConditionalExpressionHolder instances. Each holder required ALL other type guards to match before the guarded variable's type could be resolved.

With 2 type guards (e.g., $data and $update), each holder had exactly 1 condition — which could be matched directly when the user checked if ($update).

With 3+ type guards (e.g., $data, $foo, $update), each holder had 2+ conditions, creating a circular dependency: resolving $data required $foo AND $update to be known, but resolving $foo required $data AND $update. The iterative resolution loop in filterBySpecifiedTypes() couldn't break this cycle.

The fix creates individual pairwise holders instead. In a binary branch merge, knowing any single type guard's value uniquely identifies which branch was taken, so each guard is independently sufficient. This allows the resolution loop to resolve variables step by step.

Test

Added tests/PHPStan/Analyser/nsrt/bug-5051.php with four test methods covering:

  1. Extra string variable assignments in both branches (the original bug)
  2. Extra boolean variable assignments in both branches
  3. Different variable names in each branch (already worked)
  4. No extra assignments (already worked)

Fixes phpstan/phpstan#5051
Fixes phpstan/phpstan#9647

…if/else

- Changed createConditionalExpressions to create pairwise conditional holders instead of one holder with all guards
- In a binary branch merge, any single type guard uniquely identifies the branch, so individual guards are sufficient
- Previously, 3+ type guards created holders requiring all-minus-one conditions, causing circular dependency in resolution
- New regression test in tests/PHPStan/Analyser/nsrt/bug-5051.php

Closes phpstan/phpstan#5051
@staabm
Copy link
Contributor

staabm commented Feb 27, 2026

@phpstan-bot add regression test for phpstan/phpstan#9647

Copy link
Contributor

@staabm staabm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this looks good. I checked performance and I could not measure a meaningfull regression

@ondrejmirtes
Copy link
Member

The comment reactions don't work here. But you should be able to push to the fork.

@staabm staabm self-assigned this Feb 27, 2026
@staabm staabm merged commit 2681e50 into phpstan:2.1.x Feb 27, 2026
635 of 646 checks passed
@staabm staabm deleted the create-pull-request/patch-q6icc61 branch February 27, 2026 15:21
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.

4 participants