diff --git a/issue-bot/src/Console/RunCommand.php b/issue-bot/src/Console/RunCommand.php index 486b2f2aea1..e8519d8038f 100644 --- a/issue-bot/src/Console/RunCommand.php +++ b/issue-bot/src/Console/RunCommand.php @@ -21,6 +21,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Throwable; use function array_key_exists; +use function array_values; use function count; use function escapeshellarg; use function explode; @@ -143,7 +144,7 @@ static function ($result) use (&$results, &$pending, $currentIndex, $total, $run if (count($results) === $total) { ksort($results); - $deferred->resolve($results); + $deferred->resolve(array_values($results)); } else { $runNext(); } diff --git a/src/Analyser/Generics/ClosureSignatureInference.php b/src/Analyser/Generics/ClosureSignatureInference.php index c2062519c1c..8eaa247ec41 100644 --- a/src/Analyser/Generics/ClosureSignatureInference.php +++ b/src/Analyser/Generics/ClosureSignatureInference.php @@ -546,12 +546,23 @@ private static function byRefUseNames(Closure $expr): array private static function bodyYieldsOrInvokes(array $stmts, array $names): bool { $stack = $stmts; - while (count($stack) > 0) { - $node = array_pop($stack); - if ($node instanceof Node\FunctionLike || $node instanceof Node\Stmt\ClassLike) { + // closures and arrow functions written in the body: their yields are + // their own, but they can invoke the variables on the body's behalf + $nestedStack = []; + while (count($stack) > 0 || count($nestedStack) > 0) { + $nested = count($stack) === 0; + $node = $nested ? array_pop($nestedStack) : array_pop($stack); + if (!$nested && ($node instanceof Closure || $node instanceof ArrowFunction)) { + $nestedStack[] = $node; + continue; + } + if ( + ($node instanceof Node\FunctionLike && !$node instanceof Closure && !$node instanceof ArrowFunction) + || $node instanceof Node\Stmt\ClassLike + ) { continue; } - if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { + if (!$nested && ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom)) { return true; } if ( @@ -564,13 +575,14 @@ private static function bodyYieldsOrInvokes(array $stmts, array $names): bool } foreach ($node->getSubNodeNames() as $subNodeName) { $subNode = $node->$subNodeName; - if ($subNode instanceof Node) { - $stack[] = $subNode; - } elseif (is_array($subNode)) { - foreach ($subNode as $item) { - if (!$item instanceof Node) { - continue; - } + $subNodes = is_array($subNode) ? $subNode : [$subNode]; + foreach ($subNodes as $item) { + if (!$item instanceof Node) { + continue; + } + if ($nested) { + $nestedStack[] = $item; + } else { $stack[] = $item; } } diff --git a/tests/PHPStan/Analyser/nsrt/closure-byref-uses-at-invocation.php b/tests/PHPStan/Analyser/nsrt/closure-byref-uses-at-invocation.php index 0a0d738c2fa..21fa4b9abcf 100644 --- a/tests/PHPStan/Analyser/nsrt/closure-byref-uses-at-invocation.php +++ b/tests/PHPStan/Analyser/nsrt/closure-byref-uses-at-invocation.php @@ -504,4 +504,41 @@ public function referenceReboundByDestructuring(array $list): void assertType('int', $x); } + /** + * @param callable(callable(): void, callable(): void): void $then + */ + public function invokesItselfFromNestedClosure(callable $then): void + { + $rejected = false; + $runNext = function () use (&$runNext, &$rejected, $then): void { + assertType('bool', $rejected); + if ($rejected) { + return; + } + $then( + function () use ($runNext): void { + $runNext(); + }, + function () use (&$rejected): void { + $rejected = true; + }, + ); + }; + $runNext(); + } + + public function nestedGeneratorKeepsInvocationsFollowed(): void + { + $x = 1; + $c = function () use (&$x): void { + assertType('1', $x); + $g = function () { + yield 1; + }; + $x = 'a'; + }; + $c(); + assertType("'a'", $x); + } + } diff --git a/turbo-ext/src/ClosureSignatureInference.cpp b/turbo-ext/src/ClosureSignatureInference.cpp index ac4334ac077..a9c6a6ec0fc 100644 --- a/turbo-ext/src/ClosureSignatureInference.cpp +++ b/turbo-ext/src/ClosureSignatureInference.cpp @@ -583,20 +583,36 @@ zv::Val anonymousMarker(zval *site, zend_string *name, zend_long variance, zval stack.push(zv::Val::copyOf(entry.value().deref())); } } + /* closures and arrow functions written in the body: their yields are + * their own, but they can invoke the variables on the body's behalf */ + zv::Arr nestedStack = zv::Arr::create(0); for (;;) { zv::Val node = popNode(stack); - if (node.isUndef()) break; + bool nested = node.isUndef(); + if (nested) { + node = popNode(nestedStack); + if (node.isUndef()) break; + } zval *n = node.raw(); bool is; + bool isNestedFunction; + if (UNEXPECTED(!isA(n, PT_CLASS_CLOSURE_EXPR, isNestedFunction))) return false; + if (!isNestedFunction && UNEXPECTED(!isA(n, PT_CLASS_ARROW_FUNCTION, isNestedFunction))) return false; + if (!nested && isNestedFunction) { + nestedStack.push(std::move(node)); + continue; + } if (UNEXPECTED(!isA(n, PT_CLASS_FUNCTION_LIKE, is))) return false; - if (is) continue; + if (is && !isNestedFunction) continue; if (UNEXPECTED(!isA(n, PT_CLASS_CLASS_LIKE_STMT, is))) return false; if (is) continue; - if (UNEXPECTED(!isA(n, PT_CLASS_YIELD, is))) return false; - if (!is && UNEXPECTED(!isA(n, PT_CLASS_YIELD_FROM, is))) return false; - if (is) { - out = true; - return true; + if (!nested) { + if (UNEXPECTED(!isA(n, PT_CLASS_YIELD, is))) return false; + if (!is && UNEXPECTED(!isA(n, PT_CLASS_YIELD_FROM, is))) return false; + if (is) { + out = true; + return true; + } } if (UNEXPECTED(!isA(n, PT_CLASS_FUNC_CALL, is))) return false; if (is) { @@ -614,7 +630,7 @@ zv::Val anonymousMarker(zval *site, zend_string *name, zend_long variance, zval } } } - if (UNEXPECTED(!pushSubNodes(n, stack))) return false; + if (UNEXPECTED(!pushSubNodes(n, nested ? nestedStack : stack))) return false; } return true; }