Skip to content

Commit 2d2da19

Browse files
committed
Closure return type inference - understand generators
1 parent 4a6ccea commit 2d2da19

File tree

5 files changed

+83
-8
lines changed

5 files changed

+83
-8
lines changed

src/Analyser/MutatingScope.php

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1325,12 +1325,17 @@ private function resolveType(Expr $node): Type
13251325
} else {
13261326
$closureScope = $this->enterAnonymousFunctionWithoutReflection($node);
13271327
$closureReturnStatements = [];
1328-
$this->nodeScopeResolver->processStmtNodes($node, $node->stmts, $closureScope, static function (Node $node, Scope $scope) use (&$closureReturnStatements): void {
1329-
if (!$node instanceof Node\Stmt\Return_) {
1328+
$closureYieldStatements = [];
1329+
$this->nodeScopeResolver->processStmtNodes($node, $node->stmts, $closureScope, static function (Node $node, Scope $scope) use (&$closureReturnStatements, &$closureYieldStatements): void {
1330+
if ($node instanceof Node\Stmt\Return_) {
1331+
$closureReturnStatements[] = [$node, $scope];
1332+
}
1333+
1334+
if (!$node instanceof Expr\Yield_ && !$node instanceof Expr\YieldFrom) {
13301335
return;
13311336
}
13321337

1333-
$closureReturnStatements[] = [$node, $scope];
1338+
$closureYieldStatements[] = [$node, $scope];
13341339
});
13351340

13361341
$returnTypes = [];
@@ -1353,7 +1358,40 @@ private function resolveType(Expr $node): Type
13531358
$returnType = TypeCombinator::union(...$returnTypes);
13541359
}
13551360

1356-
$returnType = TypehintHelper::decideType($this->getFunctionType($node->returnType, false, false), $returnType);
1361+
if (count($closureYieldStatements) > 0) {
1362+
$keyTypes = [];
1363+
$valueTypes = [];
1364+
foreach ($closureYieldStatements as [$yieldNode, $yieldScope]) {
1365+
if ($yieldNode instanceof Expr\Yield_) {
1366+
if ($yieldNode->key === null) {
1367+
$keyTypes[] = new IntegerType();
1368+
} else {
1369+
$keyTypes[] = $yieldScope->getType($yieldNode->key);
1370+
}
1371+
1372+
if ($yieldNode->value === null) {
1373+
$valueTypes[] = new NullType();
1374+
} else {
1375+
$valueTypes[] = $yieldScope->getType($yieldNode->value);
1376+
}
1377+
1378+
continue;
1379+
}
1380+
1381+
$yieldFromType = $yieldScope->getType($yieldNode->expr);
1382+
$keyTypes[] = $yieldFromType->getIterableKeyType();
1383+
$valueTypes[] = $yieldFromType->getIterableValueType();
1384+
}
1385+
1386+
$returnType = new GenericObjectType(\Generator::class, [
1387+
TypeCombinator::union(...$keyTypes),
1388+
TypeCombinator::union(...$valueTypes),
1389+
new MixedType(),
1390+
$returnType,
1391+
]);
1392+
} else {
1393+
$returnType = TypehintHelper::decideType($this->getFunctionType($node->returnType, false, false), $returnType);
1394+
}
13571395
}
13581396

13591397
return new ClosureType(

src/Analyser/NodeScopeResolver.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2303,8 +2303,9 @@ private function processClosureNode(
23032303
$nodeCallback(new InClosureNode($expr), $closureScope);
23042304

23052305
$gatheredReturnStatements = [];
2306+
$gatheredYieldStatements = [];
23062307
$encounteredAnotherClosure = false;
2307-
$closureStmtsCallback = static function (\PhpParser\Node $node, Scope $scope) use ($nodeCallback, &$gatheredReturnStatements, &$encounteredAnotherClosure): void {
2308+
$closureStmtsCallback = static function (\PhpParser\Node $node, Scope $scope) use ($nodeCallback, &$gatheredReturnStatements, &$gatheredYieldStatements, &$encounteredAnotherClosure): void {
23082309
$nodeCallback($node, $scope);
23092310
if ($encounteredAnotherClosure) {
23102311
return;
@@ -2313,6 +2314,9 @@ private function processClosureNode(
23132314
$encounteredAnotherClosure = true;
23142315
return;
23152316
}
2317+
if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) {
2318+
$gatheredYieldStatements[] = $node;
2319+
}
23162320
if (!$node instanceof Return_) {
23172321
return;
23182322
}
@@ -2324,6 +2328,7 @@ private function processClosureNode(
23242328
$nodeCallback(new ClosureReturnStatementsNode(
23252329
$expr,
23262330
$gatheredReturnStatements,
2331+
$gatheredYieldStatements,
23272332
$statementResult
23282333
), $closureScope);
23292334

@@ -2352,6 +2357,7 @@ private function processClosureNode(
23522357
$nodeCallback(new ClosureReturnStatementsNode(
23532358
$expr,
23542359
$gatheredReturnStatements,
2360+
$gatheredYieldStatements,
23552361
$statementResult
23562362
), $closureScope);
23572363

src/Node/ClosureReturnStatementsNode.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace PHPStan\Node;
44

55
use PhpParser\Node\Expr\Closure;
6+
use PhpParser\Node\Expr\Yield_;
7+
use PhpParser\Node\Expr\YieldFrom;
68
use PhpParser\NodeAbstract;
79
use PHPStan\Analyser\StatementResult;
810

@@ -14,22 +16,28 @@ class ClosureReturnStatementsNode extends NodeAbstract implements ReturnStatemen
1416
/** @var \PHPStan\Node\ReturnStatement[] */
1517
private array $returnStatements;
1618

19+
/** @var array<int, Yield_|YieldFrom> */
20+
private array $yieldStatements;
21+
1722
private StatementResult $statementResult;
1823

1924
/**
2025
* @param \PhpParser\Node\Expr\Closure $closureExpr
2126
* @param \PHPStan\Node\ReturnStatement[] $returnStatements
27+
* @param array<int, Yield_|YieldFrom> $yieldStatements
2228
* @param \PHPStan\Analyser\StatementResult $statementResult
2329
*/
2430
public function __construct(
2531
Closure $closureExpr,
2632
array $returnStatements,
33+
array $yieldStatements,
2734
StatementResult $statementResult
2835
)
2936
{
3037
parent::__construct($closureExpr->getAttributes());
3138
$this->closureExpr = $closureExpr;
3239
$this->returnStatements = $returnStatements;
40+
$this->yieldStatements = $yieldStatements;
3341
$this->statementResult = $statementResult;
3442
}
3543

@@ -46,6 +54,14 @@ public function getReturnStatements(): array
4654
return $this->returnStatements;
4755
}
4856

57+
/**
58+
* @return array<int, Yield_|YieldFrom>
59+
*/
60+
public function getYieldStatements(): array
61+
{
62+
return $this->yieldStatements;
63+
}
64+
4965
public function getStatementResult(): StatementResult
5066
{
5167
return $this->statementResult;

src/Rules/Functions/ClosureReturnTypeRule.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Node\ClosureReturnStatementsNode;
88
use PHPStan\Rules\FunctionReturnTypeCheck;
9-
use PHPStan\Type\ObjectType;
109
use PHPStan\Type\TypeCombinator;
1110

1211
/**
@@ -37,7 +36,6 @@ public function processNode(Node $node, Scope $scope): array
3736
$returnType = $scope->getAnonymousFunctionReturnType();
3837
$containsNull = TypeCombinator::containsNull($returnType);
3938
$hasNativeTypehint = $node->getClosureExpr()->returnType !== null;
40-
$generatorType = new ObjectType(\Generator::class);
4139

4240
$messages = [];
4341
foreach ($node->getReturnStatements() as $returnStatement) {
@@ -55,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array
5553
'Anonymous function with return type void returns %s but should not return anything.',
5654
'Anonymous function should return %s but returns %s.',
5755
'Anonymous function should never return but return statement found.',
58-
$generatorType->isSuperTypeOf($returnType)->yes()
56+
count($node->getYieldStatements()) > 0
5957
);
6058

6159
foreach ($returnMessages as $returnMessage) {

tests/PHPStan/Analyser/data/closure-return-type.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,23 @@ public function doFoo(int $i): void
5656
return;
5757
};
5858
assertType('int|null', $f());
59+
60+
$f = function () {
61+
yield 1;
62+
return 2;
63+
};
64+
assertType('Generator<int, 1, mixed, 2>', $f());
65+
66+
$g = function () use ($f) {
67+
yield from $f();
68+
};
69+
assertType('Generator<int, 1, mixed, void>', $g());
70+
71+
$h = function (): \Generator {
72+
yield 1;
73+
return 2;
74+
};
75+
assertType('Generator<int, 1, mixed, 2>', $h());
5976
}
6077

6178
}

0 commit comments

Comments
 (0)