diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index e24dc605a8..6a9b163271 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1281,7 +1281,7 @@ public function getKeepVoidType(Expr $node): Type return $this->getType($clonedNode); } - public function doNotTreatPhpDocTypesAsCertain(): Scope + public function doNotTreatPhpDocTypesAsCertain(): self { return $this->promoteNativeTypes(); } diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index d0ad511457..49904b6451 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3691,6 +3691,14 @@ public function processArgs( if ($overwritingParameterType !== null) { $parameterType = $overwritingParameterType; + + // resolve the native flavour through the same extension on the + // natively-promoted scope, so the closure parameters keep + // their native precision too + $overwritingParameterNativeType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass->doNotTreatPhpDocTypesAsCertain()); + if ($overwritingParameterNativeType !== null) { + $parameterNativeType = $overwritingParameterNativeType; + } } } @@ -3766,6 +3774,14 @@ public function processArgs( if ($overwritingParameterType !== null) { $parameterType = $overwritingParameterType; + + // resolve the native flavour through the same extension on the + // natively-promoted scope, so the closure parameters keep + // their native precision too + $overwritingParameterNativeType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass->doNotTreatPhpDocTypesAsCertain()); + if ($overwritingParameterNativeType !== null) { + $parameterNativeType = $overwritingParameterNativeType; + } } } diff --git a/src/Type/Php/ArrayFilterParameterClosureTypeExtension.php b/src/Type/Php/ArrayFilterParameterClosureTypeExtension.php new file mode 100644 index 0000000000..cbad3124f8 --- /dev/null +++ b/src/Type/Php/ArrayFilterParameterClosureTypeExtension.php @@ -0,0 +1,62 @@ +getName() === 'array_filter' && $parameter->getName() === 'callback'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type + { + $args = $functionCall->getArgs(); + if (!isset($args[0])) { + return null; + } + + $arrayType = $scope->getType($args[0]->value); + $parameters = null; + if (isset($args[2])) { + $mode = $scope->getType($args[2]->value); + if ($mode instanceof ConstantIntegerType) { + if ($mode->getValue() === ARRAY_FILTER_USE_KEY) { + $parameters = [$this->createParameter('key', $scope->getIterableKeyType($arrayType))]; + } elseif ($mode->getValue() === ARRAY_FILTER_USE_BOTH) { + $parameters = [ + $this->createParameter('item', $scope->getIterableValueType($arrayType)), + $this->createParameter('key', $scope->getIterableKeyType($arrayType)), + ]; + } + } + } + + $parameters ??= [$this->createParameter('item', $scope->getIterableValueType($arrayType))]; + + return new ClosureType($parameters, new BooleanType()); + } + + private function createParameter(string $name, Type $type): NativeParameterReflection + { + return new NativeParameterReflection($name, false, $type, PassedByReference::createNo(), false, null); + } + +} diff --git a/src/Type/Php/ArrayFindParameterClosureTypeExtension.php b/src/Type/Php/ArrayFindParameterClosureTypeExtension.php new file mode 100644 index 0000000000..e99a0e8736 --- /dev/null +++ b/src/Type/Php/ArrayFindParameterClosureTypeExtension.php @@ -0,0 +1,43 @@ +getName(), ['array_find', 'array_find_key', 'array_any', 'array_all'], true) + && $parameter->getName() === 'callback'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type + { + $args = $functionCall->getArgs(); + if (!isset($args[0])) { + return null; + } + + $arrayType = $scope->getType($args[0]->value); + + return new ClosureType([ + new NativeParameterReflection('value', false, $scope->getIterableValueType($arrayType), PassedByReference::createNo(), false, null), + new NativeParameterReflection('key', false, $scope->getIterableKeyType($arrayType), PassedByReference::createNo(), false, null), + ], new BooleanType()); + } + +} diff --git a/src/Type/Php/ArrayMapParameterClosureTypeExtension.php b/src/Type/Php/ArrayMapParameterClosureTypeExtension.php new file mode 100644 index 0000000000..6ae1c8198a --- /dev/null +++ b/src/Type/Php/ArrayMapParameterClosureTypeExtension.php @@ -0,0 +1,62 @@ +getName() === 'array_map' && $parameter->getName() === 'callback'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type + { + $args = $functionCall->getArgs(); + if (count($args) < 2) { + return null; + } + + $callbackParameters = []; + $argCount = count($args); + for ($i = 1; $i < $argCount; $i++) { + $arg = $args[$i]; + $arrayType = $scope->getType($arg->value); + if ($arg->unpack) { + $constantArrays = $arrayType->getConstantArrays(); + if (count($constantArrays) === 0) { + return null; + } + foreach ($constantArrays as $constantArray) { + foreach ($constantArray->getValueTypes() as $valueType) { + $callbackParameters[] = $this->createParameter($scope->getIterableValueType($valueType)); + } + } + } else { + $callbackParameters[] = $this->createParameter($scope->getIterableValueType($arrayType)); + } + } + + return new ClosureType($callbackParameters, new MixedType()); + } + + private function createParameter(Type $type): NativeParameterReflection + { + return new NativeParameterReflection('item', false, $type, PassedByReference::createNo(), false, null); + } + +} diff --git a/src/Type/Php/ArrayWalkParameterClosureTypeExtension.php b/src/Type/Php/ArrayWalkParameterClosureTypeExtension.php new file mode 100644 index 0000000000..4ddf25ab94 --- /dev/null +++ b/src/Type/Php/ArrayWalkParameterClosureTypeExtension.php @@ -0,0 +1,45 @@ +getName() === 'array_walk' && $parameter->getName() === 'callback'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type + { + $args = $functionCall->getArgs(); + if (!isset($args[0])) { + return null; + } + + $arrayType = $scope->getType($args[0]->value); + $parameters = [ + new NativeParameterReflection('item', false, $scope->getIterableValueType($arrayType), PassedByReference::createReadsArgument(), false, null), + new NativeParameterReflection('key', false, $scope->getIterableKeyType($arrayType), PassedByReference::createNo(), false, null), + ]; + if (isset($args[2])) { + $parameters[] = new NativeParameterReflection('arg', false, $scope->getType($args[2]->value), PassedByReference::createNo(), false, null); + } + + return new ClosureType($parameters, new MixedType()); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php index b9aa8f5482..9f8c608d47 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_replace_callback_shapes.php @@ -63,7 +63,7 @@ function bug14904(string $string): void { '/a|(?b)/', function (array $match): string { assertType("array{0: non-empty-string, b: 'b'|null, 1: 'b'|null}", $match); - assertNativeType('array', $match); + assertNativeType("array{0: non-empty-string, b: 'b'|null, 1: 'b'|null}", $match); return ''; }, $string,