Switch Default First Or Last
ID |
php.switch_default_first_or_last |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Code Smell |
Language |
Php |
Tags |
code-style, control-flow, switch |
Description
Reports a switch whose default clause is neither the first nor the last clause.
A switch with fewer than three clauses has no middle and is never reported. The alternative syntax
(switch (…): … endswitch;) is covered the same way.
Rationale
<?php
function statusName(int $code): string
{
switch ($code) {
case 200:
return 'ok';
default: // FLAW — buried between the cases
return 'other';
case 404:
return 'missing';
}
}
PHP does not care where default sits, so its position carries no meaning to the engine — only to
the reader, who scans the clause list for the fallback and expects it at one end. A default in the
middle is routinely missed during review, and it is often the fossil of a case that was appended
after it by mistake.
In PHP the readability cost comes with a correctness trap. Clauses fall through unless they break
or return, and a mid-list default falls into the case written below it:
switch ($code) {
case 200:
$name = 'ok';
break;
default:
$name = 'other'; // no break — falls into case 404
case 404:
$name = 'missing'; // every unmatched code ends up here
break;
}
The fallback silently becomes "missing" for every unknown code. Nothing about the source says so at
a glance, because the reader’s eye stops at the assignment inside default.
Remediation
Move default to the end of the switch — or to the very start, when the fallback is what the reader
should see first — and check the break on both the clause you moved and the one it used to precede.
<?php
function statusName(int $code): string
{
switch ($code) {
case 200:
return 'ok';
case 404:
return 'missing';
default:
return 'other';
}
}
When the switch only maps a value to another value, a match expression removes the question
entirely: its arms are exhaustive, they do not fall through, and default reads as an arm rather
than a clause.
return match ($code) {
200 => 'ok',
404 => 'missing',
default => 'other',
};
Configuration
This rule has no configurable properties. Three points are worth spelling out.
A clause with stacked labels counts by the position of the whole clause and is reported at its
first label. case 201: default: is one clause sharing one body, so it holds one position in the
list; PHP keeps no separate node for an individual label.
A nested switch is judged on its own. Each switch is examined independently, so an inner one
with a buried default is reported whether or not the outer one has the same problem. Whether the
nesting itself is a smell is `php.no_nested_switch’s finding.
match is out of scope, as it is for php.no_nested_switch. Its default arm is the same
fallback idea, but an arm list is a comma-separated expression list read in one glance rather than a
sequence of statement blocks, and a mid-list default in a match cannot swallow a following arm,
because match has no fall-through.
A switch with no default clause at all is a different finding — see php.missing_switch_default.
A clause that falls through for want of a break is `php.no_fallthrough’s.