Skip to content

Commit 5ecdd07

Browse files
adaamzondrejmirtes
authored andcommitted
array_search return type is always array key|false
1 parent 2787c19 commit 5ecdd07

File tree

4 files changed

+146
-0
lines changed

4 files changed

+146
-0
lines changed

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,11 @@ services:
287287
tags:
288288
- phpstan.broker.dynamicFunctionReturnTypeExtension
289289

290+
-
291+
class: PHPStan\Type\Php\ArraySearchFunctionDynamicReturnTypeExtension
292+
tags:
293+
- phpstan.broker.dynamicFunctionReturnTypeExtension
294+
290295
-
291296
class: PHPStan\Type\Php\ArrayValuesFunctionDynamicReturnTypeExtension
292297
tags:
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\FunctionReflection;
8+
use PHPStan\Reflection\ParametersAcceptorSelector;
9+
use PHPStan\Type\Constant\ConstantArrayType;
10+
use PHPStan\Type\Constant\ConstantBooleanType;
11+
use PHPStan\Type\ConstantScalarType;
12+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
13+
use PHPStan\Type\NullType;
14+
use PHPStan\Type\ObjectWithoutClassType;
15+
use PHPStan\Type\Type;
16+
use PHPStan\Type\TypeCombinator;
17+
use PHPStan\Type\TypeUtils;
18+
use PHPStan\Type\UnionType;
19+
20+
final class ArraySearchFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
21+
{
22+
23+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
24+
{
25+
return $functionReflection->getName() === 'array_search';
26+
}
27+
28+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
29+
{
30+
if (count($functionCall->args) < 3) {
31+
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
32+
}
33+
34+
$strictArgType = $scope->getType($functionCall->args[2]->value);
35+
$haystackArgType = $scope->getType($functionCall->args[1]->value);
36+
if (!($strictArgType instanceof ConstantBooleanType) || $strictArgType->getValue() === false) {
37+
return new UnionType([$haystackArgType->getIterableKeyType(), new ConstantBooleanType(false), new NullType()]);
38+
}
39+
40+
$needleArgType = $scope->getType($functionCall->args[0]->value);
41+
42+
if (count(TypeUtils::getArrays($haystackArgType)) === 0) {
43+
return new NullType();
44+
}
45+
46+
if ($haystackArgType->getIterableValueType()->isSuperTypeOf($needleArgType)->no()) {
47+
return new ConstantBooleanType(false);
48+
}
49+
50+
$constantArrays = TypeUtils::getConstantArrays($haystackArgType);
51+
if (count($constantArrays) > 0) {
52+
$types = [];
53+
foreach ($constantArrays as $constantArray) {
54+
$types[] = $this->resolveTypeFromConstantHaystackAndNeedle($needleArgType, $constantArray);
55+
}
56+
57+
return TypeCombinator::union(...$types);
58+
}
59+
60+
return TypeCombinator::union($haystackArgType->getIterableKeyType(), new ConstantBooleanType(false));
61+
}
62+
63+
private function resolveTypeFromConstantHaystackAndNeedle(Type $needle, ConstantArrayType $haystack): Type
64+
{
65+
$matchesByType = [];
66+
67+
foreach ($haystack->getValueTypes() as $index => $valueType) {
68+
if ($needle->isSuperTypeOf($valueType)->no()) {
69+
$matchesByType[] = new ConstantBooleanType(false);
70+
continue;
71+
}
72+
73+
if ($needle instanceof ConstantScalarType && $valueType instanceof ConstantScalarType
74+
&& $needle->getValue() === $valueType->getValue()
75+
) {
76+
return $haystack->getKeyTypes()[$index];
77+
}
78+
79+
$matchesByType[] = $haystack->getKeyTypes()[$index];
80+
}
81+
82+
if (count($matchesByType) > 0) {
83+
if (
84+
$haystack->getIterableValueType()->accepts($needle, true)->yes()
85+
&& $needle->isSuperTypeOf(new ObjectWithoutClassType())->no()
86+
) {
87+
return TypeCombinator::union(...$matchesByType);
88+
}
89+
90+
return TypeCombinator::union(new ConstantBooleanType(false), ...$matchesByType);
91+
}
92+
93+
return new ConstantBooleanType(false);
94+
}
95+
96+
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4287,6 +4287,42 @@ public function dataArrayFunctions(): array
42874287
'array<int, int|true>',
42884288
'array_filter($withPossiblyFalsey)',
42894289
],
4290+
[
4291+
'1|\'foo\'|false',
4292+
'array_search(new stdClass, $stringOrIntegerKeys, true)',
4293+
],
4294+
[
4295+
'\'foo\'',
4296+
'array_search(\'foo\', $stringKeys, true)',
4297+
],
4298+
[
4299+
'int|false',
4300+
'array_search(new DateTimeImmutable(), $generalDateTimeValues, true)',
4301+
],
4302+
[
4303+
'string|false',
4304+
'array_search(9, $generalStringKeys, true)',
4305+
],
4306+
[
4307+
'null',
4308+
'array_search(999, $integer, true)',
4309+
],
4310+
[
4311+
'false',
4312+
'array_search(new stdClass, $generalStringKeys, true)',
4313+
],
4314+
[
4315+
'\'foo\'|false',
4316+
'array_search($generalIntegerOrString, $stringKeys, true)',
4317+
],
4318+
[
4319+
'int|false',
4320+
'array_search($generalIntegerOrString, $generalArrayOfIntegersOrStrings, true)',
4321+
],
4322+
[
4323+
'int|false',
4324+
'array_search($generalIntegerOrString, $clonedConditionalArray, true)',
4325+
],
42904326
];
42914327
}
42924328

tests/PHPStan/Analyser/data/array-functions.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,13 @@
112112
$conditionalKeysArray['lorem'] = 1;
113113
}
114114

115+
/** @var int|string $generalIntegerOrString */
116+
$generalIntegerOrString = doFoo();
117+
118+
/** @var array<int, int|string> $generalArrayOfIntegersOrStrings */
119+
$generalArrayOfIntegersOrStrings = doFoo();
120+
121+
$clonedConditionalArray = $conditionalArray;
122+
$clonedConditionalArray[(int)$generalIntegerOrString] = $generalIntegerOrString;
123+
115124
die;

0 commit comments

Comments
 (0)