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 November 3, 2026 18:28
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>
@staabm
staabm force-pushed the unnecessary-ternary branch from 453d403 to 5cb3ce4 Compare November 3, 2026 17:28
* Whether evaluating the expression can only produce its value, so that
* dropping it or evaluating it once more does not change anything.
*/
private function isPure(Expr $expr, Scope $scope): bool

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what I don't like about the PR is

  • isPure() function impl / don't we have this logic somewhere else already?
  • use of NodeTraverser which can be slow

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

2 participants