Skip to content

Commit 96750b5

Browse files
committed
Infer return type of anonymous functions from the return statements
1 parent 22d363c commit 96750b5

15 files changed

+170
-20
lines changed

src/Analyser/DirectScopeFactory.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ class DirectScopeFactory implements ScopeFactory
3232

3333
private \PHPStan\Parser\Parser $parser;
3434

35+
private NodeScopeResolver $nodeScopeResolver;
36+
3537
private bool $treatPhpDocTypesAsCertain;
3638

3739
/** @var string[] */
@@ -46,6 +48,7 @@ public function __construct(
4648
TypeSpecifier $typeSpecifier,
4749
PropertyReflectionFinder $propertyReflectionFinder,
4850
\PHPStan\Parser\Parser $parser,
51+
NodeScopeResolver $nodeScopeResolver,
4952
bool $treatPhpDocTypesAsCertain,
5053
Container $container
5154
)
@@ -58,6 +61,7 @@ public function __construct(
5861
$this->typeSpecifier = $typeSpecifier;
5962
$this->propertyReflectionFinder = $propertyReflectionFinder;
6063
$this->parser = $parser;
64+
$this->nodeScopeResolver = $nodeScopeResolver;
6165
$this->treatPhpDocTypesAsCertain = $treatPhpDocTypesAsCertain;
6266
$this->dynamicConstantNames = $container->getParameter('dynamicConstantNames');
6367
}
@@ -115,6 +119,7 @@ public function create(
115119
$this->typeSpecifier,
116120
$this->propertyReflectionFinder,
117121
$this->parser,
122+
$this->nodeScopeResolver,
118123
$context,
119124
$declareStrictTypes,
120125
$constantTypes,

src/Analyser/LazyScopeFactory.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ public function create(
8787
$this->container->getByType(TypeSpecifier::class),
8888
$this->container->getByType(PropertyReflectionFinder::class),
8989
$this->container->getByType(\PHPStan\Parser\Parser::class),
90+
$this->container->getByType(NodeScopeResolver::class),
9091
$context,
9192
$declareStrictTypes,
9293
$constantTypes,

src/Analyser/MutatingScope.php

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
use PHPStan\Type\TypeWithClassName;
8787
use PHPStan\Type\UnionType;
8888
use PHPStan\Type\VerbosityLevel;
89+
use PHPStan\Type\VoidType;
8990
use function array_key_exists;
9091

9192
class MutatingScope implements Scope
@@ -115,6 +116,8 @@ class MutatingScope implements Scope
115116

116117
private Parser $parser;
117118

119+
private NodeScopeResolver $nodeScopeResolver;
120+
118121
private \PHPStan\Analyser\ScopeContext $context;
119122

120123
/** @var \PHPStan\Type\Type[] */
@@ -172,6 +175,7 @@ class MutatingScope implements Scope
172175
* @param \PHPStan\Analyser\TypeSpecifier $typeSpecifier
173176
* @param \PHPStan\Rules\Properties\PropertyReflectionFinder $propertyReflectionFinder
174177
* @param Parser $parser
178+
* @param NodeScopeResolver $nodeScopeResolver
175179
* @param \PHPStan\Analyser\ScopeContext $context
176180
* @param bool $declareStrictTypes
177181
* @param array<string, Type> $constantTypes
@@ -200,6 +204,7 @@ public function __construct(
200204
TypeSpecifier $typeSpecifier,
201205
PropertyReflectionFinder $propertyReflectionFinder,
202206
Parser $parser,
207+
NodeScopeResolver $nodeScopeResolver,
203208
ScopeContext $context,
204209
bool $declareStrictTypes = false,
205210
array $constantTypes = [],
@@ -232,6 +237,7 @@ public function __construct(
232237
$this->typeSpecifier = $typeSpecifier;
233238
$this->propertyReflectionFinder = $propertyReflectionFinder;
234239
$this->parser = $parser;
240+
$this->nodeScopeResolver = $nodeScopeResolver;
235241
$this->context = $context;
236242
$this->declareStrictTypes = $declareStrictTypes;
237243
$this->constantTypes = $constantTypes;
@@ -1317,7 +1323,37 @@ private function resolveType(Expr $node): Type
13171323
$returnType = TypehintHelper::decideType($this->getFunctionType($node->returnType, false, false), $returnType);
13181324
}
13191325
} else {
1320-
$returnType = $this->getFunctionType($node->returnType, $node->returnType === null, false);
1326+
$closureScope = $this->enterAnonymousFunctionWithoutReflection($node);
1327+
$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_) {
1330+
return;
1331+
}
1332+
1333+
$closureReturnStatements[] = [$node, $scope];
1334+
});
1335+
1336+
$returnTypes = [];
1337+
$hasNull = false;
1338+
foreach ($closureReturnStatements as [$returnNode, $returnScope]) {
1339+
if ($returnNode->expr === null) {
1340+
$hasNull = true;
1341+
continue;
1342+
}
1343+
1344+
$returnTypes[] = $returnScope->getType($returnNode->expr);
1345+
}
1346+
1347+
if (count($returnTypes) === 0) {
1348+
$returnType = new VoidType();
1349+
} else {
1350+
if ($hasNull) {
1351+
$returnTypes[] = new NullType();
1352+
}
1353+
$returnType = TypeCombinator::union(...$returnTypes);
1354+
}
1355+
1356+
$returnType = TypehintHelper::decideType($this->getFunctionType($node->returnType, false, false), $returnType);
13211357
}
13221358

13231359
return new ClosureType(
@@ -2079,6 +2115,7 @@ public function doNotTreatPhpDocTypesAsCertain(): Scope
20792115
$this->typeSpecifier,
20802116
$this->propertyReflectionFinder,
20812117
$this->parser,
2118+
$this->nodeScopeResolver,
20822119
$this->context,
20832120
$this->declareStrictTypes,
20842121
$this->constantTypes,
@@ -2710,6 +2747,43 @@ public function enterAnonymousFunction(
27102747
Expr\Closure $closure,
27112748
?array $callableParameters = null
27122749
): self
2750+
{
2751+
$anonymousFunctionReflection = $this->getType($closure);
2752+
if (!$anonymousFunctionReflection instanceof ClosureType) {
2753+
throw new \PHPStan\ShouldNotHappenException();
2754+
}
2755+
2756+
$scope = $this->enterAnonymousFunctionWithoutReflection($closure, $callableParameters);
2757+
2758+
return $this->scopeFactory->create(
2759+
$scope->context,
2760+
$scope->isDeclareStrictTypes(),
2761+
$scope->constantTypes,
2762+
$scope->getFunction(),
2763+
$scope->getNamespace(),
2764+
$scope->variableTypes,
2765+
$scope->moreSpecificTypes,
2766+
[],
2767+
$scope->inClosureBindScopeClass,
2768+
$anonymousFunctionReflection,
2769+
true,
2770+
[],
2771+
$scope->nativeExpressionTypes,
2772+
[],
2773+
false,
2774+
$this
2775+
);
2776+
}
2777+
2778+
/**
2779+
* @param \PhpParser\Node\Expr\Closure $closure
2780+
* @param \PHPStan\Reflection\ParameterReflection[]|null $callableParameters
2781+
* @return self
2782+
*/
2783+
private function enterAnonymousFunctionWithoutReflection(
2784+
Expr\Closure $closure,
2785+
?array $callableParameters = null
2786+
): self
27132787
{
27142788
$variableTypes = [];
27152789
foreach ($closure->params as $i => $parameter) {
@@ -2773,11 +2847,6 @@ public function enterAnonymousFunction(
27732847
$variableTypes['this'] = VariableTypeHolder::createYes($this->getVariableType('this'));
27742848
}
27752849

2776-
$anonymousFunctionReflection = $this->getType($closure);
2777-
if (!$anonymousFunctionReflection instanceof ClosureType) {
2778-
throw new \PHPStan\ShouldNotHappenException();
2779-
}
2780-
27812850
return $this->scopeFactory->create(
27822851
$this->context,
27832852
$this->isDeclareStrictTypes(),
@@ -2788,7 +2857,7 @@ public function enterAnonymousFunction(
27882857
$moreSpecificTypes,
27892858
[],
27902859
$this->inClosureBindScopeClass,
2791-
$anonymousFunctionReflection,
2860+
null,
27922861
true,
27932862
[],
27942863
$nativeTypes,

src/Testing/TestCase.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,7 @@ public function createScopeFactory(Broker $broker, TypeSpecifier $typeSpecifier)
555555
$typeSpecifier,
556556
new PropertyReflectionFinder(),
557557
$this->getParser(),
558+
self::getContainer()->getByType(NodeScopeResolver::class),
558559
$this->shouldTreatPhpDocTypesAsCertain(),
559560
$container
560561
);

src/Type/FileTypeMapper.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,8 @@ function (Node $node) use (&$dependentFiles) {
634634
}
635635
}
636636
}
637+
638+
return null;
637639
},
638640
static function (): void {
639641
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10720,6 +10720,11 @@ public function dataVarAboveDeclare(): array
1072010720
return $this->gatherAssertTypes(__DIR__ . '/data/var-above-declare.php');
1072110721
}
1072210722

10723+
public function dataClosureReturnType(): array
10724+
{
10725+
return $this->gatherAssertTypes(__DIR__ . '/data/closure-return-type.php');
10726+
}
10727+
1072310728
/**
1072410729
* @param string $file
1072510730
* @return array<string, mixed[]>
@@ -10929,6 +10934,7 @@ private function gatherAssertTypes(string $file): array
1092910934
* @dataProvider dataBug4351
1093010935
* @dataProvider dataVarAboveUse
1093110936
* @dataProvider dataVarAboveDeclare
10937+
* @dataProvider dataClosureReturnType
1093210938
* @param string $assertType
1093310939
* @param string $file
1093410940
* @param mixed ...$args

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
$predicate = function (object $thing): bool { return true; };
88

99
$closure = \Closure::fromCallable($predicate);
10-
assertType('Closure(object): bool', $closure);
10+
assertType('Closure(object): true', $closure);
1111

1212
$newThis = new class {};
1313
$boundClosure = $closure->bindTo($newThis);
14-
assertType('Closure(object): bool', $boundClosure);
14+
assertType('Closure(object): true', $boundClosure);
1515

1616
$staticallyBoundClosure = \Closure::bind($closure, $newThis);
17-
assertType('Closure(object): bool', $staticallyBoundClosure);
17+
assertType('Closure(object): true', $staticallyBoundClosure);
1818

1919
$returnType = $closure->call($newThis, new class {});
20-
assertType('bool', $returnType);
20+
assertType('true', $returnType);
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
namespace ClosureReturnType;
4+
5+
use function PHPStan\Analyser\assertType;
6+
7+
class Foo
8+
{
9+
10+
public function doFoo(int $i): void
11+
{
12+
$f = function () {
13+
14+
};
15+
assertType('void', $f());
16+
17+
$f = function () {
18+
return;
19+
};
20+
assertType('void', $f());
21+
22+
$f = function () {
23+
return 1;
24+
};
25+
assertType('1', $f());
26+
27+
$f = function (): array {
28+
return ['foo' => 'bar'];
29+
};
30+
assertType('array(\'foo\' => \'bar\')', $f());
31+
32+
$f = function (string $s) {
33+
return $s;
34+
};
35+
assertType('string', $f('foo'));
36+
37+
$f = function () use ($i) {
38+
return $i;
39+
};
40+
assertType('int', $f());
41+
42+
$f = function () use ($i) {
43+
if (rand(0, 1)) {
44+
return $i;
45+
}
46+
47+
return null;
48+
};
49+
assertType('int|null', $f());
50+
51+
$f = function () use ($i) {
52+
if (rand(0, 1)) {
53+
return $i;
54+
}
55+
56+
return;
57+
};
58+
assertType('int|null', $f());
59+
}
60+
61+
}

tests/PHPStan/Analyser/data/generics.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ function f($a, $b)
147147
*/
148148
function testF($arrayOfInt, $callableOrNull)
149149
{
150-
assertType('array<string>', f($arrayOfInt, function (int $a): string {
150+
assertType('array<string&numeric>', f($arrayOfInt, function (int $a): string {
151151
return (string)$a;
152152
}));
153153
assertType('array<string>', f($arrayOfInt, function ($a): string {

tests/PHPStan/Analyser/data/mixed-typehint.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ function (mixed $foo) {
2929
assertType('mixed', $foo);
3030
$f = function (): mixed {
3131

32+
};
33+
assertType('void', $f());
34+
35+
$f = function () use ($foo): mixed {
36+
return $foo;
3237
};
3338
assertType('mixed', $f());
3439
};

0 commit comments

Comments
 (0)