Skip to content

Commit 873b78c

Browse files
authored
Report finite-typed values in a constant array haystack that can never be the in_array()/array_search()/array_keys() needle (#6054)
1 parent f05a4d4 commit 873b78c

7 files changed

Lines changed: 292 additions & 0 deletions

File tree

‎conf/bleedingEdge.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ parameters:
2222
checkDynamicConstantNameValues: true
2323
unusedLabel: true
2424
newOnNonObject: true
25+
finiteTypesInHaystack: true

‎conf/config.level4.neon‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ conditionalTags:
1414
phpstan.rules.rule: %exceptions.check.tooWideThrowType%
1515
PHPStan\Rules\Keywords\UnusedLabelRule:
1616
phpstan.rules.rule: %featureToggles.unusedLabel%
17+
PHPStan\Rules\Comparison\ImpossibleInArrayHaystackFiniteTypesRule:
18+
phpstan.rules.rule: %featureToggles.finiteTypesInHaystack%
1719

1820
parameters:
1921
checkAdvancedIsset: true
@@ -35,3 +37,8 @@ services:
3537

3638
-
3739
class: PHPStan\Rules\Keywords\UnusedLabelRule
40+
41+
-
42+
class: PHPStan\Rules\Comparison\ImpossibleInArrayHaystackFiniteTypesRule
43+
arguments:
44+
treatPhpDocTypesAsCertain: %treatPhpDocTypesAsCertain%

‎conf/config.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ parameters:
4848
checkDynamicConstantNameValues: false
4949
unusedLabel: false
5050
newOnNonObject: false
51+
finiteTypesInHaystack: false
5152
fileExtensions:
5253
- php
5354
checkAdvancedIsset: false

‎conf/parametersSchema.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ parametersSchema:
5151
checkDynamicConstantNameValues: bool()
5252
unusedLabel: bool()
5353
newOnNonObject: bool()
54+
finiteTypesInHaystack: bool()
5455
])
5556
fileExtensions: listOf(string())
5657
checkAdvancedIsset: bool()
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\FuncCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\InitializerExprTypeResolver;
9+
use PHPStan\Rules\IdentifierRuleError;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleErrorBuilder;
12+
use PHPStan\TrinaryLogic;
13+
use PHPStan\Type\Type;
14+
use PHPStan\Type\VerbosityLevel;
15+
use function array_key_exists;
16+
use function count;
17+
use function max;
18+
use function sprintf;
19+
use function strtolower;
20+
21+
/**
22+
* Reports finite-typed values in a constant array haystack passed to in_array(),
23+
* array_search() or array_keys() that can never be the needle, using
24+
* Type::getConstantArrays() and Type::getFiniteTypes() instead of inspecting the
25+
* AST of the array literal.
26+
*
27+
* @implements Rule<FuncCall>
28+
*/
29+
final class ImpossibleInArrayHaystackFiniteTypesRule implements Rule
30+
{
31+
32+
/** Argument positions of the needle and the haystack per supported function. */
33+
private const FUNCTIONS = [
34+
'in_array' => ['needle' => 0, 'haystack' => 1],
35+
'array_search' => ['needle' => 0, 'haystack' => 1],
36+
'array_keys' => ['needle' => 1, 'haystack' => 0],
37+
];
38+
39+
public function __construct(
40+
private InitializerExprTypeResolver $initializerExprTypeResolver,
41+
private bool $treatPhpDocTypesAsCertain,
42+
)
43+
{
44+
}
45+
46+
public function getNodeType(): string
47+
{
48+
return FuncCall::class;
49+
}
50+
51+
public function processNode(Node $node, Scope $scope): array
52+
{
53+
if (!$node->name instanceof Node\Name) {
54+
return [];
55+
}
56+
57+
$functionName = strtolower((string) $node->name);
58+
if (!array_key_exists($functionName, self::FUNCTIONS)) {
59+
return [];
60+
}
61+
62+
$needleArg = self::FUNCTIONS[$functionName]['needle'];
63+
$haystackArg = self::FUNCTIONS[$functionName]['haystack'];
64+
65+
$args = $node->getArgs();
66+
if (count($args) <= max($needleArg, $haystackArg)) {
67+
return [];
68+
}
69+
70+
$needleType = $this->treatPhpDocTypesAsCertain ? $scope->getType($args[$needleArg]->value) : $scope->getNativeType($args[$needleArg]->value);
71+
$haystackType = $this->treatPhpDocTypesAsCertain ? $scope->getType($args[$haystackArg]->value) : $scope->getNativeType($args[$haystackArg]->value);
72+
73+
$constantArrays = $haystackType->getConstantArrays();
74+
if (count($constantArrays) === 0) {
75+
return [];
76+
}
77+
78+
$isStrict = count($args) >= 3
79+
? ($this->treatPhpDocTypesAsCertain ? $scope->getType($args[2]->value) : $scope->getNativeType($args[2]->value))->isTrue()
80+
: TrinaryLogic::createNo();
81+
82+
$deadValueTypes = [];
83+
$anyPossibleMatch = false;
84+
foreach ($constantArrays as $constantArray) {
85+
foreach ($constantArray->getValueTypes() as $valueType) {
86+
if (!$this->canNeverMatch($needleType, $valueType, $isStrict)) {
87+
$anyPossibleMatch = true;
88+
continue;
89+
}
90+
91+
if (count($valueType->getFiniteTypes()) === 0) {
92+
continue;
93+
}
94+
95+
$deadValueTypes[$valueType->describe(VerbosityLevel::precise())] = $valueType;
96+
}
97+
}
98+
99+
// When no haystack value can ever match, the whole in_array() call is
100+
// impossible and reported by ImpossibleCheckTypeFunctionCallRule instead.
101+
// array_search() has no such companion rule, so keep reporting there.
102+
if (!$anyPossibleMatch && $functionName === 'in_array') {
103+
return [];
104+
}
105+
106+
$verb = $isStrict->no() ? 'equal to' : 'identical to';
107+
108+
$errors = [];
109+
foreach ($deadValueTypes as $valueType) {
110+
$errors[] = $this->buildError($valueType, $needleType, $functionName, $verb);
111+
}
112+
113+
return $errors;
114+
}
115+
116+
private function canNeverMatch(Type $needleType, Type $valueType, TrinaryLogic $isStrict): bool
117+
{
118+
if ($isStrict->yes()) {
119+
return $this->initializerExprTypeResolver->resolveIdenticalType($needleType, $valueType)->type->isFalse()->yes();
120+
}
121+
122+
if ($isStrict->no()) {
123+
return $this->initializerExprTypeResolver->resolveEqualType($needleType, $valueType)->type->isFalse()->yes();
124+
}
125+
126+
return $this->initializerExprTypeResolver->resolveIdenticalType($needleType, $valueType)->type->isFalse()->yes()
127+
&& $this->initializerExprTypeResolver->resolveEqualType($needleType, $valueType)->type->isFalse()->yes();
128+
}
129+
130+
private function buildError(Type $valueType, Type $needleType, string $functionName, string $verb): IdentifierRuleError
131+
{
132+
return RuleErrorBuilder::message(sprintf(
133+
'Value %s in the haystack passed to %s() can never be %s the needle type %s.',
134+
$valueType->describe(VerbosityLevel::precise()),
135+
$functionName,
136+
$verb,
137+
$needleType->describe(VerbosityLevel::precise()),
138+
))->identifier('function.impossibleHaystackValue')->build();
139+
}
140+
141+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Comparison;
4+
5+
use PHPStan\Reflection\InitializerExprTypeResolver;
6+
use PHPStan\Rules\Rule;
7+
use PHPStan\Testing\RuleTestCase;
8+
use PHPUnit\Framework\Attributes\RequiresPhp;
9+
10+
/**
11+
* @extends RuleTestCase<ImpossibleInArrayHaystackFiniteTypesRule>
12+
*/
13+
class ImpossibleInArrayHaystackFiniteTypesRuleTest extends RuleTestCase
14+
{
15+
16+
protected function getRule(): Rule
17+
{
18+
return new ImpossibleInArrayHaystackFiniteTypesRule(
19+
self::getContainer()->getByType(InitializerExprTypeResolver::class),
20+
true,
21+
);
22+
}
23+
24+
#[RequiresPhp('>= 8.1.0')]
25+
public function testRule(): void
26+
{
27+
$this->analyse([__DIR__ . '/data/impossible-in-array-finite-types.php'], [
28+
[
29+
'Value ImpossibleInArrayFiniteTypes\Foo::ONE in the haystack passed to in_array() can never be identical to the needle type int.',
30+
19,
31+
],
32+
[
33+
'Value ImpossibleInArrayFiniteTypes\Foo::ONE in the haystack passed to in_array() can never be equal to the needle type int.',
34+
26,
35+
],
36+
[
37+
'Value ImpossibleInArrayFiniteTypes\Foo::ONE in the haystack passed to array_search() can never be identical to the needle type int.',
38+
33,
39+
],
40+
[
41+
'Value ImpossibleInArrayFiniteTypes\Foo::ONE in the haystack passed to array_keys() can never be identical to the needle type int.',
42+
38,
43+
],
44+
[
45+
'Value ImpossibleInArrayFiniteTypes\Foo::TWO in the haystack passed to in_array() can never be identical to the needle type ImpossibleInArrayFiniteTypes\Foo::ONE.',
46+
48,
47+
],
48+
]);
49+
}
50+
51+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace ImpossibleInArrayFiniteTypes;
6+
7+
enum Foo
8+
{
9+
10+
case ONE;
11+
case TWO;
12+
case THREE;
13+
14+
}
15+
16+
function reportedFiniteValueStrict(int $i): void
17+
{
18+
// Foo::ONE can never be an int, but 1 and 2 can.
19+
if (in_array($i, [Foo::ONE, 1, 2], true)) {
20+
echo 'yes';
21+
}
22+
}
23+
24+
function reportedFiniteValueLoose(int $i): void
25+
{
26+
if (in_array($i, [Foo::ONE, 1, 2])) {
27+
echo 'yes';
28+
}
29+
}
30+
31+
function reportedArraySearch(int $i): void
32+
{
33+
array_search($i, [Foo::ONE, 1, 2], true);
34+
}
35+
36+
function reportedArrayKeys(int $i): void
37+
{
38+
array_keys([Foo::ONE, 1, 2], $i, true);
39+
}
40+
41+
function reportedEnumNeedle(Foo $foo): void
42+
{
43+
if ($foo !== Foo::ONE) {
44+
return;
45+
}
46+
47+
// Foo::TWO can never be Foo::ONE (finite value in haystack), but Foo::ONE can.
48+
if (in_array($foo, [Foo::ONE, Foo::TWO], true)) {
49+
echo 'yes';
50+
}
51+
}
52+
53+
function noErrorEverythingMatches(int $i): void
54+
{
55+
if (in_array($i, [1, 2, 3], true)) {
56+
echo 'yes';
57+
}
58+
}
59+
60+
function noErrorMixedNeedle(mixed $i): void
61+
{
62+
if (in_array($i, [Foo::ONE, 1, 2], true)) {
63+
echo 'yes';
64+
}
65+
}
66+
67+
function noErrorWholeCallImpossible(int $i): void
68+
{
69+
// Whole call is impossible - reported by ImpossibleCheckTypeFunctionCallRule instead.
70+
if (in_array($i, [Foo::ONE, Foo::TWO], true)) {
71+
echo 'yes';
72+
}
73+
}
74+
75+
function noErrorNonConstantHaystack(int $i, array $haystack): void
76+
{
77+
if (in_array($i, $haystack, true)) {
78+
echo 'yes';
79+
}
80+
}
81+
82+
/**
83+
* @param int|string $i
84+
*/
85+
function noErrorUnionNeedleMatches(int|string $i): void
86+
{
87+
if (in_array($i, ['a', 'b', 1], true)) {
88+
echo 'yes';
89+
}
90+
}

0 commit comments

Comments
 (0)