Skip to content

Commit 09dbdcf

Browse files
authored
Fix more in_array issues
1 parent 8bb4537 commit 09dbdcf

10 files changed

+300
-52
lines changed

src/Rules/Comparison/ImpossibleCheckTypeHelper.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,14 @@ public function findSpecifiedType(
9999
$needleArg = $node->getArgs()[0]->value;
100100
$needleType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($needleArg) : $scope->getNativeType($needleArg));
101101
$valueType = $haystackType->getIterableValueType();
102-
$constantNeedleTypesCount = count($needleType->getConstantScalarValues());
103-
$constantHaystackTypesCount = count($valueType->getConstantScalarValues());
102+
$constantNeedleTypesCount = count($needleType->getConstantScalarValues())
103+
+ count($needleType->getEnumCases());
104+
$constantHaystackTypesCount = count($valueType->getConstantScalarValues())
105+
+ count($valueType->getEnumCases());
104106
$isNeedleSupertype = $needleType->isSuperTypeOf($valueType);
105107
if ($haystackType->isConstantArray()->no()) {
106108
if ($haystackType->isIterableAtLeastOnce()->yes()) {
109+
// In this case the generic implementation via typeSpecifier fails, because the argument types cannot be narrowed down.
107110
if ($constantNeedleTypesCount === 1 && $constantHaystackTypesCount === 1) {
108111
if ($isNeedleSupertype->yes()) {
109112
return true;
@@ -112,8 +115,9 @@ public function findSpecifiedType(
112115
return false;
113116
}
114117
}
118+
119+
return null;
115120
}
116-
return null;
117121
}
118122

119123
if (!$haystackType instanceof ConstantArrayType || count($haystackType->getValueTypes()) > 0) {

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8540,43 +8540,6 @@ public function testPropertyArrayAssignment(
85408540
);
85418541
}
85428542

8543-
public function dataInArray(): array
8544-
{
8545-
return [
8546-
[
8547-
'\'bar\'|\'foo\'',
8548-
'$s',
8549-
],
8550-
[
8551-
'string',
8552-
'$mixed',
8553-
],
8554-
[
8555-
'string',
8556-
'$r',
8557-
],
8558-
[
8559-
'\'foo\'',
8560-
'$fooOrBarOrBaz',
8561-
],
8562-
];
8563-
}
8564-
8565-
/**
8566-
* @dataProvider dataInArray
8567-
*/
8568-
public function testInArray(
8569-
string $description,
8570-
string $expression,
8571-
): void
8572-
{
8573-
$this->assertTypes(
8574-
__DIR__ . '/data/in-array.php',
8575-
$description,
8576-
$expression,
8577-
);
8578-
}
8579-
85808543
public function dataGetParentClass(): array
85818544
{
85828545
return [

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,7 @@ public function dataFileAsserts(): iterable
989989
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7031.php');
990990
yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-array-intersect.php');
991991
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7153.php');
992+
yield from $this->gatherAssertTypes(__DIR__ . '/data/in-array.php');
992993
yield from $this->gatherAssertTypes(__DIR__ . '/data/in-array-non-empty.php');
993994
yield from $this->gatherAssertTypes(__DIR__ . '/data/in-array-haystack-subtract.php');
994995
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4117.php');

tests/PHPStan/Analyser/data/bug-5668.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
class Foo
88
{
99

10-
1110
/**
1211
* @param array<int, 'test'|'bar'> $in
1312
*/
@@ -27,9 +26,11 @@ function has2(array $in): void
2726
/**
2827
* @param non-empty-array<int, 'test'|'bar'> $in
2928
*/
30-
function has3(array $in): void
29+
function has3(array $in, string $s): void
3130
{
3231
assertType('bool', in_array('test', $in, true));
32+
assertType('bool', in_array(rand() ? 'test' : 'bar', $in, true));
33+
assertType('bool', in_array($s, $in, true));
3334
}
3435

3536

@@ -41,4 +42,12 @@ function has4(array $in): void
4142
assertType('true', in_array('test', $in, true));
4243
}
4344

45+
/**
46+
* @param non-empty-array<int, string> $in
47+
*/
48+
function has5(array $in): void
49+
{
50+
assertType('bool', in_array('test', $in, true));
51+
}
52+
4453
}

tests/PHPStan/Analyser/data/in-array-enum.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,25 @@ enum FooUnitEnum
1515
class Foo
1616
{
1717

18+
/**
19+
* @param array<string> $strings
20+
* @param array<int> $ints
21+
*/
22+
public function nonConstantValues(FooUnitEnum $a, array $strings, array $ints): void
23+
{
24+
assertType('false', in_array($a, $strings, true));
25+
assertType('false', in_array($a, $strings, false));
26+
assertType('false', in_array($a, $strings));
27+
28+
assertType('bool', in_array($a->name, $strings, true));
29+
assertType('bool', in_array($a->name, $strings, false));
30+
assertType('bool', in_array($a->name, $strings));
31+
32+
assertType('false', in_array($a->name, $ints, true));
33+
assertType('bool', in_array($a->name, $ints, false));
34+
assertType('bool', in_array($a->name, $ints));
35+
}
36+
1837
public function looseCheckEnumSpecifyNeedle(mixed $v): void
1938
{
2039
if (in_array($v, FooUnitEnum::cases())) {

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace InArrayTypeSpecifyingExtension;
44

5+
use function PHPStan\Testing\assertType;
6+
57
class Foo
68
{
79

@@ -41,7 +43,19 @@ public function doFoo(
4143
return;
4244
}
4345

44-
die;
46+
assertType('\'bar\'|\'foo\'', $s);
47+
assertType('string', $mixed);
48+
assertType('string', $r);
49+
assertType('\'foo\'', $fooOrBarOrBaz);
50+
}
51+
52+
/** @param array<string> $strings */
53+
public function doBar(int $i, array $strings): void
54+
{
55+
assertType('bool', in_array($i, $strings));
56+
assertType('bool', in_array($i, $strings, false));
57+
assertType('false', in_array($i, $strings, true));
58+
assertType('false', in_array(1, $strings, true));
4559
}
4660

4761
}

tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php

Lines changed: 132 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
use PHPStan\Rules\Rule;
66
use PHPStan\Testing\RuleTestCase;
77
use stdClass;
8+
use function array_filter;
9+
use function array_map;
10+
use function array_values;
11+
use function count;
812
use const PHP_VERSION_ID;
913

1014
/**
@@ -252,6 +256,11 @@ public function testImpossibleCheckTypeFunctionCall(): void
252256
889,
253257
'Remove remaining cases below this one and this error will disappear too.',
254258
],
259+
[
260+
'Call to function in_array() with arguments 1, array<string> and true will always evaluate to false.',
261+
927,
262+
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
263+
],
255264
],
256265
);
257266
}
@@ -357,6 +366,11 @@ public function testImpossibleCheckTypeFunctionCallWithoutAlwaysTrue(): void
357366
'Call to function is_numeric() with \'blabla\' will always evaluate to false.',
358367
694,
359368
],
369+
[
370+
'Call to function in_array() with arguments 1, array<string> and true will always evaluate to false.',
371+
927,
372+
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
373+
],
360374
],
361375
);
362376
}
@@ -387,6 +401,16 @@ public function testReportTypesFromPhpDocs(): void
387401
19,
388402
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
389403
],
404+
[
405+
'Call to function in_array() with arguments int, array<string> and true will always evaluate to false.',
406+
27,
407+
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
408+
],
409+
[
410+
'Call to function in_array() with arguments 1, array<string> and true will always evaluate to false.',
411+
30,
412+
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
413+
],
390414
]);
391415
}
392416

@@ -513,6 +537,11 @@ public function testBugInArrayDateFormat(): void
513537
'Call to function in_array() with arguments int, array{} and true will always evaluate to false.',
514538
47,
515539
],
540+
[
541+
'Call to function in_array() with arguments int, array<int, string> and true will always evaluate to false.',
542+
61,
543+
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
544+
],
516545
]);
517546
}
518547

@@ -813,15 +842,11 @@ public function testObjectShapes(): void
813842
]);
814843
}
815844

816-
public function testLooseComparisonAgainstEnums(): void
845+
/** @return list<array{0: string, 1: int, 2?: string}> */
846+
private static function getLooseComparisonAgainsEnumsIssues(): array
817847
{
818-
if (PHP_VERSION_ID < 80100) {
819-
$this->markTestSkipped('Test requires PHP 8.1.');
820-
}
821-
822-
$this->checkAlwaysTrueCheckTypeFunctionCall = true;
823-
$this->treatPhpDocTypesAsCertain = true;
824-
$this->analyse([__DIR__ . '/data/loose-comparison-against-enums.php'], [
848+
$tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.';
849+
return [
825850
[
826851
'Call to function in_array() with LooseComparisonAgainstEnums\\FooUnitEnum and array{\'A\'} will always evaluate to false.',
827852
21,
@@ -910,7 +935,105 @@ public function testLooseComparisonAgainstEnums(): void
910935
'Call to function in_array() with null and array{LooseComparisonAgainstEnums\FooBackedEnum} will always evaluate to false.',
911936
96,
912937
],
913-
]);
938+
[
939+
'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum and array<string> will always evaluate to false.',
940+
125,
941+
$tipText,
942+
],
943+
[
944+
'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum, array<string> and false will always evaluate to false.',
945+
128,
946+
$tipText,
947+
],
948+
[
949+
'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum, array<string> and true will always evaluate to false.',
950+
131,
951+
$tipText,
952+
],
953+
[
954+
'Call to function in_array() with string and array<LooseComparisonAgainstEnums\FooUnitEnum> will always evaluate to false.',
955+
143,
956+
$tipText,
957+
],
958+
[
959+
'Call to function in_array() with arguments string, array<LooseComparisonAgainstEnums\FooUnitEnum> and false will always evaluate to false.',
960+
146,
961+
$tipText,
962+
],
963+
[
964+
'Call to function in_array() with arguments string, array<LooseComparisonAgainstEnums\FooUnitEnum> and true will always evaluate to false.',
965+
149,
966+
$tipText,
967+
],
968+
[
969+
'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum::B and non-empty-array<LooseComparisonAgainstEnums\FooUnitEnum::A> will always evaluate to false.',
970+
159,
971+
$tipText,
972+
],
973+
[
974+
'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum::A and non-empty-array<LooseComparisonAgainstEnums\FooUnitEnum::A> will always evaluate to true.',
975+
162,
976+
$tipText,
977+
],
978+
[
979+
'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::A, non-empty-array<LooseComparisonAgainstEnums\FooUnitEnum::A> and false will always evaluate to true.',
980+
165,
981+
'BUG',
982+
//$tipText,
983+
],
984+
[
985+
'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::A, non-empty-array<LooseComparisonAgainstEnums\FooUnitEnum::A> and true will always evaluate to true.',
986+
168,
987+
'BUG',
988+
//$tipText,
989+
],
990+
[
991+
'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::B, non-empty-array<LooseComparisonAgainstEnums\FooUnitEnum::A> and false will always evaluate to false.',
992+
171,
993+
'BUG',
994+
//$tipText,
995+
],
996+
[
997+
'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum::B, non-empty-array<LooseComparisonAgainstEnums\FooUnitEnum::A> and true will always evaluate to false.',
998+
174,
999+
'BUG',
1000+
//$tipText,
1001+
],
1002+
];
1003+
}
1004+
1005+
public function testLooseComparisonAgainstEnums(): void
1006+
{
1007+
if (PHP_VERSION_ID < 80100) {
1008+
$this->markTestSkipped('Test requires PHP 8.1.');
1009+
}
1010+
1011+
$this->checkAlwaysTrueCheckTypeFunctionCall = true;
1012+
$this->treatPhpDocTypesAsCertain = true;
1013+
$issues = array_map(
1014+
static function (array $i): array {
1015+
if (($i[2] ?? null) === 'BUG') {
1016+
unset($i[2]);
1017+
}
1018+
1019+
return $i;
1020+
},
1021+
self::getLooseComparisonAgainsEnumsIssues(),
1022+
);
1023+
$this->analyse([__DIR__ . '/data/loose-comparison-against-enums.php'], $issues);
1024+
}
1025+
1026+
public function testLooseComparisonAgainstEnumsNoPhpdoc(): void
1027+
{
1028+
if (PHP_VERSION_ID < 80100) {
1029+
$this->markTestSkipped('Test requires PHP 8.1.');
1030+
}
1031+
1032+
$this->checkAlwaysTrueCheckTypeFunctionCall = true;
1033+
$this->treatPhpDocTypesAsCertain = false;
1034+
$issues = self::getLooseComparisonAgainsEnumsIssues();
1035+
$issues = array_values(array_filter($issues, static fn (array $i) => count($i) === 2));
1036+
$this->analyse([__DIR__ . '/data/loose-comparison-against-enums.php'], $issues);
9141037
}
9151038

9161039
}

tests/PHPStan/Rules/Comparison/data/check-type-function-call-not-phpdoc.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,14 @@ public function doFoo(
2121
}
2222
}
2323

24+
/** @param array<string> $strings */
25+
public function checkInArray(int $i, array $strings): void
26+
{
27+
if (in_array($i, $strings, true)) {
28+
}
29+
30+
if (in_array(1, $strings, true)) {
31+
}
32+
}
33+
2434
}

0 commit comments

Comments
 (0)