Skip to content

Compare ternary arm values, not their truthiness, in exact narrowing contexts - #6580

Merged
ondrejmirtes merged 1 commit into
2.2.xfrom
ternary-exact-context-narrowing
Sep 24, 2026
Merged

ondrejmirtes merged 1 commit into
2.2.xfrom
ternary-exact-context-narrowing

Conversation

@ondrejmirtes

Copy link
Copy Markdown
Member

TernaryHandler::specifyTypes() narrows cond ? if : else through the boolean rewrite (cond && if) || (!cond && else) and passes the context down unchanged. That only holds for truthy/falsey contexts. For the exact contexts (=== true, === false, !== true, !== false) the arms became boolean operands, so e.g. (!$a ? f() : 0) !== false read 0 !== false as "0 is truthy", ruled out the $a arm and narrowed $a to false.

The assign-time conditional expressions (if $b is int then $a is false) are built from exactly this narrowing, so the wrong type then leaked into later branches.

Exact contexts now compare each arm to the constant instead ((cond && if !== false) || (!cond && else !== false), evaluated in a truthy context), so only the condition is treated by truthiness. Side effect: === false / === true on a ternary now narrows more precisely, e.g. (!$a ? f() : 0) === false gives $a as false.

Closes phpstan/phpstan#15307
Closes phpstan/phpstan#15226

🤖 Generated with Claude Code

https://claude.ai/code/session_01LuUxEfBoMtVfZmJsUxA85s

…contexts

`(cond ? if : else) !== false` was narrowed through the boolean rewrite
`(cond && if) || (!cond && else)` with the `!== false` context passed to the
arms, which reads `0 !== false` as `0` being truthy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LuUxEfBoMtVfZmJsUxA85s
@ondrejmirtes
ondrejmirtes merged commit fadf8d6 into 2.2.x Sep 24, 2026
861 of 874 checks passed
@ondrejmirtes
ondrejmirtes deleted the ternary-exact-context-narrowing branch September 24, 2026 20:26
ondrejmirtes added a commit that referenced this pull request Sep 24, 2026
The ternary exact-context narrowing (#6580) is ported onto the composed
(cond && if) || (!cond && else) narrowing: in an exact context the arms
become `arm === true` / `arm !== false` (...) operands of a truthy
decomposition, narrowed like the comparison node would be, with a native
mirror in turbo-ext/src/TernaryHandler.cpp.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LuUxEfBoMtVfZmJsUxA85s
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.

1 participant