From 366e33043161368a20e8829ec9e6e72000f578e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9on=20Gersen?= Date: Tue, 16 Aug 2022 17:21:22 +0200 Subject: [PATCH] Use argument types as parameter types for inline closures (#7798) --- conf/config.neon | 10 ++++ src/Analyser/NodeScopeResolver.php | 59 ++++++++++++++++--- src/Parser/ArrowFunctionArgVisitor.php | 24 ++++++++ src/Parser/ClosureArgVisitor.php | 24 ++++++++ .../Analyser/NodeScopeResolverTest.php | 6 ++ .../data/arrow-function-argument-type.php | 26 ++++++++ .../Analyser/data/closure-argument-type.php | 40 +++++++++++++ 7 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 src/Parser/ArrowFunctionArgVisitor.php create mode 100644 src/Parser/ClosureArgVisitor.php create mode 100644 tests/PHPStan/Analyser/data/arrow-function-argument-type.php create mode 100644 tests/PHPStan/Analyser/data/closure-argument-type.php diff --git a/conf/config.neon b/conf/config.neon index 0f54599481..9550e013c5 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -417,6 +417,16 @@ services: tags: - phpstan.parser.richParserNodeVisitor + - + class: PHPStan\Parser\ClosureArgVisitor + tags: + - phpstan.parser.richParserNodeVisitor + + - + class: PHPStan\Parser\ArrowFunctionArgVisitor + tags: + - phpstan.parser.richParserNodeVisitor + - class: PHPStan\Parser\NewAssignedToPropertyVisitor tags: diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index dce4506afd..1e7941bd5c 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -108,6 +108,7 @@ use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\Native\NativeMethodReflection; +use PHPStan\Reflection\Native\NativeParameterReflection; use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\Php\PhpMethodReflection; @@ -2960,7 +2961,31 @@ private function processClosureNode( $byRefUses = []; - if ($passedToType !== null && !$passedToType->isCallable()->no()) { + $callableParameters = null; + $closureCallArgs = $expr->getAttribute('closureCallArgs'); + + if ($closureCallArgs !== null) { + $acceptors = $scope->getType($expr)->getCallableParametersAcceptors($scope); + if (count($acceptors) === 1) { + $callableParameters = $acceptors[0]->getParameters(); + + foreach ($callableParameters as $index => $callableParameter) { + if (!isset($closureCallArgs[$index])) { + continue; + } + + $type = $scope->getType($closureCallArgs[$index]->value); + $callableParameters[$index] = new NativeParameterReflection( + $callableParameter->getName(), + $callableParameter->isOptional(), + $type, + $callableParameter->passedByReference(), + $callableParameter->isVariadic(), + $callableParameter->getDefaultValue(), + ); + } + } + } elseif ($passedToType !== null && !$passedToType->isCallable()->no()) { if ($passedToType instanceof UnionType) { $passedToType = TypeCombinator::union(...array_filter( $passedToType->getTypes(), @@ -2968,13 +2993,10 @@ private function processClosureNode( )); } - $callableParameters = null; $acceptors = $passedToType->getCallableParametersAcceptors($scope); if (count($acceptors) === 1) { $callableParameters = $acceptors[0]->getParameters(); } - } else { - $callableParameters = null; } $useScope = $scope; @@ -3101,7 +3123,31 @@ private function processArrowFunctionNode( $nodeCallback($expr->returnType, $scope); } - if ($passedToType !== null && !$passedToType->isCallable()->no()) { + $callableParameters = null; + $arrowFunctionCallArgs = $expr->getAttribute('arrowFunctionCallArgs'); + + if ($arrowFunctionCallArgs !== null) { + $acceptors = $scope->getType($expr)->getCallableParametersAcceptors($scope); + if (count($acceptors) === 1) { + $callableParameters = $acceptors[0]->getParameters(); + + foreach ($callableParameters as $index => $callableParameter) { + if (!isset($arrowFunctionCallArgs[$index])) { + continue; + } + + $type = $scope->getType($arrowFunctionCallArgs[$index]->value); + $callableParameters[$index] = new NativeParameterReflection( + $callableParameter->getName(), + $callableParameter->isOptional(), + $type, + $callableParameter->passedByReference(), + $callableParameter->isVariadic(), + $callableParameter->getDefaultValue(), + ); + } + } + } elseif ($passedToType !== null && !$passedToType->isCallable()->no()) { if ($passedToType instanceof UnionType) { $passedToType = TypeCombinator::union(...array_filter( $passedToType->getTypes(), @@ -3109,13 +3155,10 @@ private function processArrowFunctionNode( )); } - $callableParameters = null; $acceptors = $passedToType->getCallableParametersAcceptors($scope); if (count($acceptors) === 1) { $callableParameters = $acceptors[0]->getParameters(); } - } else { - $callableParameters = null; } $arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters); diff --git a/src/Parser/ArrowFunctionArgVisitor.php b/src/Parser/ArrowFunctionArgVisitor.php new file mode 100644 index 0000000000..c04bc64781 --- /dev/null +++ b/src/Parser/ArrowFunctionArgVisitor.php @@ -0,0 +1,24 @@ +name instanceof Node\Expr\ArrowFunction) { + $args = $node->getArgs(); + + if (count($args) > 0) { + $node->name->setAttribute('arrowFunctionCallArgs', $args); + } + } + return null; + } + +} diff --git a/src/Parser/ClosureArgVisitor.php b/src/Parser/ClosureArgVisitor.php new file mode 100644 index 0000000000..c873a96992 --- /dev/null +++ b/src/Parser/ClosureArgVisitor.php @@ -0,0 +1,24 @@ +name instanceof Node\Expr\Closure) { + $args = $node->getArgs(); + + if (count($args) > 0) { + $node->name->setAttribute('closureCallArgs', $args); + } + } + return null; + } + +} diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 0ba226227e..bebe21244e 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -971,6 +971,12 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-7469.php'); yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Variables/data/bug-3391.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6901.php'); + + if (PHP_VERSION_ID >= 70400) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/arrow-function-argument-type.php'); + } + + yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-argument-type.php'); } /** diff --git a/tests/PHPStan/Analyser/data/arrow-function-argument-type.php b/tests/PHPStan/Analyser/data/arrow-function-argument-type.php new file mode 100644 index 0000000000..3e1448e6ff --- /dev/null +++ b/tests/PHPStan/Analyser/data/arrow-function-argument-type.php @@ -0,0 +1,26 @@ + assertType('int', $context))($integer); + + (fn($context) => assertType('array{a: int}', $context))($array); + + (fn($context) => assertType('string|null', $context))($nullableString); + + (fn($a, $b, $c) => assertType('array{int, array{a: int}, string|null}', [$a, $b, $c]))($integer, $array, $nullableString); + + (fn($a, $b, $c = null) => assertType('array{int, array{a: int}, mixed}', [$a, $b, $c]))($integer, $array); + } + +} diff --git a/tests/PHPStan/Analyser/data/closure-argument-type.php b/tests/PHPStan/Analyser/data/closure-argument-type.php new file mode 100644 index 0000000000..6fd537211d --- /dev/null +++ b/tests/PHPStan/Analyser/data/closure-argument-type.php @@ -0,0 +1,40 @@ +