Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Revert "Add comment explaining why Pass 1 does not need No-certainty …
…check"

This reverts commit 19ec231.
  • Loading branch information
staabm committed Apr 13, 2026
commit b23f08f361d9fb535ebb337663ec9d825c8c8818
8 changes: 0 additions & 8 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -3237,14 +3237,6 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
}

// Pass 2: Supertype match. Only runs when Pass 1 found no exact match for this expression.
// Skip No-certainty holders here because supertype matching is asymmetric:
// a broader condition type (the "undefined" branch) is more likely to match
// as a supertype of a narrowed specified type than a narrower condition type
// (the "defined" branch), causing conditionally-defined variables to be
// incorrectly resolved as never defined.
// Pass 1 does not need this check because exact matching cannot produce
// this asymmetry - if the specified type exactly equals the No branch's
// condition, the scope genuinely corresponds to the "undefined" state.
foreach ($conditionalExpressions as $conditionalExpression) {
if ($conditionalExpression->getTypeHolder()->getCertainty()->no()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

do we need a similar check for the "Pass 1" foreach loop a few lines above?
if so, add a test-case which fails without a similar fix for "Pass 1".

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Pushed. Here's a summary of the review response:

staabm's question: Does Pass 1 need a similar No-certainty check?

Answer: No. The key difference is the matching strategy:

  • Pass 1 uses exact matching (equals()). If the specified type exactly equals the No branch's condition type, the scope genuinely corresponds to the "variable is undefined" state. Exact matching can't produce the asymmetry that causes the bug — it's impossible for a type to simultaneously equal two different condition types.

  • Pass 2 uses supertype matching (isSuperTypeOf()), which is inherently asymmetric. A broader condition type (the "undefined" branch, with fewer constraints like array&hasOffsetValue('a', 'b')) is more likely to be a supertype of a narrowed type than a narrower condition type (the "defined" branch, like array&hasOffsetValue('a', 'b')&hasOffsetValue('c', ...)). This asymmetry causes only the No-certainty branch to match, incorrectly resolving the variable as never defined.

I added a comment explaining this reasoning directly in the code at the Pass 2 check, so future readers understand why the guard is only needed there. All tests (11,853) and make phpstan pass.

continue;
Expand Down
Loading