No Nested Switch
ID |
php.no_nested_switch |
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 statement written inside another switch.
The inner switch is reported, since that is the statement to extract. Nesting several levels deep
reports every inner level. The alternative syntax (switch (…): … endswitch;) is covered the same
way.
Rationale
<?php
function dispatch(string $verb, string $scope): string
{
switch ($verb) {
case 'get':
switch ($scope) { // FLAW — two selectors interleaved in one function
case 'user':
return 'get.user';
default:
return 'get.any';
}
default:
return 'unknown';
}
}
Two switches in one place force the reader to track two independent selector values and two sets of
case labels at the same time. Worse, break binds to the innermost enclosing switch, so an inner
break reads as if it left the outer one; getting that wrong silently falls through into the next
outer case. The number of paths through the function is the product of the two label counts, which
is why nested switches are where a missed combination hides.
Remediation
Extract the inner switch into a function named after the decision it makes. Each switch then has one selector, and the outer one reads as a single dispatch.
<?php
function dispatch(string $verb, string $scope): string
{
switch ($verb) {
case 'get':
return 'get.' . scopeOf($scope);
default:
return 'unknown';
}
}
function scopeOf(string $scope): string
{
switch ($scope) {
case 'user':
return 'user';
default:
return 'any';
}
}
When the inner decision maps one value to another with no statements involved, a match expression
replaces it inline and is not reported:
switch ($verb) {
case 'get':
return match ($scope) {
'user' => 'get.user',
default => 'get.any',
};
default:
return 'unknown';
}
Configuration
This rule has no configurable properties. Two shapes are excluded.
A switch inside a closure or an arrow function. The ancestor walk stops at the first enclosing
function, and a function () use (…) closure or an fn () ⇒ … arrow function is one. Such a body
is a unit of code the reader already considers on its own, even though it sits inside the outer
switch’s braces lexically.
match, in either position. PHP 8’s match is an expression whose arms hold a single
expression each, with no fall-through and no break, so the confusion this rule is about cannot
arise in one. It is also the construct the language now recommends in place of switch, so
reporting a match nested in a switch would penalise exactly the refactoring the rule asks for.
php.switch_default_first_or_last scopes itself to switch for the same reason.