diff --git a/src/Analyser/ArgumentsNormalizer.php b/src/Analyser/ArgumentsNormalizer.php index 98deefabe1..6c6db632ac 100644 --- a/src/Analyser/ArgumentsNormalizer.php +++ b/src/Analyser/ArgumentsNormalizer.php @@ -157,7 +157,7 @@ public static function reorderNewArguments( * @param Arg[] $callArgs * @return ?array */ - private static function reorderArgs(ParametersAcceptor $parametersAcceptor, array $callArgs): ?array + public static function reorderArgs(ParametersAcceptor $parametersAcceptor, array $callArgs): ?array { if (count($callArgs) === 0) { return []; diff --git a/src/Analyser/DirectInternalScopeFactory.php b/src/Analyser/DirectInternalScopeFactory.php index 07d7fbe09c..c662e1d9a0 100644 --- a/src/Analyser/DirectInternalScopeFactory.php +++ b/src/Analyser/DirectInternalScopeFactory.php @@ -46,7 +46,7 @@ public function __construct( * @param array $expressionTypes * @param array $nativeExpressionTypes * @param array $conditionalExpressions - * @param list $inFunctionCallsStack + * @param list $inFunctionCallsStack * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions */ diff --git a/src/Analyser/InternalScopeFactory.php b/src/Analyser/InternalScopeFactory.php index 7d3414f297..fdacebc9ec 100644 --- a/src/Analyser/InternalScopeFactory.php +++ b/src/Analyser/InternalScopeFactory.php @@ -17,7 +17,7 @@ interface InternalScopeFactory * @param list $inClosureBindScopeClasses * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions - * @param list $inFunctionCallsStack + * @param list $inFunctionCallsStack */ public function create( ScopeContext $context, diff --git a/src/Analyser/LazyInternalScopeFactory.php b/src/Analyser/LazyInternalScopeFactory.php index 0d74666e85..028600646c 100644 --- a/src/Analyser/LazyInternalScopeFactory.php +++ b/src/Analyser/LazyInternalScopeFactory.php @@ -42,7 +42,7 @@ public function __construct( * @param array $conditionalExpressions * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions - * @param list $inFunctionCallsStack + * @param list $inFunctionCallsStack */ public function create( ScopeContext $context, diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 6c707dbb52..c5ef1f4f3f 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -131,6 +131,7 @@ use stdClass; use Throwable; use function abs; +use function array_filter; use function array_key_exists; use function array_key_first; use function array_keys; @@ -186,7 +187,7 @@ class MutatingScope implements Scope * @param array $currentlyAssignedExpressions * @param array $currentlyAllowedUndefinedExpressions * @param array $nativeExpressionTypes - * @param list $inFunctionCallsStack + * @param list $inFunctionCallsStack */ public function __construct( private InternalScopeFactory $scopeFactory, @@ -1233,6 +1234,14 @@ private function resolveType(string $exprString, Expr $node): Type foreach ($arrayMapArgs as $funcCallArg) { $callableParameters[] = new DummyParameter('item', $this->getType($funcCallArg->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null); } + } else { + $inFunctionCallsStackCount = count($this->inFunctionCallsStack); + if ($inFunctionCallsStackCount > 0) { + [, $inParameter] = $this->inFunctionCallsStack[$inFunctionCallsStackCount - 1]; + if ($inParameter !== null) { + $callableParameters = $this->nodeScopeResolver->createCallableParameters($this, $node, null, $inParameter->getType()); + } + } } if ($node instanceof Expr\ArrowFunction) { @@ -2590,14 +2599,14 @@ public function hasExpressionType(Expr $node): TrinaryLogic } /** - * @param MethodReflection|FunctionReflection $reflection + * @param MethodReflection|FunctionReflection|null $reflection */ public function pushInFunctionCall($reflection, ?ParameterReflection $parameter): self { $stack = $this->inFunctionCallsStack; $stack[] = [$reflection, $parameter]; - $scope = $this->scopeFactory->create( + return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), $this->getFunction(), @@ -2615,11 +2624,6 @@ public function pushInFunctionCall($reflection, ?ParameterReflection $parameter) $this->parentScope, $this->nativeTypesPromoted, ); - $scope->resolvedTypes = $this->resolvedTypes; - $scope->truthyScopes = $this->truthyScopes; - $scope->falseyScopes = $this->falseyScopes; - - return $scope; } public function popInFunctionCall(): self @@ -2627,7 +2631,7 @@ public function popInFunctionCall(): self $stack = $this->inFunctionCallsStack; array_pop($stack); - $scope = $this->scopeFactory->create( + return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), $this->getFunction(), @@ -2645,11 +2649,6 @@ public function popInFunctionCall(): self $this->parentScope, $this->nativeTypesPromoted, ); - $scope->resolvedTypes = $this->resolvedTypes; - $scope->truthyScopes = $this->truthyScopes; - $scope->falseyScopes = $this->falseyScopes; - - return $scope; } /** @api */ @@ -2677,12 +2676,18 @@ public function isInClassExists(string $className): bool public function getFunctionCallStack(): array { - return array_map(static fn ($values) => $values[0], $this->inFunctionCallsStack); + return array_values(array_filter( + array_map(static fn ($values) => $values[0], $this->inFunctionCallsStack), + static fn (FunctionReflection|MethodReflection|null $reflection) => $reflection !== null, + )); } public function getFunctionCallStackWithParameters(): array { - return $this->inFunctionCallsStack; + return array_values(array_filter( + $this->inFunctionCallsStack, + static fn ($item) => $item[0] !== null, + )); } /** @api */ diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 6ce976569f..b0ab0ae213 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -4157,7 +4157,7 @@ private function processArrowFunctionNode( * @param Node\Arg[] $args * @return ParameterReflection[]|null */ - private function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array + public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array { $callableParameters = null; if ($args !== null) { diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index c6f597313a..04b55513ee 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -4,6 +4,8 @@ use Closure; use PhpParser\Node; +use PHPStan\Analyser\ArgumentsNormalizer; +use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; use PHPStan\Parser\ArrayFilterArgVisitor; @@ -336,16 +338,44 @@ public static function selectFromArgs( } } + $reorderedArgs = $args; + $parameters = null; + $singleParametersAcceptor = null; + if (count($parametersAcceptors) === 1) { + $reorderedArgs = ArgumentsNormalizer::reorderArgs($parametersAcceptors[0], $args); + $singleParametersAcceptor = $parametersAcceptors[0]; + } + $hasName = false; - foreach ($args as $i => $arg) { - $type = $scope->getType($arg->value); - if ($arg->name !== null) { - $index = $arg->name->toString(); + foreach ($reorderedArgs ?? $args as $i => $arg) { + $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; + $parameter = null; + if ($singleParametersAcceptor !== null) { + $parameters = $singleParametersAcceptor->getParameters(); + if (isset($parameters[$i])) { + $parameter = $parameters[$i]; + } elseif (count($parameters) > 0 && $singleParametersAcceptor->isVariadic()) { + $parameter = $parameters[count($parameters) - 1]; + } + } + + if ($parameter !== null && $scope instanceof MutatingScope) { + $scope = $scope->pushInFunctionCall(null, $parameter); + } + + $type = $scope->getType($originalArg->value); + + if ($parameter !== null && $scope instanceof MutatingScope) { + $scope = $scope->popInFunctionCall(); + } + + if ($originalArg->name !== null) { + $index = $originalArg->name->toString(); $hasName = true; } else { $index = $i; } - if ($arg->unpack) { + if ($originalArg->unpack) { $unpack = true; $types[$index] = $type->getIterableValueType(); } else { diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index d767e310de..dabbc6dca5 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -90,6 +90,7 @@ public function dataFileAsserts(): iterable if (PHP_VERSION_ID >= 70400) { yield from $this->gatherAssertTypes(__DIR__ . '/data/native-types.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/reflection-type.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-passed-to-type.php'); } if (PHP_VERSION_ID >= 80100) { diff --git a/tests/PHPStan/Analyser/TestClosureTypeRule.php b/tests/PHPStan/Analyser/TestClosureTypeRule.php new file mode 100644 index 0000000000..9d3128b1c7 --- /dev/null +++ b/tests/PHPStan/Analyser/TestClosureTypeRule.php @@ -0,0 +1,39 @@ + + */ +class TestClosureTypeRule implements Rule +{ + + public function getNodeType(): string + { + return FunctionLike::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node instanceof Closure && !$node instanceof Node\Expr\ArrowFunction) { + return []; + } + + $type = $scope->getType($node); + + return [ + RuleErrorBuilder::message(sprintf('Closure type: %s', $type->describe(VerbosityLevel::precise()))) + ->identifier('tests.closureType') + ->build(), + ]; + } + +} diff --git a/tests/PHPStan/Analyser/TestClosureTypeRuleTest.php b/tests/PHPStan/Analyser/TestClosureTypeRuleTest.php new file mode 100644 index 0000000000..0172b7a8ae --- /dev/null +++ b/tests/PHPStan/Analyser/TestClosureTypeRuleTest.php @@ -0,0 +1,33 @@ + + */ +class TestClosureTypeRuleTest extends RuleTestCase +{ + + protected function getRule(): TRule + { + return new TestClosureTypeRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/closure-passed-to-type.php'], [ + [ + 'Closure type: Closure(mixed): (1|2|3)', + 25, + ], + [ + 'Closure type: Closure(mixed): (1|2|3)', + 35, + ], + ]); + } + +} diff --git a/tests/PHPStan/Analyser/data/closure-passed-to-type.php b/tests/PHPStan/Analyser/data/closure-passed-to-type.php new file mode 100644 index 0000000000..a34ded1591 --- /dev/null +++ b/tests/PHPStan/Analyser/data/closure-passed-to-type.php @@ -0,0 +1,39 @@ + $items + * @param callable(T): U $cb + * @return array + */ + public function doFoo(array $items, callable $cb) + { + + } + + public function doBar() + { + $a = [1, 2, 3]; + $b = $this->doFoo($a, function ($item) { + assertType('1|2|3', $item); + return $item; + }); + assertType('array<1|2|3>', $b); + } + + public function doBaz() + { + $a = [1, 2, 3]; + $b = $this->doFoo($a, fn ($item) => $item); + assertType('array<1|2|3>', $b); + } + +} diff --git a/tests/PHPStan/Analyser/data/generics.php b/tests/PHPStan/Analyser/data/generics.php index b7731b35b9..bcd7ecf616 100644 --- a/tests/PHPStan/Analyser/data/generics.php +++ b/tests/PHPStan/Analyser/data/generics.php @@ -159,7 +159,7 @@ function testF($arrayOfInt, $callableOrNull) assertType('array', f($arrayOfInt, function ($a): string { return (string)$a; })); - assertType('array', f($arrayOfInt, function ($a) { + assertType('array', f($arrayOfInt, function ($a) { return $a; })); assertType('array', f($arrayOfInt, $callableOrNull));