Skip to content

ReturnBinaryOrToEarlyReturnRector removes operands from a returned || chain containing a parenthesized && #9802

Description

@jasonbaciulis

Bug Report

Subject Details
Rector version v2.4.5 (also reproduced on dev-main via the demo)

ReturnBinaryOrToEarlyReturnRector silently removes operands from a returned || chain when the chain contains a parenthesized && operand, changing the code's semantics.

Given a return of 4+ ||-ed operands where a parenthesized && appears before the last two and a method call follows it, the rule emits early returns only for the operands after the && — the operands before it are deleted:

 public function hasStale(): bool
 {
-    return $this->donation === null
-        || ($this->commission > 0 && $this->transaction === null)
-        || $this->somethingElse()
-        || $this->anotherThing();
+    if ($this->somethingElse()) {
+        return true;
+    }
+    return $this->anotherThing();
 }

Minimal PHP Code Causing Issue

https://getrector.com/demo/0f4aa7d0-6594-4513-93fe-e39fa41afc4a

Notes on the reproduction — every element is load-bearing:

  • With only 3 operands (a || (b && c) || $this->d()) the rule correctly skips the transform; the bug needs 4+ operands so the && is met inside a nested recursion.
  • The operand after the && must be a method call, otherwise CallAnalyzer::doesIfHasObjectCall() bails before the broken output is emitted.

Suspected root cause: createMultipleIfs() returns [] when it encounters a BooleanAnd, intended as an abort signal that refactor() checks via $ifs === []. But when the && is reached inside the recursion through collectLeftBooleanOrToIfs(), the outer loop merges that empty array ($ifs = [...$ifs, ...$this->collectLeftBooleanOrToIfs(...)]) and continues collecting the remaining operands — the abort is swallowed, and everything before the && is lost. [] is ambiguous between "abort the whole transform" and "no ifs collected yet".

Expected Behaviour

Either a faithful transform with one early return per operand:

public function hasStale(): bool
{
    if ($this->donation === null) {
        return true;
    }
    if ($this->commission > 0 && $this->transaction === null) {
        return true;
    }
    if ($this->somethingElse()) {
        return true;
    }
    return (bool) $this->anotherThing();
}

or skipping the node entirely, as already happens for the 3-operand case containing &&.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions