Skip to content

Carry template argument constraints of if/elseif/else branches ruled out by the condition onto the if result scope - #6827

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

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

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

With unresolvedTemplateArguments (bleedingEdge), a generic call inside an if branch whose condition is always false got unresolved(*NEVER*) template arguments. The result was a false-positive return.type, or generator.valueType for a yield. The template arguments of such a call come only from the context, for example the declared return type. That context never reached the resolver, because IfHandler dropped the dead branch's whole statement result. Now the constraints of the dead branch are kept.

Changes

  • src/Analyser/StmtHandler/IfHandler.php: the scopes of branches that do not take part in the result are collected. This covers an if with an always-false condition, plus elseif/else branches that come after an always-true condition. Their template argument constraints are added to the final scope.
  • turbo-ext/src/IfHandler.cpp: the same change in the native mirror.
  • Probed and already correct: dead while, for, foreach over an empty array, switch case, match arm, ternary, && right side, dead catch, unreachable statement after return. Dead elseif/else branches already worked in the probed shapes. They are now handled the same way and are covered by the test.

Root cause

InternalStatementResult merges the constraints of exit points and end statements into its scope. IfHandler only kept those for branches the condition does not rule out. A ruled-out branch is still walked, and rules still run on it. Its calls registered template argument sites, but the constraints meant to resolve them, such as the return send from ReturnHandler, were thrown away. So the sites resolved to their initial never. Other handlers already add the constraints of parts that do not flow into their result: TernaryHandler, ForHandler, TryCatchHandler, BooleanAndHandler/BooleanOrHandler. IfHandler was the one that did not.

Test

  • ReturnTypeRuleTest::testBug15476 (tests/PHPStan/Rules/Methods/data/bug-15476.php): the issue's reproducer, plus a dead elseif, a dead else, an else after an always-true elseif, and a dead branch nested in a live if.
  • YieldTypeRuleTest::testBug15476 (tests/PHPStan/Rules/Generators/data/bug-15476.php): the same pattern with yield in a dead if branch and a dead else.
  • Both failed before the fix. They pass with and without the turbo extension loaded.

Fixes phpstan/phpstan#15476

🤖 Generated with Claude Code

…ruled out by the condition onto the `if` result scope

- IfHandler dropped the whole statement result of a branch whose condition
  is always false (or an elseif/else after an always-true condition),
  including the template argument constraints collected in it, e.g. the
  send from the declared return type. The branch is still walked and its
  rules still see its calls, so with unresolvedTemplateArguments their
  template arguments stayed unresolved(*NEVER*) and return.type /
  generator.valueType were reported.
- The dropped branches' scopes now contribute their constraints to the
  final scope, like TernaryHandler, ForHandler and TryCatchHandler already
  do for parts that do not flow into the result.
- Ported the same change to the turbo-ext IfHandler.cpp mirror.
- Probed while/for/foreach/switch/match/ternary/&&/dead catch/unreachable
  statement after return: already correct.
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.

return.type with unresolved(*NEVER*) template arguments in an if branch whose condition is always false (unresolvedTemplateArguments)

1 participant