Skip to content

Report ternary operators that always give one of their branches - #6894

Open
SanderMuller wants to merge 3 commits into
phpstan:2.3.xfrom
SanderMuller:unnecessary-ternary
Open

SanderMuller wants to merge 3 commits into
phpstan:2.3.xfrom
SanderMuller:unnecessary-ternary

Conversation

@SanderMuller

Copy link
Copy Markdown
Contributor

Adds a bleeding edge rule for a ternary that always gives one of its branches, including the short ?: form.

fn (?int $i) => $i !== null ? $i : null; // reported
fn (?int $i) => $i === null ? null : $i; // reported
fn (?Foo $f) => $f ?: null;              // reported
fn (?int $i) => $i ?: null;              // not reported, 0 becomes null

It stays quiet when:

  • a dropped or moved expression can have side effects. Only variables, literals, constants, ===, !==, !, &&, ||, instanceof and calls to functions without side effects count as pure. Property fetches do not, because of __get and hooks.
  • the moved branch has a call, for example $s === false ? 0 : strlen($s). Self-analysis found this one.
  • the value is a float, because -0.0 ?: 0.0 gives 0.0.
  • a truthiness check is on a value that can be a falsy object, for example an empty SimpleXMLElement. The rule uses toBoolean() for this, so ?Countable ?: null is still reported until toBoolean() returns bool for these interfaces. Return bool from toBoolean() of object, object{} and interfaces that SimpleXMLElement, GMP or BcMath\Number can implement #6892 proposes that.

On a 4.5k-file real-world project it reports one true positive, with no measurable CPU cost (113.9 s on, 113.1 s off, medians of 3).

Closes phpstan/phpstan#15608

🤖 Generated with Claude Code

SanderMuller and others added 3 commits October 31, 2026 18:56
The rule is behind the unnecessaryTernary toggle, which bleeding edge
turns on. It covers the short ?: form too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The constant condition and float checks now read the constant values,
and the object check uses isObject(), so a mutant of them changes the
result. New test cases cover a constant condition and a function that
can have side effects.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ternary operator is unnecessary

1 participant