-
Notifications
You must be signed in to change notification settings - Fork 546
Add ArrayFindFunctionReturnTypeExtension #3518
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
be71b31
b18c7e0
0aea23f
cbacc8a
4b6752d
c4f0089
a4ba9df
9a75d3d
104d3fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| <?php declare(strict_types = 1); | ||
|
|
||
| namespace PHPStan\Parser; | ||
|
|
||
| use PhpParser\Node; | ||
| use PhpParser\NodeVisitorAbstract; | ||
| use function in_array; | ||
|
|
||
| final class ArrayFindArgVisitor extends NodeVisitorAbstract | ||
| { | ||
|
|
||
| public const ATTRIBUTE_NAME = 'isArrayFindArg'; | ||
|
|
||
| public function enterNode(Node $node): ?Node | ||
| { | ||
| if ($node instanceof Node\Expr\FuncCall && $node->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; | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| <?php declare(strict_types = 1); | ||
|
|
||
| namespace PHPStan\Type\Php; | ||
|
|
||
| use PhpParser\Node\Expr\FuncCall; | ||
| use PHPStan\Analyser\Scope; | ||
| use PHPStan\Reflection\FunctionReflection; | ||
| use PHPStan\Type\DynamicFunctionReturnTypeExtension; | ||
| use PHPStan\Type\Type; | ||
| use PHPStan\Type\TypeCombinator; | ||
| use function array_map; | ||
| use function count; | ||
|
|
||
| final class ArrayFindFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension | ||
| { | ||
|
|
||
| public function __construct(private ArrayFilterFunctionReturnTypeHelper $arrayFilterFunctionReturnTypeHelper) | ||
| { | ||
| } | ||
|
|
||
| public function isFunctionSupported(FunctionReflection $functionReflection): bool | ||
| { | ||
| return $functionReflection->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; | ||
| } | ||
|
|
||
| $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); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| <?php declare(strict_types = 1); | ||
|
|
||
| namespace PHPStan\Type\Php; | ||
|
|
||
| use PhpParser\Node\Expr\FuncCall; | ||
| 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 count; | ||
|
|
||
| final class ArrayFindKeyFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension | ||
| { | ||
|
|
||
| public function isFunctionSupported(FunctionReflection $functionReflection): bool | ||
| { | ||
| return $functionReflection->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()); | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| <?php | ||
|
|
||
| namespace { | ||
|
|
||
| if (!function_exists('array_find_key')) { | ||
| /** | ||
| * @param array<mixed> $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)) { // @phpstan-ignore if.condNotBoolean | ||
| return $key; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
| } | ||
|
|
||
| } | ||
|
|
||
| namespace ArrayFindKey | ||
| { | ||
|
|
||
| use function PHPStan\Testing\assertType; | ||
|
|
||
| /** | ||
| * @param array<mixed> $array | ||
| * @phpstan-ignore missingType.callable | ||
| */ | ||
| 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 | ||
| * @phpstan-ignore missingType.callable | ||
| */ | ||
| 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')); | ||
|
||
| } | ||
|
|
||
| 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); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| <?php | ||
|
|
||
| namespace { | ||
|
|
||
| if (!function_exists('array_find')) { | ||
| /** | ||
| * @param array<mixed> $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)) { // @phpstan-ignore if.condNotBoolean | ||
| return $value; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
| } | ||
|
|
||
| } | ||
|
|
||
| namespace ArrayFind | ||
| { | ||
|
|
||
| use function PHPStan\Testing\assertType; | ||
|
|
||
| /** | ||
| * @param array<mixed> $array | ||
| * @param non-empty-array<mixed> $non_empty_array | ||
| * @phpstan-ignore missingType.callable | ||
| */ | ||
| 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')); | ||
| } | ||
|
|
||
| /** | ||
| * @param array{1, 'foo', \DateTime} $array | ||
| * @phpstan-ignore missingType.callable | ||
| */ | ||
| function testConstant(array $array, callable $callback): void | ||
| { | ||
| assertType("1|'foo'|DateTime|null", array_find($array, $callback)); | ||
| assertType('1', array_find($array, 'is_int')); | ||
| } | ||
|
|
||
| /** | ||
| * @param array<int> $array | ||
| * @param non-empty-array<int> $non_empty_array | ||
| * @phpstan-ignore missingType.callable | ||
| */ | ||
| 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')); | ||
| } | ||
|
|
||
| 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); | ||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should test on non-empty-array and it should gives int|string
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can add a test case, but if you pass a
non-empty-array<mixed>the function may return anyint|string|null.https://3v4l.org/Biok8
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, my bad.