Repository navigation
Read remembered equivalent and complementary comparison forms when resolving ===, !==, ==, !=, <, <=, >, >= - #6948
Merged
VincentLanglet merged 1 commit intoNov 4, 2026
Conversation
…solving `===`, `!==`, `==`, `!=`, `<`, `<=`, `>`, `>=` - BinaryOpHandler type callback: before computing a comparison's type from its operands, look up the forms the scope may already track for it - the operand-swapped form and the complement (`$a === $b` <-> `$b === $a`, `$a !== $b`, `$b !== $a`; same for `==`/`!=`; `$a < $b` <-> `$b > $a`, and `$a >= $b` / `$b <= $a` as the complement only when both operands are integers, because NAN and uncomparable values are neither `<` nor `>=`). A falsey `match (true)` arm stores `$a !== $b` as `false`; a later `$a === $b` arm now resolves to `true`, so the match is exhaustive in either arm order. - BinaryOpHandler specify callback: narrowing a comparison to true/false also pins the equivalent/complementary forms the scope already tracks, so a form remembered as plain `bool` (e.g. after an earlier merged match) cannot keep answering stale. - Ported both to the native mirror in turbo-ext/src/BinaryOpHandler.cpp (regenerated generated/BinaryOpHandler.h for the new private methods). - Probed but left as-is: `instanceof` / `is_*()` / `!$bool` arm pairs already resolve (their negation reads the narrowed operand); float `<` / `>=` stay non-complementary on purpose.
VincentLanglet
approved these changes
Nov 4, 2026
Contributor
|
@staabm I recommend to remove the whitespace changes to review this PR. |
staabm
approved these changes
Nov 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A
match (true)whose arms are complementary comparisons, e.g.$bar !== $bazand then$bar === $baz, was reported asmatch.unhandled("does not handle remaining value: true"). On the current branch this happened in both arm orders. After a falsey arm, the scope remembers that arm's comparison expression asfalse. Nothing connected that remembered value to the next arm's comparison: it is a different AST node (the complement, or the same comparison with its operands swapped). That comparison was priced from itsintoperands only, so it came out asbool.The comparison type resolution now reads the equivalent and complementary forms the scope already tracks. The narrowing of a comparison also updates those forms.
Changes
src/Analyser/ExprHandler/BinaryOpHandler.phpgetEquivalentComparisonForms()lists the forms that always evaluate to the same value as the comparison, or always to the opposite value:===/!==and==/!=: both operand orders, plus the complement.<,<=,>,>=: the operand-swapped mirror ($a < $b≡$b > $a). The complement ($a >= $b) is included only when both operands are integers. NAN and uncomparable values are neither<nor>=, so the two are not complements in general.resolveRememberedComparisonType(): the type callback returnstrue/falsewhen one of these forms is tracked in the scope with a constant boolean value.specifyRememberedComparisonForms(): the specify callback wraps the existing narrowing. When the comparison is narrowed to true or false, it also pins the forms the scope already tracks, so a form remembered as plainbool(from an earlier merged match) does not keep answering with a stale value.turbo-ext/src/BinaryOpHandler.cpp+turbo-ext/src/generated/BinaryOpHandler.h: native port of the three methods and of the wrapped specify callback.Probed and already correct: arm pairs built from
instanceof,is_*()and!$bool. Their negations read the narrowed operand, not a separate remembered expression.Root cause
The scope keeps comparison verdicts keyed by the printed expression, so
$a !== $b,$a === $band$b === $aare unrelated keys. TheBinaryOpHandlertype callback priced each of them from the operand types alone and never consulted a remembered verdict stored under an equivalent or complementary key. The same pattern affected every equality and relational operator. All of them are now handled in the same place.Test
tests/PHPStan/Rules/Comparison/data/bug-10583.php(MatchExpressionRuleTest::testBug10583) covers:==/!=with swapped operands,</<=and>/<=onint, andmatch (false);</>=pair, which is still correctly reported as unhandled.tests/PHPStan/Analyser/nsrt/bug-10583.phpasserts the inferred types of every form after a falsey arm. It includes the stale-boolscenario (two matches in a row) and checks that reassigning an operand invalidates the remembered verdict.walk-trace.phpis identical over its default corpus.Fixes phpstan/phpstan#10583
Fixes phpstan/phpstan#5610
🤖 Generated with Claude Code