Skip to content

Commit ca41b7d

Browse files
committed
Resolving type of closure - get $passedToType from inFunctionCallsStack
1 parent 0159d34 commit ca41b7d

12 files changed

+174
-27
lines changed

src/Analyser/ArgumentsNormalizer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ public static function reorderNewArguments(
157157
* @param Arg[] $callArgs
158158
* @return ?array<int, Arg>
159159
*/
160-
private static function reorderArgs(ParametersAcceptor $parametersAcceptor, array $callArgs): ?array
160+
public static function reorderArgs(ParametersAcceptor $parametersAcceptor, array $callArgs): ?array
161161
{
162162
if (count($callArgs) === 0) {
163163
return [];

src/Analyser/DirectInternalScopeFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public function __construct(
4646
* @param array<string, ExpressionTypeHolder> $expressionTypes
4747
* @param array<string, ExpressionTypeHolder> $nativeExpressionTypes
4848
* @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
49-
* @param list<array{FunctionReflection|MethodReflection, ParameterReflection|null}> $inFunctionCallsStack
49+
* @param list<array{FunctionReflection|MethodReflection|null, ParameterReflection|null}> $inFunctionCallsStack
5050
* @param array<string, true> $currentlyAssignedExpressions
5151
* @param array<string, true> $currentlyAllowedUndefinedExpressions
5252
*/

src/Analyser/InternalScopeFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ interface InternalScopeFactory
1717
* @param list<string> $inClosureBindScopeClasses
1818
* @param array<string, true> $currentlyAssignedExpressions
1919
* @param array<string, true> $currentlyAllowedUndefinedExpressions
20-
* @param list<array{MethodReflection|FunctionReflection, ParameterReflection|null}> $inFunctionCallsStack
20+
* @param list<array{MethodReflection|FunctionReflection|null, ParameterReflection|null}> $inFunctionCallsStack
2121
*/
2222
public function create(
2323
ScopeContext $context,

src/Analyser/LazyInternalScopeFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public function __construct(
4242
* @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
4343
* @param array<string, true> $currentlyAssignedExpressions
4444
* @param array<string, true> $currentlyAllowedUndefinedExpressions
45-
* @param list<array{FunctionReflection|MethodReflection, ParameterReflection|null}> $inFunctionCallsStack
45+
* @param list<array{FunctionReflection|MethodReflection|null, ParameterReflection|null}> $inFunctionCallsStack
4646
*/
4747
public function create(
4848
ScopeContext $context,

src/Analyser/MutatingScope.php

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
use stdClass;
132132
use Throwable;
133133
use function abs;
134+
use function array_filter;
134135
use function array_key_exists;
135136
use function array_key_first;
136137
use function array_keys;
@@ -186,7 +187,7 @@ class MutatingScope implements Scope
186187
* @param array<string, true> $currentlyAssignedExpressions
187188
* @param array<string, true> $currentlyAllowedUndefinedExpressions
188189
* @param array<string, ExpressionTypeHolder> $nativeExpressionTypes
189-
* @param list<array{MethodReflection|FunctionReflection, ParameterReflection|null}> $inFunctionCallsStack
190+
* @param list<array{MethodReflection|FunctionReflection|null, ParameterReflection|null}> $inFunctionCallsStack
190191
*/
191192
public function __construct(
192193
private InternalScopeFactory $scopeFactory,
@@ -1233,6 +1234,14 @@ private function resolveType(string $exprString, Expr $node): Type
12331234
foreach ($arrayMapArgs as $funcCallArg) {
12341235
$callableParameters[] = new DummyParameter('item', $this->getType($funcCallArg->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null);
12351236
}
1237+
} else {
1238+
$inFunctionCallsStackCount = count($this->inFunctionCallsStack);
1239+
if ($inFunctionCallsStackCount > 0) {
1240+
[, $inParameter] = $this->inFunctionCallsStack[$inFunctionCallsStackCount - 1];
1241+
if ($inParameter !== null) {
1242+
$callableParameters = $this->nodeScopeResolver->createCallableParameters($this, $node, null, $inParameter->getType());
1243+
}
1244+
}
12361245
}
12371246

12381247
if ($node instanceof Expr\ArrowFunction) {
@@ -2590,14 +2599,14 @@ public function hasExpressionType(Expr $node): TrinaryLogic
25902599
}
25912600

25922601
/**
2593-
* @param MethodReflection|FunctionReflection $reflection
2602+
* @param MethodReflection|FunctionReflection|null $reflection
25942603
*/
25952604
public function pushInFunctionCall($reflection, ?ParameterReflection $parameter): self
25962605
{
25972606
$stack = $this->inFunctionCallsStack;
25982607
$stack[] = [$reflection, $parameter];
25992608

2600-
$scope = $this->scopeFactory->create(
2609+
return $this->scopeFactory->create(
26012610
$this->context,
26022611
$this->isDeclareStrictTypes(),
26032612
$this->getFunction(),
@@ -2615,19 +2624,14 @@ public function pushInFunctionCall($reflection, ?ParameterReflection $parameter)
26152624
$this->parentScope,
26162625
$this->nativeTypesPromoted,
26172626
);
2618-
$scope->resolvedTypes = $this->resolvedTypes;
2619-
$scope->truthyScopes = $this->truthyScopes;
2620-
$scope->falseyScopes = $this->falseyScopes;
2621-
2622-
return $scope;
26232627
}
26242628

26252629
public function popInFunctionCall(): self
26262630
{
26272631
$stack = $this->inFunctionCallsStack;
26282632
array_pop($stack);
26292633

2630-
$scope = $this->scopeFactory->create(
2634+
return $this->scopeFactory->create(
26312635
$this->context,
26322636
$this->isDeclareStrictTypes(),
26332637
$this->getFunction(),
@@ -2645,11 +2649,6 @@ public function popInFunctionCall(): self
26452649
$this->parentScope,
26462650
$this->nativeTypesPromoted,
26472651
);
2648-
$scope->resolvedTypes = $this->resolvedTypes;
2649-
$scope->truthyScopes = $this->truthyScopes;
2650-
$scope->falseyScopes = $this->falseyScopes;
2651-
2652-
return $scope;
26532652
}
26542653

26552654
/** @api */
@@ -2677,12 +2676,18 @@ public function isInClassExists(string $className): bool
26772676

26782677
public function getFunctionCallStack(): array
26792678
{
2680-
return array_map(static fn ($values) => $values[0], $this->inFunctionCallsStack);
2679+
return array_values(array_filter(
2680+
array_map(static fn ($values) => $values[0], $this->inFunctionCallsStack),
2681+
static fn (FunctionReflection|MethodReflection|null $reflection) => $reflection !== null,
2682+
));
26812683
}
26822684

26832685
public function getFunctionCallStackWithParameters(): array
26842686
{
2685-
return $this->inFunctionCallsStack;
2687+
return array_values(array_filter(
2688+
$this->inFunctionCallsStack,
2689+
static fn ($item) => $item[0] !== null,
2690+
));
26862691
}
26872692

26882693
/** @api */

src/Analyser/NodeScopeResolver.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4157,7 +4157,7 @@ private function processArrowFunctionNode(
41574157
* @param Node\Arg[] $args
41584158
* @return ParameterReflection[]|null
41594159
*/
4160-
private function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array
4160+
public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array
41614161
{
41624162
$callableParameters = null;
41634163
if ($args !== null) {

src/Reflection/ParametersAcceptorSelector.php

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use Closure;
66
use PhpParser\Node;
7+
use PHPStan\Analyser\ArgumentsNormalizer;
8+
use PHPStan\Analyser\MutatingScope;
79
use PHPStan\Analyser\Scope;
810
use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr;
911
use PHPStan\Parser\ArrayFilterArgVisitor;
@@ -336,16 +338,44 @@ public static function selectFromArgs(
336338
}
337339
}
338340

341+
$reorderedArgs = $args;
342+
$parameters = null;
343+
$singleParametersAcceptor = null;
344+
if (count($parametersAcceptors) === 1) {
345+
$reorderedArgs = ArgumentsNormalizer::reorderArgs($parametersAcceptors[0], $args);
346+
$singleParametersAcceptor = $parametersAcceptors[0];
347+
}
348+
339349
$hasName = false;
340-
foreach ($args as $i => $arg) {
341-
$type = $scope->getType($arg->value);
342-
if ($arg->name !== null) {
343-
$index = $arg->name->toString();
350+
foreach ($reorderedArgs ?? $args as $i => $arg) {
351+
$originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg;
352+
$parameter = null;
353+
if ($singleParametersAcceptor !== null) {
354+
$parameters = $singleParametersAcceptor->getParameters();
355+
if (isset($parameters[$i])) {
356+
$parameter = $parameters[$i];
357+
} elseif (count($parameters) > 0 && $singleParametersAcceptor->isVariadic()) {
358+
$parameter = $parameters[count($parameters) - 1];
359+
}
360+
}
361+
362+
if ($parameter !== null && $scope instanceof MutatingScope) {
363+
$scope = $scope->pushInFunctionCall(null, $parameter);
364+
}
365+
366+
$type = $scope->getType($originalArg->value);
367+
368+
if ($parameter !== null && $scope instanceof MutatingScope) {
369+
$scope = $scope->popInFunctionCall();
370+
}
371+
372+
if ($originalArg->name !== null) {
373+
$index = $originalArg->name->toString();
344374
$hasName = true;
345375
} else {
346376
$index = $i;
347377
}
348-
if ($arg->unpack) {
378+
if ($originalArg->unpack) {
349379
$unpack = true;
350380
$types[$index] = $type->getIterableValueType();
351381
} else {

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ public function dataFileAsserts(): iterable
9090
if (PHP_VERSION_ID >= 70400) {
9191
yield from $this->gatherAssertTypes(__DIR__ . '/data/native-types.php');
9292
yield from $this->gatherAssertTypes(__DIR__ . '/data/reflection-type.php');
93+
yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-passed-to-type.php');
9394
}
9495

9596
if (PHP_VERSION_ID >= 80100) {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\Closure;
7+
use PhpParser\Node\FunctionLike;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Rules\RuleErrorBuilder;
10+
use PHPStan\Type\VerbosityLevel;
11+
use function sprintf;
12+
13+
/**
14+
* @implements Rule<FunctionLike>
15+
*/
16+
class TestClosureTypeRule implements Rule
17+
{
18+
19+
public function getNodeType(): string
20+
{
21+
return FunctionLike::class;
22+
}
23+
24+
public function processNode(Node $node, Scope $scope): array
25+
{
26+
if (!$node instanceof Closure && !$node instanceof Node\Expr\ArrowFunction) {
27+
return [];
28+
}
29+
30+
$type = $scope->getType($node);
31+
32+
return [
33+
RuleErrorBuilder::message(sprintf('Closure type: %s', $type->describe(VerbosityLevel::precise())))
34+
->identifier('tests.closureType')
35+
->build(),
36+
];
37+
}
38+
39+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PHPStan\Rules\Rule as TRule;
6+
use PHPStan\Testing\RuleTestCase;
7+
8+
/**
9+
* @extends RuleTestCase<TestClosureTypeRule>
10+
*/
11+
class TestClosureTypeRuleTest extends RuleTestCase
12+
{
13+
14+
protected function getRule(): TRule
15+
{
16+
return new TestClosureTypeRule();
17+
}
18+
19+
public function testRule(): void
20+
{
21+
$this->analyse([__DIR__ . '/data/closure-passed-to-type.php'], [
22+
[
23+
'Closure type: Closure(mixed): (1|2|3)',
24+
25,
25+
],
26+
[
27+
'Closure type: Closure(mixed): (1|2|3)',
28+
35,
29+
],
30+
]);
31+
}
32+
33+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace ClosurePassedToType;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
/**
11+
* @template T
12+
* @template U
13+
* @param array<T> $items
14+
* @param callable(T): U $cb
15+
* @return array<U>
16+
*/
17+
public function doFoo(array $items, callable $cb)
18+
{
19+
20+
}
21+
22+
public function doBar()
23+
{
24+
$a = [1, 2, 3];
25+
$b = $this->doFoo($a, function ($item) {
26+
assertType('1|2|3', $item);
27+
return $item;
28+
});
29+
assertType('array<1|2|3>', $b);
30+
}
31+
32+
public function doBaz()
33+
{
34+
$a = [1, 2, 3];
35+
$b = $this->doFoo($a, fn ($item) => $item);
36+
assertType('array<1|2|3>', $b);
37+
}
38+
39+
}

tests/PHPStan/Analyser/data/generics.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ function testF($arrayOfInt, $callableOrNull)
159159
assertType('array<string>', f($arrayOfInt, function ($a): string {
160160
return (string)$a;
161161
}));
162-
assertType('array', f($arrayOfInt, function ($a) {
162+
assertType('array<int>', f($arrayOfInt, function ($a) {
163163
return $a;
164164
}));
165165
assertType('array<string>', f($arrayOfInt, $callableOrNull));

0 commit comments

Comments
 (0)