Skip to content

Commit 3dbb89e

Browse files
leongersenondrejmirtes
authored andcommitted
Use argument types as parameter types for inline closures (#7798)
1 parent 087141e commit 3dbb89e

File tree

7 files changed

+181
-8
lines changed

7 files changed

+181
-8
lines changed

conf/config.neon

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,16 @@ services:
417417
tags:
418418
- phpstan.parser.richParserNodeVisitor
419419

420+
-
421+
class: PHPStan\Parser\ClosureArgVisitor
422+
tags:
423+
- phpstan.parser.richParserNodeVisitor
424+
425+
-
426+
class: PHPStan\Parser\ArrowFunctionArgVisitor
427+
tags:
428+
- phpstan.parser.richParserNodeVisitor
429+
420430
-
421431
class: PHPStan\Parser\NewAssignedToPropertyVisitor
422432
tags:

src/Analyser/NodeScopeResolver.php

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
use PHPStan\Reflection\InitializerExprTypeResolver;
109109
use PHPStan\Reflection\MethodReflection;
110110
use PHPStan\Reflection\Native\NativeMethodReflection;
111+
use PHPStan\Reflection\Native\NativeParameterReflection;
111112
use PHPStan\Reflection\ParametersAcceptor;
112113
use PHPStan\Reflection\ParametersAcceptorSelector;
113114
use PHPStan\Reflection\Php\PhpMethodReflection;
@@ -2960,21 +2961,42 @@ private function processClosureNode(
29602961

29612962
$byRefUses = [];
29622963

2963-
if ($passedToType !== null && !$passedToType->isCallable()->no()) {
2964+
$callableParameters = null;
2965+
$closureCallArgs = $expr->getAttribute('closureCallArgs');
2966+
2967+
if ($closureCallArgs !== null) {
2968+
$acceptors = $scope->getType($expr)->getCallableParametersAcceptors($scope);
2969+
if (count($acceptors) === 1) {
2970+
$callableParameters = $acceptors[0]->getParameters();
2971+
2972+
foreach ($callableParameters as $index => $callableParameter) {
2973+
if (!isset($closureCallArgs[$index])) {
2974+
continue;
2975+
}
2976+
2977+
$type = $scope->getType($closureCallArgs[$index]->value);
2978+
$callableParameters[$index] = new NativeParameterReflection(
2979+
$callableParameter->getName(),
2980+
$callableParameter->isOptional(),
2981+
$type,
2982+
$callableParameter->passedByReference(),
2983+
$callableParameter->isVariadic(),
2984+
$callableParameter->getDefaultValue(),
2985+
);
2986+
}
2987+
}
2988+
} elseif ($passedToType !== null && !$passedToType->isCallable()->no()) {
29642989
if ($passedToType instanceof UnionType) {
29652990
$passedToType = TypeCombinator::union(...array_filter(
29662991
$passedToType->getTypes(),
29672992
static fn (Type $type) => $type->isCallable()->yes(),
29682993
));
29692994
}
29702995

2971-
$callableParameters = null;
29722996
$acceptors = $passedToType->getCallableParametersAcceptors($scope);
29732997
if (count($acceptors) === 1) {
29742998
$callableParameters = $acceptors[0]->getParameters();
29752999
}
2976-
} else {
2977-
$callableParameters = null;
29783000
}
29793001

29803002
$useScope = $scope;
@@ -3101,21 +3123,42 @@ private function processArrowFunctionNode(
31013123
$nodeCallback($expr->returnType, $scope);
31023124
}
31033125

3104-
if ($passedToType !== null && !$passedToType->isCallable()->no()) {
3126+
$callableParameters = null;
3127+
$arrowFunctionCallArgs = $expr->getAttribute('arrowFunctionCallArgs');
3128+
3129+
if ($arrowFunctionCallArgs !== null) {
3130+
$acceptors = $scope->getType($expr)->getCallableParametersAcceptors($scope);
3131+
if (count($acceptors) === 1) {
3132+
$callableParameters = $acceptors[0]->getParameters();
3133+
3134+
foreach ($callableParameters as $index => $callableParameter) {
3135+
if (!isset($arrowFunctionCallArgs[$index])) {
3136+
continue;
3137+
}
3138+
3139+
$type = $scope->getType($arrowFunctionCallArgs[$index]->value);
3140+
$callableParameters[$index] = new NativeParameterReflection(
3141+
$callableParameter->getName(),
3142+
$callableParameter->isOptional(),
3143+
$type,
3144+
$callableParameter->passedByReference(),
3145+
$callableParameter->isVariadic(),
3146+
$callableParameter->getDefaultValue(),
3147+
);
3148+
}
3149+
}
3150+
} elseif ($passedToType !== null && !$passedToType->isCallable()->no()) {
31053151
if ($passedToType instanceof UnionType) {
31063152
$passedToType = TypeCombinator::union(...array_filter(
31073153
$passedToType->getTypes(),
31083154
static fn (Type $type) => $type->isCallable()->yes(),
31093155
));
31103156
}
31113157

3112-
$callableParameters = null;
31133158
$acceptors = $passedToType->getCallableParametersAcceptors($scope);
31143159
if (count($acceptors) === 1) {
31153160
$callableParameters = $acceptors[0]->getParameters();
31163161
}
3117-
} else {
3118-
$callableParameters = null;
31193162
}
31203163

31213164
$arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Parser;
4+
5+
use PhpParser\Node;
6+
use PhpParser\NodeVisitorAbstract;
7+
use function count;
8+
9+
class ArrowFunctionArgVisitor extends NodeVisitorAbstract
10+
{
11+
12+
public function enterNode(Node $node): ?Node
13+
{
14+
if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Expr\ArrowFunction) {
15+
$args = $node->getArgs();
16+
17+
if (count($args) > 0) {
18+
$node->name->setAttribute('arrowFunctionCallArgs', $args);
19+
}
20+
}
21+
return null;
22+
}
23+
24+
}

src/Parser/ClosureArgVisitor.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Parser;
4+
5+
use PhpParser\Node;
6+
use PhpParser\NodeVisitorAbstract;
7+
use function count;
8+
9+
class ClosureArgVisitor extends NodeVisitorAbstract
10+
{
11+
12+
public function enterNode(Node $node): ?Node
13+
{
14+
if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Expr\Closure) {
15+
$args = $node->getArgs();
16+
17+
if (count($args) > 0) {
18+
$node->name->setAttribute('closureCallArgs', $args);
19+
}
20+
}
21+
return null;
22+
}
23+
24+
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,12 @@ public function dataFileAsserts(): iterable
971971
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-7469.php');
972972
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Variables/data/bug-3391.php');
973973
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6901.php');
974+
975+
if (PHP_VERSION_ID >= 70400) {
976+
yield from $this->gatherAssertTypes(__DIR__ . '/data/arrow-function-argument-type.php');
977+
}
978+
979+
yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-argument-type.php');
974980
}
975981

976982
/**
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace ArrowFunctionArgumentType;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
/**
11+
* @param array{a: int} $array
12+
*/
13+
public function doFoo(int $integer, array $array, ?string $nullableString)
14+
{
15+
(fn($context) => assertType('int', $context))($integer);
16+
17+
(fn($context) => assertType('array{a: int}', $context))($array);
18+
19+
(fn($context) => assertType('string|null', $context))($nullableString);
20+
21+
(fn($a, $b, $c) => assertType('array{int, array{a: int}, string|null}', [$a, $b, $c]))($integer, $array, $nullableString);
22+
23+
(fn($a, $b, $c = null) => assertType('array{int, array{a: int}, mixed}', [$a, $b, $c]))($integer, $array);
24+
}
25+
26+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace ClosureArgumentType;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
/**
11+
* @param array{a: int} $array
12+
*/
13+
public function doFoo(int $integer, array $array, ?string $nullableString)
14+
{
15+
(function($context) {
16+
assertType('int', $context);
17+
})($integer);
18+
19+
(function($context) {
20+
assertType('array{a: int}', $context);
21+
})($array);
22+
23+
(function($context) {
24+
assertType('string|null', $context);
25+
})($nullableString);
26+
27+
(function($context1, $context2, $context3) {
28+
assertType('int', $context1);
29+
assertType('array{a: int}', $context2);
30+
assertType('string|null', $context3);
31+
})($integer, $array, $nullableString);
32+
33+
(function($context1, $context2, $context3 = null) {
34+
assertType('int', $context1);
35+
assertType('array{a: int}', $context2);
36+
assertType('mixed', $context3);
37+
})($integer, $array);
38+
}
39+
40+
}

0 commit comments

Comments
 (0)