Duplicate Condition In If Chain
ID |
php.duplicate_condition_in_if_chain |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
Php |
Tags |
control-flow, dead-code, reliability |
Description
Reports a condition that is structurally identical to an earlier condition in the same
if / elseif chain, the same switch, or the same match.
Each finding sits on the duplicated condition — the branch that can never run — rather than on the chain as a whole, so three copies of one condition produce two findings.
Rationale
<?php
function describe(int $code): string
{
if ($code === 200) {
return 'ok';
} elseif ($code === 404) {
return 'missing';
} elseif ($code === 200) { // FLAW — the first branch already took this code
return 'cached';
}
return 'other';
}
An if / elseif chain, a switch and a match all evaluate their branches top to bottom and
stop at the first one that matches. A condition that repeats an earlier one therefore selects a
branch that can never execute. The code it guards is dead, and — more to the point — the case the
author meant to handle there is not handled at all.
The shape is almost always a copy-paste: the branch was duplicated to add a case and one operand
was never changed. Nothing in the language complains. In a match, the consequence is sharper
still: the value falls through to default, or to an UnhandledMatchError when there is no
default.
switch ($code) {
case 200: return 'ok';
case 404: return 'missing';
case 200: return 'cached'; // FLAW — unreachable case label
}
Remediation
Decide which case the duplicated branch was meant to cover and fix the condition; or, if the branch is genuinely redundant, delete it.
<?php
function describe(int $code): string
{
if ($code === 200) {
return 'ok';
} elseif ($code === 404) {
return 'missing';
} elseif ($code === 203) {
return 'cached';
}
return 'other';
}
When two branches really do share a condition and a body, merge them — case 200: case 203: in a
switch, 200, 203 ⇒ in a match, || in an if.
Configuration
This rule has no configurable properties. Three points are worth spelling out.
Comparison is scoped to one construct. The same condition written in two unrelated if
statements, or in a chain nested inside another chain, is ordinary code and is not reported.
Comparison is structural, not logical. Conditions are compared as parsed-token sequences, so
comments and formatting are ignored, but $a && $b and $b && $a are two different conditions.
Reporting logically equivalent forms would need reasoning the rule deliberately does not attempt.
A condition that calls, constructs or assigns is excluded, in either position. Two textually identical calls do not make the later branch provably dead, because a call may answer differently on each evaluation:
if ($rows->current() === null) { // OK — the iterator has moved on by the second test
return 'empty';
} elseif ($rows->current() === null) {
return 'exhausted';
}
Both spellings of a rung — elseif and else if — and the alternative syntax
(if (…): … elseif (…): … endif;) are analysed as one chain. In a switch, comparison is label by
label, so a grouped case 200: case 201: contributes two conditions and a later case 201: is
reported on its own.
This rule compares conditions. php.condition_similar_block compares branch bodies and reports
at the chain’s leading if, so the two never land on the same line.