Skip to content

Commit cbacc8a

Browse files
committed
Add ArrayFindArgVisitor
1 parent 0aea23f commit cbacc8a

File tree

5 files changed

+139
-2
lines changed

5 files changed

+139
-2
lines changed

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,11 @@ services:
325325
tags:
326326
- phpstan.parser.richParserNodeVisitor
327327

328+
-
329+
class: PHPStan\Parser\ArrayFindArgVisitor
330+
tags:
331+
- phpstan.parser.richParserNodeVisitor
332+
328333
-
329334
class: PHPStan\Parser\ArrayMapArgVisitor
330335
tags:

src/Parser/ArrayFindArgVisitor.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Parser;
4+
5+
use PhpParser\Node;
6+
use PhpParser\NodeVisitorAbstract;
7+
use function in_array;
8+
9+
final class ArrayFindArgVisitor extends NodeVisitorAbstract
10+
{
11+
12+
public const ATTRIBUTE_NAME = 'isArrayFindArg';
13+
14+
public function enterNode(Node $node): ?Node
15+
{
16+
if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) {
17+
$functionName = $node->name->toLowerString();
18+
if (in_array($functionName, ['array_find', 'array_find_key'], true)) {
19+
$args = $node->getRawArgs();
20+
if (isset($args[0])) {
21+
$args[0]->setAttribute(self::ATTRIBUTE_NAME, true);
22+
}
23+
}
24+
}
25+
return null;
26+
}
27+
28+
}

src/Reflection/ParametersAcceptorSelector.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPStan\Analyser\Scope;
1010
use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr;
1111
use PHPStan\Parser\ArrayFilterArgVisitor;
12+
use PHPStan\Parser\ArrayFindArgVisitor;
1213
use PHPStan\Parser\ArrayMapArgVisitor;
1314
use PHPStan\Parser\ArrayWalkArgVisitor;
1415
use PHPStan\Parser\ClosureBindArgVisitor;
@@ -257,6 +258,70 @@ public static function selectFromArgs(
257258
];
258259
}
259260

261+
if (isset($args[0]) && (bool) $args[0]->getAttribute(ArrayFindArgVisitor::ATTRIBUTE_NAME)) {
262+
$acceptor = $parametersAcceptors[0];
263+
$parameters = $acceptor->getParameters();
264+
$parameters[1] = new NativeParameterReflection(
265+
$parameters[1]->getName(),
266+
$parameters[1]->isOptional(),
267+
new UnionType([
268+
new CallableType(
269+
[
270+
new DummyParameter('value', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null),
271+
new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null),
272+
],
273+
new BooleanType(),
274+
false,
275+
),
276+
new NullType(),
277+
]),
278+
$parameters[1]->passedByReference(),
279+
$parameters[1]->isVariadic(),
280+
$parameters[1]->getDefaultValue(),
281+
);
282+
$parametersAcceptors = [
283+
new FunctionVariant(
284+
$acceptor->getTemplateTypeMap(),
285+
$acceptor->getResolvedTemplateTypeMap(),
286+
$parameters,
287+
$acceptor->isVariadic(),
288+
$acceptor->getReturnType(),
289+
$acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(),
290+
),
291+
];
292+
}
293+
294+
if (isset($args[0]) && (bool) $args[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) {
295+
$arrayWalkParameters = [
296+
new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createReadsArgument(), false, null),
297+
new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null),
298+
];
299+
if (isset($args[2])) {
300+
$arrayWalkParameters[] = new DummyParameter('arg', $scope->getType($args[2]->value), false, PassedByReference::createNo(), false, null);
301+
}
302+
303+
$acceptor = $parametersAcceptors[0];
304+
$parameters = $acceptor->getParameters();
305+
$parameters[1] = new NativeParameterReflection(
306+
$parameters[1]->getName(),
307+
$parameters[1]->isOptional(),
308+
new CallableType($arrayWalkParameters, new MixedType(), false),
309+
$parameters[1]->passedByReference(),
310+
$parameters[1]->isVariadic(),
311+
$parameters[1]->getDefaultValue(),
312+
);
313+
$parametersAcceptors = [
314+
new FunctionVariant(
315+
$acceptor->getTemplateTypeMap(),
316+
$acceptor->getResolvedTemplateTypeMap(),
317+
$parameters,
318+
$acceptor->isVariadic(),
319+
$acceptor->getReturnType(),
320+
$acceptor instanceof ParametersAcceptorWithPhpDocs ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(),
321+
),
322+
];
323+
}
324+
260325
if (isset($args[0])) {
261326
$closureBindToVar = $args[0]->getAttribute(ClosureBindToVarVisitor::ATTRIBUTE_NAME);
262327
if (

tests/PHPStan/Analyser/nsrt/array-find-key.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
namespace {
44

55
if (!function_exists('array_find_key')) {
6+
/**
7+
* @param array<mixed> $array
8+
* @param callable(mixed, array-key=): mixed $callback
9+
* @return ?array-key
10+
*/
611
function array_find_key(array $array, callable $callback)
712
{
813
foreach ($array as $key => $value) {
9-
if ($callback($value, $key)) {
14+
if ($callback($value, $key)) { // @phpstan-ignore if.condNotBoolean
1015
return $key;
1116
}
1217
}
@@ -24,6 +29,7 @@ function array_find_key(array $array, callable $callback)
2429

2530
/**
2631
* @param array<mixed> $array
32+
* @phpstan-ignore missingType.callable
2733
*/
2834
function testMixed(array $array, callable $callback): void
2935
{
@@ -33,11 +39,24 @@ function testMixed(array $array, callable $callback): void
3339

3440
/**
3541
* @param array{1, 'foo', \DateTime} $array
42+
* @phpstan-ignore missingType.callable
3643
*/
3744
function testConstant(array $array, callable $callback): void
3845
{
3946
assertType("0|1|2|null", array_find_key($array, $callback));
4047
assertType("0|1|2|null", array_find_key($array, 'is_int'));
4148
}
4249

50+
function testCallback(): void
51+
{
52+
$subject = ['foo' => 1, 'bar' => null, 'buz' => ''];
53+
$result = array_find_key($subject, function ($value, $key) {
54+
assertType("array{value: 1|''|null, key: 'bar'|'buz'|'foo'}", compact('value', 'key'));
55+
56+
return is_int($value);
57+
});
58+
59+
assertType("'bar'|'buz'|'foo'|null", $result);
60+
}
61+
4362
}

tests/PHPStan/Analyser/nsrt/array-find.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
namespace {
44

55
if (!function_exists('array_find')) {
6+
/**
7+
* @param array<mixed> $array
8+
* @param callable(mixed, array-key=): mixed $callback
9+
* @return mixed
10+
*/
611
function array_find(array $array, callable $callback)
712
{
813
foreach ($array as $key => $value) {
9-
if ($callback($value, $key)) {
14+
if ($callback($value, $key)) { // @phpstan-ignore if.condNotBoolean
1015
return $value;
1116
}
1217
}
@@ -25,6 +30,7 @@ function array_find(array $array, callable $callback)
2530
/**
2631
* @param array<mixed> $array
2732
* @param non-empty-array<mixed> $non_empty_array
33+
* @phpstan-ignore missingType.callable
2834
*/
2935
function testMixed(array $array, array $non_empty_array, callable $callback): void
3036
{
@@ -36,6 +42,7 @@ function testMixed(array $array, array $non_empty_array, callable $callback): vo
3642

3743
/**
3844
* @param array{1, 'foo', \DateTime} $array
45+
* @phpstan-ignore missingType.callable
3946
*/
4047
function testConstant(array $array, callable $callback): void
4148
{
@@ -46,6 +53,7 @@ function testConstant(array $array, callable $callback): void
4653
/**
4754
* @param array<int> $array
4855
* @param non-empty-array<int> $non_empty_array
56+
* @phpstan-ignore missingType.callable
4957
*/
5058
function testInt(array $array, array $non_empty_array, callable $callback): void
5159
{
@@ -56,4 +64,16 @@ function testInt(array $array, array $non_empty_array, callable $callback): void
5664
assertType('int|null', array_find($non_empty_array, 'is_int'));
5765
}
5866

67+
function testCallback(): void
68+
{
69+
$subject = ['foo' => 1, 'bar' => null, 'buz' => ''];
70+
$result = array_find($subject, function ($value, $key) {
71+
assertType("array{value: 1|''|null, key: 'bar'|'buz'|'foo'}", compact('value', 'key'));
72+
73+
return is_int($value);
74+
});
75+
76+
assertType("1|''|null", $result);
77+
}
78+
5979
}

0 commit comments

Comments
 (0)