NonExhaustiveEnumMatch
error native on by default PHP ≥ 8.1Group: Probable bugs
Detect match over a resolved enum when reachable enum cases are missing and there is no default arm.
Example
<?php
enum Choice {case Left;case Right;} function label(Choice $c) {return match($c) {Choice::Left=>'L'};}Reported:
- line 2: Handle every reachable enum case.
Detection
Report match over a resolved enum when reachable enum cases are missing and there is no default arm.
Resolve builtin calls and named arguments before checking the contract. Project-local callees are considered only when their bodies or immutable summaries establish the required facts. The default is enabled, with positive proof required.
For flow-dependent conditions, require a reachable source-to-use path with no intervening invalidation. Unknown calls, ambiguous aliases, recursion or analysis-budget exhaustion cannot establish definite facts.
An arm condition written as a constant of the enum that aliases one of its cases (
const Fallback = self::Left;, directly or through another such constant) handles that case.The required cases are the ones the subject may still hold at the match. Strict comparisons (
===,!==) of the subject with a case of its enum (or an aliasing constant) narrow them where they guard the match: an equality that holds leaves one case, one that fails removes that case. Conditions combine through!,&&/and,||/or(each side of a true disjunction keeps what it proves), comparisons withtrue/falseand a local holding a comparison's result. A comparison guards the match:- in the taken branch of an
if/elseif/elsechain, of a ternary or of a loop condition, in the right operand of&&/||, or in amatcharm (adefaultarm keeps the cases earlier arms did not take); - after a branch on it that always leaves (return, throw, exit, break, continue), or a loop on it that has ended.
Assigning a case (
$e = E::B;) or a constant aliasing one fixes the subject to that case; a value whose declared type is the enum may hold any case. Inside a method of the enum,$thisnarrows the same way.- in the taken branch of an
What a comparison proves holds until the subject may change: an assignment,
unsetor loop binding of it, a by-reference binding written through the other name, a call that may take it by reference (a resolved by-reference parameter, or a callee that cannot be resolved: unknown function, dynamic name, method of an unknown class), and, for a by-reference parameter or agloballocal, any call.extract(),parse_str()with one argument, variable-variable writes,include/requireandeval()before the match, or a local bound to an array element by reference, leave the subject unknown. A jump label between a comparison and the match proves nothing. Otherwise the comparisons are ignored and every case stays required.
Exceptions (no report)
- Exhaustive matches, flow-narrowed case sets, default arms and unknown subject types are excluded. A resolved by-reference call without an output contract invalidates the previous enum type; its parameter declaration constrains entry to the call, not the value returned through the reference.
- Unresolved targets, incomplete required facts, suppressed findings and PHP versions lacking the referenced API or language feature produce no report. User declarations shadowing builtin names are not builtin contracts.
- A match with an arm condition that is not a constant of the subject's enum, or is a constant of it that cannot be traced to a case (a non-case value, an unknown name, a dynamic name), has an unknown arm set and is not reported.
Fix
No automatic fix: missing result expressions cannot be invented.
PHP versions
Available from PHP 8.1. Builtin behavior and argument contracts follow the configured PHP version. Extension-specific calls require resolved extension API symbols; do not treat same-named application functions as extension calls.
Options
None. Enable or disable this rule through normal rule configuration.
Configure
In custos.json:
{
"rules": {
"NonExhaustiveEnumMatch": {
"enabled": false
}
}
}Suppress
Before the statement or declaration (or the first statement of the file):
// @custos-ignore NonExhaustiveEnumMatch