From be71b31f4762902731e1256be9417c85bad381fd Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Tue, 1 Oct 2024 01:35:32 +0900 Subject: [PATCH 1/9] Add ArrayFindFunctionReturnTypeExtension and ArrayFindKeyFunctionReturnTypeExtension --- conf/config.neon | 10 +++++ .../ArrayFindFunctionReturnTypeExtension.php | 36 ++++++++++++++++ ...rrayFindKeyFunctionReturnTypeExtension.php | 36 ++++++++++++++++ .../PHPStan/Analyser/nsrt/array-find-key.php | 43 +++++++++++++++++++ tests/PHPStan/Analyser/nsrt/array-find.php | 43 +++++++++++++++++++ 5 files changed, 168 insertions(+) create mode 100644 src/Type/Php/ArrayFindFunctionReturnTypeExtension.php create mode 100644 src/Type/Php/ArrayFindKeyFunctionReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/array-find-key.php create mode 100644 tests/PHPStan/Analyser/nsrt/array-find.php diff --git a/conf/config.neon b/conf/config.neon index f9d8ca018e..1f023713f3 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1220,6 +1220,16 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\ArrayFindFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + - + class: PHPStan\Type\Php\ArrayFindKeyFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\ArrayKeyDynamicReturnTypeExtension tags: diff --git a/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..dcb9800b47 --- /dev/null +++ b/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php @@ -0,0 +1,36 @@ +getName() === 'array_find'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 2) { + return null; + } + + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if (count($arrayType->getArrays()) < 1) { + return null; + } + + return TypeCombinator::union($arrayType->getIterableValueType(), new NullType()); + } + +} diff --git a/src/Type/Php/ArrayFindKeyFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFindKeyFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..97c514f427 --- /dev/null +++ b/src/Type/Php/ArrayFindKeyFunctionReturnTypeExtension.php @@ -0,0 +1,36 @@ +getName() === 'array_find_key'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 2) { + return null; + } + + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + if (count($arrayType->getArrays()) < 1) { + return null; + } + + return TypeCombinator::union($arrayType->getIterableKeyType(), new NullType()); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-find-key.php b/tests/PHPStan/Analyser/nsrt/array-find-key.php new file mode 100644 index 0000000000..9af716a850 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-find-key.php @@ -0,0 +1,43 @@ + $value) { + if ($callback($value, $key)) { + return $key; + } + } + + return null; + } + } + +} + +namespace ArrayFindKey +{ + + use function PHPStan\Testing\assertType; + + /** + * @param array $array + */ + function testMixed(array $array, callable $callback): void + { + assertType('int|string|null', array_find_key($array, $callback)); + assertType('int|string|null', array_find_key($array, 'is_int')); + } + + /** + * @param array{1, 'foo', \DateTime} $array + */ + function testConstant(array $array, callable $callback): void + { + assertType("0|1|2|null", array_find_key($array, $callback)); + assertType("0|1|2|null", array_find_key($array, 'is_int')); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/array-find.php b/tests/PHPStan/Analyser/nsrt/array-find.php new file mode 100644 index 0000000000..55bbc570be --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/array-find.php @@ -0,0 +1,43 @@ + $value) { + if ($callback($value, $key)) { + return $value; + } + } + + return null; + } + } + +} + +namespace ArrayFind +{ + + use function PHPStan\Testing\assertType; + + /** + * @param array $array + */ + function testMixed(array $array, callable $callback): void + { + assertType('mixed', array_find($array, $callback)); + assertType('mixed', array_find($array, 'is_int')); + } + + /** + * @param array{1, 'foo', \DateTime} $array + */ + function testConstant(array $array, callable $callback): void + { + assertType("1|'foo'|DateTime|null", array_find($array, $callback)); + assertType("1|'foo'|DateTime|null", array_find($array, 'is_int')); + } + +} From b18c7e09a61736e6ac6a8eb445bbcaf16018d910 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Tue, 1 Oct 2024 02:46:08 +0900 Subject: [PATCH 2/9] Make ArrayFindFunctionReturnTypeExtension a wrapper for array_filter() --- src/Type/Php/ArrayFindFunctionReturnTypeExtension.php | 6 +++++- tests/PHPStan/Analyser/nsrt/array-find.php | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php index dcb9800b47..7feed8eded 100644 --- a/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php @@ -3,12 +3,14 @@ namespace PHPStan\Type\Php; use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function array_map; use function count; final class ArrayFindFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -30,7 +32,9 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return null; } - return TypeCombinator::union($arrayType->getIterableValueType(), new NullType()); + $resultTypes = $scope->getType(new FuncCall(new Name('\array_filter'), $functionCall->getArgs()))->getArrays(); + + return TypeCombinator::union(new NullType(), ...array_map(fn ($type) => $type->getIterableValueType(), $resultTypes)); } } diff --git a/tests/PHPStan/Analyser/nsrt/array-find.php b/tests/PHPStan/Analyser/nsrt/array-find.php index 55bbc570be..969d8acf08 100644 --- a/tests/PHPStan/Analyser/nsrt/array-find.php +++ b/tests/PHPStan/Analyser/nsrt/array-find.php @@ -28,7 +28,7 @@ function array_find(array $array, callable $callback) function testMixed(array $array, callable $callback): void { assertType('mixed', array_find($array, $callback)); - assertType('mixed', array_find($array, 'is_int')); + assertType('int|null', array_find($array, 'is_int')); } /** @@ -37,7 +37,7 @@ function testMixed(array $array, callable $callback): void function testConstant(array $array, callable $callback): void { assertType("1|'foo'|DateTime|null", array_find($array, $callback)); - assertType("1|'foo'|DateTime|null", array_find($array, 'is_int')); + assertType("1|null", array_find($array, 'is_int')); } } From 0aea23f427f60a4e734c0e26a9c53f442a8ffcc7 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Tue, 1 Oct 2024 19:34:18 +0900 Subject: [PATCH 3/9] Convert non-empty-arrays resulting from array_filter() to non-nullable --- .../ArrayFindFunctionReturnTypeExtension.php | 5 +++-- tests/PHPStan/Analyser/nsrt/array-find.php | 20 +++++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php index 7feed8eded..904aa16ab5 100644 --- a/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php @@ -32,9 +32,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return null; } - $resultTypes = $scope->getType(new FuncCall(new Name('\array_filter'), $functionCall->getArgs()))->getArrays(); + $resultTypes = $scope->getType(new FuncCall(new Name('\array_filter'), $functionCall->getArgs())); + $resultType = TypeCombinator::union(...array_map(fn ($type) => $type->getIterableValueType(), $resultTypes->getArrays())); - return TypeCombinator::union(new NullType(), ...array_map(fn ($type) => $type->getIterableValueType(), $resultTypes)); + return $resultTypes->isIterableAtLeastOnce()->yes() ? $resultType : TypeCombinator::addNull($resultType); } } diff --git a/tests/PHPStan/Analyser/nsrt/array-find.php b/tests/PHPStan/Analyser/nsrt/array-find.php index 969d8acf08..d4e8e10d47 100644 --- a/tests/PHPStan/Analyser/nsrt/array-find.php +++ b/tests/PHPStan/Analyser/nsrt/array-find.php @@ -24,11 +24,14 @@ function array_find(array $array, callable $callback) /** * @param array $array + * @param non-empty-array $non_empty_array */ - function testMixed(array $array, callable $callback): void + function testMixed(array $array, array $non_empty_array, callable $callback): void { assertType('mixed', array_find($array, $callback)); assertType('int|null', array_find($array, 'is_int')); + assertType('mixed', array_find($non_empty_array, $callback)); + assertType('int|null', array_find($non_empty_array, 'is_int')); } /** @@ -37,7 +40,20 @@ function testMixed(array $array, callable $callback): void function testConstant(array $array, callable $callback): void { assertType("1|'foo'|DateTime|null", array_find($array, $callback)); - assertType("1|null", array_find($array, 'is_int')); + assertType('1', array_find($array, 'is_int')); + } + + /** + * @param array $array + * @param non-empty-array $non_empty_array + */ + function testInt(array $array, array $non_empty_array, callable $callback): void + { + assertType('int|null', array_find($array, $callback)); + assertType('int|null', array_find($array, 'is_int')); + assertType('int|null', array_find($non_empty_array, $callback)); + // should be 'int' + assertType('int|null', array_find($non_empty_array, 'is_int')); } } From cbacc8aaea3ad7859bd5f3c4bd9f476caa2d976e Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sat, 2 Nov 2024 16:08:45 +0900 Subject: [PATCH 4/9] Add ArrayFindArgVisitor --- conf/config.neon | 5 ++ src/Parser/ArrayFindArgVisitor.php | 28 ++++++++ src/Reflection/ParametersAcceptorSelector.php | 65 +++++++++++++++++++ .../PHPStan/Analyser/nsrt/array-find-key.php | 21 +++++- tests/PHPStan/Analyser/nsrt/array-find.php | 22 ++++++- 5 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 src/Parser/ArrayFindArgVisitor.php diff --git a/conf/config.neon b/conf/config.neon index 1f023713f3..11e845136d 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -325,6 +325,11 @@ services: tags: - phpstan.parser.richParserNodeVisitor + - + class: PHPStan\Parser\ArrayFindArgVisitor + tags: + - phpstan.parser.richParserNodeVisitor + - class: PHPStan\Parser\ArrayMapArgVisitor tags: diff --git a/src/Parser/ArrayFindArgVisitor.php b/src/Parser/ArrayFindArgVisitor.php new file mode 100644 index 0000000000..8b25b36491 --- /dev/null +++ b/src/Parser/ArrayFindArgVisitor.php @@ -0,0 +1,28 @@ +name instanceof Node\Name) { + $functionName = $node->name->toLowerString(); + if (in_array($functionName, ['array_find', 'array_find_key'], true)) { + $args = $node->getRawArgs(); + if (isset($args[0])) { + $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); + } + } + } + return null; + } + +} diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 619ee2aa81..e50ef2df4c 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -9,6 +9,7 @@ use PHPStan\Analyser\Scope; use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; use PHPStan\Parser\ArrayFilterArgVisitor; +use PHPStan\Parser\ArrayFindArgVisitor; use PHPStan\Parser\ArrayMapArgVisitor; use PHPStan\Parser\ArrayWalkArgVisitor; use PHPStan\Parser\ClosureBindArgVisitor; @@ -257,6 +258,70 @@ public static function selectFromArgs( ]; } + if (isset($args[0]) && (bool) $args[0]->getAttribute(ArrayFindArgVisitor::ATTRIBUTE_NAME)) { + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + $parameters[1] = new NativeParameterReflection( + $parameters[1]->getName(), + $parameters[1]->isOptional(), + new UnionType([ + new CallableType( + [ + new DummyParameter('value', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + ], + new BooleanType(), + false, + ), + new NullType(), + ]), + $parameters[1]->passedByReference(), + $parameters[1]->isVariadic(), + $parameters[1]->getDefaultValue(), + ); + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + $parameters, + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + + if (isset($args[0]) && (bool) $args[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) { + $arrayWalkParameters = [ + new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createReadsArgument(), false, null), + new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + ]; + if (isset($args[2])) { + $arrayWalkParameters[] = new DummyParameter('arg', $scope->getType($args[2]->value), false, PassedByReference::createNo(), false, null); + } + + $acceptor = $parametersAcceptors[0]; + $parameters = $acceptor->getParameters(); + $parameters[1] = new NativeParameterReflection( + $parameters[1]->getName(), + $parameters[1]->isOptional(), + new CallableType($arrayWalkParameters, new MixedType(), false), + $parameters[1]->passedByReference(), + $parameters[1]->isVariadic(), + $parameters[1]->getDefaultValue(), + ); + $parametersAcceptors = [ + new FunctionVariant( + $acceptor->getTemplateTypeMap(), + $acceptor->getResolvedTemplateTypeMap(), + $parameters, + $acceptor->isVariadic(), + $acceptor->getReturnType(), + $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), + ), + ]; + } + if (isset($args[0])) { $closureBindToVar = $args[0]->getAttribute(ClosureBindToVarVisitor::ATTRIBUTE_NAME); if ( diff --git a/tests/PHPStan/Analyser/nsrt/array-find-key.php b/tests/PHPStan/Analyser/nsrt/array-find-key.php index 9af716a850..5caf828f53 100644 --- a/tests/PHPStan/Analyser/nsrt/array-find-key.php +++ b/tests/PHPStan/Analyser/nsrt/array-find-key.php @@ -3,10 +3,15 @@ namespace { if (!function_exists('array_find_key')) { + /** + * @param array $array + * @param callable(mixed, array-key=): mixed $callback + * @return ?array-key + */ function array_find_key(array $array, callable $callback) { foreach ($array as $key => $value) { - if ($callback($value, $key)) { + if ($callback($value, $key)) { // @phpstan-ignore if.condNotBoolean return $key; } } @@ -24,6 +29,7 @@ function array_find_key(array $array, callable $callback) /** * @param array $array + * @phpstan-ignore missingType.callable */ function testMixed(array $array, callable $callback): void { @@ -33,6 +39,7 @@ function testMixed(array $array, callable $callback): void /** * @param array{1, 'foo', \DateTime} $array + * @phpstan-ignore missingType.callable */ function testConstant(array $array, callable $callback): void { @@ -40,4 +47,16 @@ function testConstant(array $array, callable $callback): void assertType("0|1|2|null", array_find_key($array, 'is_int')); } + function testCallback(): void + { + $subject = ['foo' => 1, 'bar' => null, 'buz' => '']; + $result = array_find_key($subject, function ($value, $key) { + assertType("array{value: 1|''|null, key: 'bar'|'buz'|'foo'}", compact('value', 'key')); + + return is_int($value); + }); + + assertType("'bar'|'buz'|'foo'|null", $result); + } + } diff --git a/tests/PHPStan/Analyser/nsrt/array-find.php b/tests/PHPStan/Analyser/nsrt/array-find.php index d4e8e10d47..f3b5b0b822 100644 --- a/tests/PHPStan/Analyser/nsrt/array-find.php +++ b/tests/PHPStan/Analyser/nsrt/array-find.php @@ -3,10 +3,15 @@ namespace { if (!function_exists('array_find')) { + /** + * @param array $array + * @param callable(mixed, array-key=): mixed $callback + * @return mixed + */ function array_find(array $array, callable $callback) { foreach ($array as $key => $value) { - if ($callback($value, $key)) { + if ($callback($value, $key)) { // @phpstan-ignore if.condNotBoolean return $value; } } @@ -25,6 +30,7 @@ function array_find(array $array, callable $callback) /** * @param array $array * @param non-empty-array $non_empty_array + * @phpstan-ignore missingType.callable */ function testMixed(array $array, array $non_empty_array, callable $callback): void { @@ -36,6 +42,7 @@ function testMixed(array $array, array $non_empty_array, callable $callback): vo /** * @param array{1, 'foo', \DateTime} $array + * @phpstan-ignore missingType.callable */ function testConstant(array $array, callable $callback): void { @@ -46,6 +53,7 @@ function testConstant(array $array, callable $callback): void /** * @param array $array * @param non-empty-array $non_empty_array + * @phpstan-ignore missingType.callable */ function testInt(array $array, array $non_empty_array, callable $callback): void { @@ -56,4 +64,16 @@ function testInt(array $array, array $non_empty_array, callable $callback): void assertType('int|null', array_find($non_empty_array, 'is_int')); } + function testCallback(): void + { + $subject = ['foo' => 1, 'bar' => null, 'buz' => '']; + $result = array_find($subject, function ($value, $key) { + assertType("array{value: 1|''|null, key: 'bar'|'buz'|'foo'}", compact('value', 'key')); + + return is_int($value); + }); + + assertType("1|''|null", $result); + } + } From 4b6752db2b441f269496687d52afaf28bc07b2ba Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Sun, 3 Nov 2024 03:17:43 +0900 Subject: [PATCH 5/9] Fix --- src/Type/Php/ArrayFindFunctionReturnTypeExtension.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php index 904aa16ab5..cb3c65b62d 100644 --- a/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php @@ -7,7 +7,6 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use function array_map; @@ -33,7 +32,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $resultTypes = $scope->getType(new FuncCall(new Name('\array_filter'), $functionCall->getArgs())); - $resultType = TypeCombinator::union(...array_map(fn ($type) => $type->getIterableValueType(), $resultTypes->getArrays())); + $resultType = TypeCombinator::union(...array_map(static fn ($type) => $type->getIterableValueType(), $resultTypes->getArrays())); return $resultTypes->isIterableAtLeastOnce()->yes() ? $resultType : TypeCombinator::addNull($resultType); } From c4f008912c170a28c008dd92a1aeae8e9f2b0305 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Mon, 4 Nov 2024 02:39:40 +0900 Subject: [PATCH 6/9] Use ArrayFilterFunctionReturnTypeHelper instead of FuncCall --- src/Type/Php/ArrayFindFunctionReturnTypeExtension.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php index cb3c65b62d..220d8fa0ef 100644 --- a/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFindFunctionReturnTypeExtension.php @@ -3,7 +3,6 @@ namespace PHPStan\Type\Php; use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Name; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\DynamicFunctionReturnTypeExtension; @@ -15,6 +14,10 @@ final class ArrayFindFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private ArrayFilterFunctionReturnTypeHelper $arrayFilterFunctionReturnTypeHelper) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_find'; @@ -31,7 +34,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return null; } - $resultTypes = $scope->getType(new FuncCall(new Name('\array_filter'), $functionCall->getArgs())); + $arrayArg = $functionCall->getArgs()[0]->value ?? null; + $callbackArg = $functionCall->getArgs()[1]->value ?? null; + + $resultTypes = $this->arrayFilterFunctionReturnTypeHelper->getType($scope, $arrayArg, $callbackArg, null); $resultType = TypeCombinator::union(...array_map(static fn ($type) => $type->getIterableValueType(), $resultTypes->getArrays())); return $resultTypes->isIterableAtLeastOnce()->yes() ? $resultType : TypeCombinator::addNull($resultType); From a4ba9dfb47bbcf49b169cdd9911f17fc97ec4204 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 6 Nov 2024 03:25:22 +0900 Subject: [PATCH 7/9] Remove redundant copied code --- src/Reflection/ParametersAcceptorSelector.php | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index e50ef2df4c..e40bec3269 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -291,37 +291,6 @@ public static function selectFromArgs( ]; } - if (isset($args[0]) && (bool) $args[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) { - $arrayWalkParameters = [ - new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createReadsArgument(), false, null), - new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), - ]; - if (isset($args[2])) { - $arrayWalkParameters[] = new DummyParameter('arg', $scope->getType($args[2]->value), false, PassedByReference::createNo(), false, null); - } - - $acceptor = $parametersAcceptors[0]; - $parameters = $acceptor->getParameters(); - $parameters[1] = new NativeParameterReflection( - $parameters[1]->getName(), - $parameters[1]->isOptional(), - new CallableType($arrayWalkParameters, new MixedType(), false), - $parameters[1]->passedByReference(), - $parameters[1]->isVariadic(), - $parameters[1]->getDefaultValue(), - ); - $parametersAcceptors = [ - new FunctionVariant( - $acceptor->getTemplateTypeMap(), - $acceptor->getResolvedTemplateTypeMap(), - $parameters, - $acceptor->isVariadic(), - $acceptor->getReturnType(), - $acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(), - ), - ]; - } - if (isset($args[0])) { $closureBindToVar = $args[0]->getAttribute(ClosureBindToVarVisitor::ATTRIBUTE_NAME); if ( From 9a75d3dc60122edba0ea3a4c909e8179a2cb0f0f Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 6 Nov 2024 04:58:12 +0900 Subject: [PATCH 8/9] Fix ParametersAcceptorSelector --- src/Reflection/ParametersAcceptorSelector.php | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index e40bec3269..07a7c28080 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -261,20 +261,18 @@ public static function selectFromArgs( if (isset($args[0]) && (bool) $args[0]->getAttribute(ArrayFindArgVisitor::ATTRIBUTE_NAME)) { $acceptor = $parametersAcceptors[0]; $parameters = $acceptor->getParameters(); + $argType = $scope->getType($args[0]->value); $parameters[1] = new NativeParameterReflection( $parameters[1]->getName(), $parameters[1]->isOptional(), - new UnionType([ - new CallableType( - [ - new DummyParameter('value', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), - new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), - ], - new BooleanType(), - false, - ), - new NullType(), - ]), + new CallableType( + [ + new DummyParameter('value', $scope->getIterableValueType($argType), false, PassedByReference::createNo(), false, null), + new DummyParameter('key', $scope->getIterableKeyType($argType), false, PassedByReference::createNo(), false, null), + ], + new BooleanType(), + false, + ), $parameters[1]->passedByReference(), $parameters[1]->isVariadic(), $parameters[1]->getDefaultValue(), From 104d3fb9560997c490475e9762d70c3725101684 Mon Sep 17 00:00:00 2001 From: USAMI Kenta Date: Wed, 6 Nov 2024 04:58:40 +0900 Subject: [PATCH 9/9] Add tests for array_find() and array_find_key() to CallToFunctionParametersRuleTest --- .../CallToFunctionParametersRuleTest.php | 52 ++++++++++++++++++ .../Rules/Functions/data/array_find.php | 53 +++++++++++++++++++ .../Rules/Functions/data/array_find_key.php | 53 +++++++++++++++++++ 3 files changed, 158 insertions(+) create mode 100644 tests/PHPStan/Rules/Functions/data/array_find.php create mode 100644 tests/PHPStan/Rules/Functions/data/array_find_key.php diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 4b00fb720e..cec109d5f8 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -886,6 +886,58 @@ public function testArrayFilterCallback(bool $checkExplicitMixed): void $this->analyse([__DIR__ . '/data/array_filter_callback.php'], $errors); } + public function testArrayFindCallback(): void + { + $this->analyse([__DIR__ . '/data/array_find.php'], [ + [ + 'Parameter #2 $callback of function array_find expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 22, + ], + [ + 'Parameter #2 $callback of function array_find expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 30, + ], + [ + 'Parameter #2 $callback of function array_find expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(int, string): (\'bar\'|\'foo\') given.', + 36, + ], + [ + 'Parameter #2 $callback of function array_find expects callable(mixed, int|string): bool, Closure(string, array): false given.', + 49, + ], + [ + 'Parameter #2 $callback of function array_find expects callable(mixed, int|string): bool, Closure(string, int): array{} given.', + 52, + ], + ]); + } + + public function testArrayFindKeyCallback(): void + { + $this->analyse([__DIR__ . '/data/array_find_key.php'], [ + [ + 'Parameter #2 $callback of function array_find_key expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 22, + ], + [ + 'Parameter #2 $callback of function array_find_key expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(string, int): bool given.', + 30, + ], + [ + 'Parameter #2 $callback of function array_find_key expects callable(1|2, \'bar\'|\'foo\'): bool, Closure(int, string): (\'bar\'|\'foo\') given.', + 36, + ], + [ + 'Parameter #2 $callback of function array_find_key expects callable(mixed, int|string): bool, Closure(string, array): false given.', + 49, + ], + [ + 'Parameter #2 $callback of function array_find_key expects callable(mixed, int|string): bool, Closure(string, int): array{} given.', + 52, + ], + ]); + } + public function testBug5356(): void { $this->analyse([__DIR__ . '/data/bug-5356.php'], [ diff --git a/tests/PHPStan/Rules/Functions/data/array_find.php b/tests/PHPStan/Rules/Functions/data/array_find.php new file mode 100644 index 0000000000..a749bd8294 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_find.php @@ -0,0 +1,53 @@ += 8.4 + +// ok +array_find( + ['foo' => 1, 'bar' => 2], + function($value, $key) { + return $key === 0; + } +); + +// ok +array_find( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key === 0; + } +); + +// bad parameters +array_find( + ['foo' => 1, 'bar' => 2], + function(string $value, int $key): bool { + return $key === 0; + } +); + +// bad parameters +array_find( + ['foo' => 1, 'bar' => 2], + fn (string $item, int $key) => $key === 0, +); + +// bad return type +array_find( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key; + }, +); + +if (is_array($array)) { + // ok + array_find($array, fn ($value, $key) => $key === 0); + + // ok + array_find($array, fn (string $value, int $key) => $key === 0); + + // bad parameters + array_find($array, fn (string $item, array $key) => $key === 0); + + // bad return type + array_find($array, fn (string $value, int $key): array => []); +} diff --git a/tests/PHPStan/Rules/Functions/data/array_find_key.php b/tests/PHPStan/Rules/Functions/data/array_find_key.php new file mode 100644 index 0000000000..393468d42c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/array_find_key.php @@ -0,0 +1,53 @@ += 8.4 + +// ok +array_find_key( + ['foo' => 1, 'bar' => 2], + function($value, $key) { + return $key === 0; + } +); + +// ok +array_find_key( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key === 0; + } +); + +// bad parameters +array_find_key( + ['foo' => 1, 'bar' => 2], + function(string $value, int $key): bool { + return $key === 0; + } +); + +// bad parameters +array_find_key( + ['foo' => 1, 'bar' => 2], + fn (string $item, int $key) => $key === 0, +); + +// bad return type +array_find_key( + ['foo' => 1, 'bar' => 2], + function(int $value, string $key) { + return $key; + }, +); + +if (is_array($array)) { + // ok + array_find_key($array, fn ($value, $key) => $key === 0); + + // ok + array_find_key($array, fn (string $value, int $key) => $key === 0); + + // bad parameters + array_find_key($array, fn (string $item, array $key) => $key === 0); + + // bad return type + array_find_key($array, fn (string $value, int $key): array => []); +}