Skip to content

Pick the ternary's final scope from the native condition type when treatPhpDocTypesAsCertain is off - #6822

Open
phpstan-bot wants to merge 1 commit into
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-l0yswwg
Open

phpstan-bot wants to merge 1 commit into
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-l0yswwg

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

With treatPhpDocTypesAsCertain: false, a repeated $x === null ? ... : ... on a value whose type comes only from PHPDoc (@return Expr, no native type) was reported as identical.alwaysFalse. After the first ternary, the native type of $x was mixed~null. The branch where it is null had been thrown away.

The ternary now picks its final scope from the native condition type when PHPDoc types are not treated as certain.

Changes

  • src/Analyser/ExprHandler/TernaryHandler.php: new #[AutowiredParameter] bool $treatPhpDocTypesAsCertain constructor parameter. The "condition is always true/false, keep a single branch scope" decision now reads getNativeType() when it is off.
  • turbo-ext/src/TernaryHandler.cpp, turbo-ext/src/generated/TernaryHandler.h: the same change in the native twin (new property slot, constructor parameter, native-type read).
  • Probed the analogous constructs with the same PHPDoc-only reproducer: if/elseif, switch, match, &&, ||, ??, while and the short ternary ?:. All except the ternary (long and short, which share this code) already behaved correctly. IfHandler, ForHandler, WhileHandler, DoWhileHandler and MatchHandler already use the native type in this mode.

Root cause

TernaryHandler::processExpr() used $ternaryCondResult->getType() to decide whether the condition is always true or false. If it was, the handler kept only one branch scope instead of merging both. The PHPDoc type Expr makes $x === null always false, so only the falsey scope was kept. That scope's native type for $x is mixed~null. The native null from the truthy branch was lost, and PHPDoc certainty leaked into the native type. The statement handlers already gate this decision on treatPhpDocTypesAsCertain; the ternary did not.

Test

  • New StrictComparisonOfDifferentTypesRuleWithUncertainPhpDocTypesTest (loads uncertain-phpdoc-types.neon) analysing tests/PHPStan/Rules/Comparison/data/bug-15403.php.
  • The data file contains:
    • the issue's reproducer verbatim
    • a version with the ternaries assigned to variables
    • a short-ternary variant
    • a negated !== variant
  • The test expects no errors. Without the fix it reports three false positives (the assigned, short-ternary and negated variants).
  • The verbatim reproducer reproduces in the full CLI but not in this single-rule test: there, another rule first asks for the type of the return expression on demand. It is kept in the file as written in the issue.
  • ImpossibleInstanceOfRuleWithUncertainPhpDocTypesTest::testTernaryElseDoNotReportPhpDoc: removed the line-20 expectation. Line 20 repeats the PHPDoc-only instanceof check from line 19, which is correctly not reported. The line-19 ternary dropped its falsey scope, which is this same bug, so the expectation was wrong.
  • Also verified with the turbo extension loaded: signature parity, side-by-side.php and smoke.php pass.

Fixes phpstan/phpstan#15403

🤖 Generated with Claude Code

…reatPhpDocTypesAsCertain` is off

- TernaryHandler decided whether to keep only the truthy or only the falsey
  branch scope from the PHPDoc condition type. With a PHPDoc-only `@return Expr`,
  `$x === null` is PHPDoc-false, so the truthy branch (where the native type is
  `null`) was dropped and the native type stayed `mixed~null` after the ternary,
  making a repeated `$x === null` "always false" even with
  treatPhpDocTypesAsCertain: false.
- Inject the `treatPhpDocTypesAsCertain` parameter and read the native
  condition type when it is off, like IfHandler, ForHandler, WhileHandler,
  DoWhileHandler and MatchHandler already do.
- Port the change to the turbo-ext C++ twin (TernaryHandler.cpp + regenerated
  declarations header).
- Drop the stale line-20 expectation in ImpossibleInstanceOfRuleWithUncertainPhpDocTypesTest:
  it was produced by the same scope drop after the line-19 ternary.
- Probed if/elseif, switch, match, &&, ||, ??, while and short ternary; only
  the ternary (long and short) was affected.
@VincentLanglet
VincentLanglet requested a review from staabm October 24, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants