Detect generators by scanning the body for yield, including unreachable code and excluding nested functions#5910
Open
phpstan-bot wants to merge 1 commit into
Conversation
…able code and excluding nested functions
- Add `YieldFindingVisitor` that collects every `yield`/`yield from` in a
function-like body, regardless of reachability, while not descending into
nested function-likes (a `yield` in a nested closure makes that closure a
generator, not the outer scope).
- `NodeScopeResolver`: gather the `*ReturnStatementsNode` yield statements with
the visitor instead of accumulating only the reachable yields visited by the
node callback, so `isGenerator()` is correct for functions, methods and
closures even when the only `yield` is unreachable.
- `NodeScopeResolver`: compute a syntactic `bodyHasYield` flag for the
`ExecutionEndNode` so `MissingReturnRule` no longer reports a missing return
for generators whose `yield` sits behind a terminating statement (e.g.
`while (false) { return; yield; }` or `throw; yield;`). The flow-based
`hasYield` propagated in `StatementResult` is left untouched so loop-condition
rules keep using reachable yields only.
- `PhpFunctionFromParserNodeReflection::isGenerator()`: use the visitor instead
of a recursive walk that descended into nested function-likes. Previously a
function/method containing a nested generator closure was wrongly treated as a
generator, which suppressed its return-type check.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A
yieldanywhere in a function body makes it a generator at compile time, even when theyieldis unreachable. PHPStan determined generator-ness from the reachable control flow ($hasYieldtracked throughStatementResult), so ayieldplaced behind a terminating statement was missed:PHPStan wrongly reported
Function foo() should return Generator but return statement is missing.This fix determines generator-ness syntactically instead.Changes
src/Parser/YieldFindingVisitor.php(new): collects everyyield/yield frombelonging to a single function-like, regardless of reachability, and does not descend into nested function-likes.src/Analyser/NodeScopeResolver.php:FunctionReturnStatementsNode,MethodReturnStatementsNodeandClosureReturnStatementsNodewith the visitor (findInNodes($stmts)) instead of only collecting reachableyieldnodes seen by the node callback. This fixes the node-basedisGenerator()/getYieldStatements()used byClosureReturnTypeRuleandNeverRuleHelper.bodyHasYieldflag and use it for thehasYieldof theExecutionEndNodeconsumed byMissingReturnRule. The flow-basedhasYieldcarried by the returnedStatementResultis intentionally left unchanged, so loop-condition rules (WhileLoopAlwaysTrueConditionRule,DoWhileLoopConstantConditionRule, …) keep reasoning about reachable yields only.src/Reflection/Php/PhpFunctionFromParserNodeReflection.php:isGenerator()now uses the visitor on the body statements instead of a recursive walk that descended into nested function-likes.Root cause
Two facets of the same problem — "is this function a generator?" was answered without a correct syntactic notion of "a
yieldthat belongs to this function body":Unreachable yields were ignored.
MissingReturnRule(viaStatementResult::hasYield()) and the node-basedisGenerator()(via yields gathered by the node callback) only saw yields on reachable control-flow paths. Ayieldafterreturn/throw, or inside awhile (false)body, made the function a generator in PHP but not in PHPStan.Nested yields were over-counted.
PhpFunctionFromParserNodeReflection::isGenerator()recursively walked the whole function node and treated ayieldinside a nested closure as belonging to the outer function. That falsely marked the outer function as a generator and suppressed its return-type check (FunctionReturnTypeCheckshort-circuits for generators), so a real return-type mismatch went unreported.A single
YieldFindingVisitor(find yields anywhere in the body, stop at nested function-likes) fixes both facets and is now the one place generator-ness is derived from.Test
tests/PHPStan/Rules/Missing/data/bug-4100.php+MissingReturnRuleTest::testBug4100: the reported snippet plus thethrowvariant from the discussion, covering a function, a method and a closure with an unreachableyield. Fails before the fix with fourreturn statement is missingerrors.tests/PHPStan/Rules/Functions/data/bug-4100.php+ReturnTypeRuleTest::testBug4100: a non-generator function returning the wrong type while containing a nested generator closure. The return-type mismatch is only reported after the over-detection is fixed.Fixes phpstan/phpstan#4100