Skip to content

Fix #13029: False match.unhandled on array with multiple booleans#4907

Closed
phpstan-bot wants to merge 5 commits into2.1.xfrom
create-pull-request/patch-v77nhhu
Closed

Fix #13029: False match.unhandled on array with multiple booleans#4907
phpstan-bot wants to merge 5 commits into2.1.xfrom
create-pull-request/patch-v77nhhu

Conversation

@phpstan-bot
Copy link
Collaborator

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

Summary

When using match(true) with boolean conditions involving array access expressions like $arr['a'] && $arr['b'], PHPStan incorrectly reported "Match expression does not handle remaining value: true" even when all boolean combinations were exhaustively covered. The same pattern worked correctly with plain variables.

Changes

  • src/Analyser/TypeSpecifier.php: Relaxed the Variable-only restriction in processBooleanSureConditionalTypes() and processBooleanNotSureConditionalTypes() to allow any expression type (including ArrayDimFetch) when creating conditional expression holders. Changed self-reference detection from variable name comparison to expression string comparison.
  • src/Analyser/NodeScopeResolver.php: Extended isScopeConditionallyImpossible() to also find boolean-typed offsets in constant array shapes (e.g., array{a: bool, b: bool}) and construct ArrayDimFetch expressions for them. Renamed helper method to scopeHasNeverBooleanExpr() to reflect the broader scope.
  • tests/PHPStan/Rules/Comparison/data/bug-13029.php: New regression test with exhaustive match(true) using array access boolean conditions.
  • tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php: Added testBug13029().

Root cause

The match(true) exhaustiveness detection works by checking if no boolean combination is possible after all match arms are processed. This uses filterByTruthyValue/filterByFalseyValue which rely on ConditionalExpressionHolder to propagate type narrowings between linked expressions.

Two issues prevented this from working with array access expressions:

  1. TypeSpecifier: processBooleanSureConditionalTypes() and processBooleanNotSureConditionalTypes() only created conditional expression holders for Variable nodes, skipping ArrayDimFetch and other expression types. This meant that when processing $arr['a'] && $arr['b'] in a falsey context, no conditional dependencies between $arr['a'] and $arr['b'] were created.

  2. NodeScopeResolver: isScopeConditionallyImpossible() only looked at plain boolean variables via getDefinedVariables(), missing boolean values stored as array offsets.

Test

The regression test covers three scenarios:

  • Exhaustive match(true) with $arr['a'] && $arr['b'] style conditions
  • Exhaustive match(true) with $arr['a'] === true && $arr['b'] === true style conditions
  • match(true) with a default arm (should never report errors)

All expect no match.unhandled errors.

Fixes phpstan/phpstan#13029

Closes phpstan/phpstan#12517
Closes phpstan/phpstan#13446
Closes phpstan/phpstan#6486
Closes phpstan/phpstan#9155

ondrejmirtes and others added 5 commits February 12, 2026 16:51
- Extended TypeSpecifier::processBooleanSureConditionalTypes() and
  processBooleanNotSureConditionalTypes() to create conditional
  expression holders for any expression type, not just Variables
- Used expression string comparison instead of variable name comparison
  for self-reference detection in conditional holder creation
- Extended isScopeConditionallyImpossible() to extract boolean-typed
  offsets from constant array shapes and check them for contradictions
- New regression test in tests/PHPStan/Rules/Comparison/data/bug-13029.php
Closes phpstan/phpstan#6486

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Closes phpstan/phpstan#9155

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Closes phpstan/phpstan#13446

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Closes phpstan/phpstan#12517

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ondrejmirtes ondrejmirtes deleted the create-pull-request/patch-v77nhhu branch February 12, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment