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.

© 2026 Xygeni. All rights reserved