Skip to content

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
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-q71sugo
Open

Detect generators by scanning the body for yield, including unreachable code and excluding nested functions#5910
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-q71sugo

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

A yield anywhere in a function body makes it a generator at compile time, even when the yield is unreachable. PHPStan determined generator-ness from the reachable control flow ($hasYield tracked through StatementResult), so a yield placed behind a terminating statement was missed:

function foo(): Generator {
	while (false) {
		return;
		yield 2; // unreachable, but still makes foo() a generator
	}
}

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 every yield/yield from belonging to a single function-like, regardless of reachability, and does not descend into nested function-likes.
  • src/Analyser/NodeScopeResolver.php:
    • Gather the yield statements for FunctionReturnStatementsNode, MethodReturnStatementsNode and ClosureReturnStatementsNode with the visitor (findInNodes($stmts)) instead of only collecting reachable yield nodes seen by the node callback. This fixes the node-based isGenerator()/getYieldStatements() used by ClosureReturnTypeRule and NeverRuleHelper.
    • Compute a syntactic bodyHasYield flag and use it for the hasYield of the ExecutionEndNode consumed by MissingReturnRule. The flow-based hasYield carried by the returned StatementResult is 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 yield that belongs to this function body":

  1. Unreachable yields were ignored. MissingReturnRule (via StatementResult::hasYield()) and the node-based isGenerator() (via yields gathered by the node callback) only saw yields on reachable control-flow paths. A yield after return/throw, or inside a while (false) body, made the function a generator in PHP but not in PHPStan.

  2. Nested yields were over-counted. PhpFunctionFromParserNodeReflection::isGenerator() recursively walked the whole function node and treated a yield inside a nested closure as belonging to the outer function. That falsely marked the outer function as a generator and suppressed its return-type check (FunctionReturnTypeCheck short-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 the throw variant from the discussion, covering a function, a method and a closure with an unreachable yield. Fails before the fix with four return statement is missing errors.
  • 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

…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.
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.

Yield not detected / make $hasYield less error-prone

2 participants