Skip to content

Commit 5cd44d5

Browse files
committed
Merge branch 2.1.x into 2.2.x
2 parents fc48b3e + fd42dc7 commit 5cd44d5

File tree

6 files changed

+251
-11
lines changed

6 files changed

+251
-11
lines changed

phpstan-baseline.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1587,6 +1587,12 @@ parameters:
15871587
count: 1
15881588
path: src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php
15891589

1590+
-
1591+
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantArrayType is error-prone and deprecated. Use Type::getConstantArrays() instead.'
1592+
identifier: phpstanApi.instanceofType
1593+
count: 1
1594+
path: src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php
1595+
15901596
-
15911597
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.'
15921598
identifier: phpstanApi.instanceofType

src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
use PHPStan\Reflection\FunctionReflection;
1717
use PHPStan\Type\Accessory\NonEmptyArrayType;
1818
use PHPStan\Type\ArrayType;
19+
use PHPStan\Type\Constant\ConstantArrayType;
1920
use PHPStan\Type\FunctionTypeSpecifyingExtension;
2021
use PHPStan\Type\MixedType;
22+
use PHPStan\Type\Type;
2123
use PHPStan\Type\TypeCombinator;
2224
use function count;
2325
use function strtolower;
@@ -113,25 +115,18 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
113115
}
114116

115117
$specifiedTypes = new SpecifiedTypes();
116-
if (
117-
$context->true()
118-
|| (
119-
$context->false()
120-
&& count($arrayValueType->getFiniteTypes()) > 0
121-
&& count($needleType->getFiniteTypes()) > 0
122-
&& $arrayType->isIterableAtLeastOnce()->yes()
123-
)
124-
) {
118+
$narrowingValueType = $this->computeNeedleNarrowingType($context, $needleType, $arrayType, $arrayValueType);
119+
if ($narrowingValueType !== null) {
125120
$specifiedTypes = $this->typeSpecifier->create(
126121
$needleExpr,
127-
$arrayValueType,
122+
$narrowingValueType,
128123
$context,
129124
$scope,
130125
);
131126
if ($needleExpr instanceof AlwaysRememberedExpr) {
132127
$specifiedTypes = $specifiedTypes->unionWith($this->typeSpecifier->create(
133128
$needleExpr->getExpr(),
134-
$arrayValueType,
129+
$narrowingValueType,
135130
$context,
136131
$scope,
137132
));
@@ -171,4 +166,68 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n
171166
return $specifiedTypes;
172167
}
173168

169+
/**
170+
* Computes the type to narrow the needle against, or null if no narrowing should occur.
171+
* In true context, returns the array value type directly.
172+
* In false context, returns only the values guaranteed to be in every possible variant of the array.
173+
*/
174+
private function computeNeedleNarrowingType(TypeSpecifierContext $context, Type $needleType, Type $arrayType, Type $arrayValueType): ?Type
175+
{
176+
if ($context->true()) {
177+
return $arrayValueType;
178+
}
179+
180+
if (
181+
!$context->false()
182+
|| count($needleType->getFiniteTypes()) === 0
183+
|| !$arrayType->isIterableAtLeastOnce()->yes()
184+
) {
185+
return null;
186+
}
187+
188+
$arrays = $arrayType->getArrays();
189+
$guaranteedValueTypePerArray = [];
190+
foreach ($arrays as $array) {
191+
if ($array instanceof ConstantArrayType) {
192+
$innerGuaranteeValueType = [];
193+
foreach ($array->getValueTypes() as $i => $valueType) {
194+
if ($array->isOptionalKey($i)) {
195+
continue;
196+
}
197+
198+
$finiteTypes = $valueType->getFiniteTypes();
199+
if (count($finiteTypes) !== 1) {
200+
continue;
201+
}
202+
203+
$innerGuaranteeValueType[] = $finiteTypes[0];
204+
}
205+
206+
if (count($innerGuaranteeValueType) === 0) {
207+
return null;
208+
}
209+
210+
$guaranteedValueTypePerArray[] = TypeCombinator::union(...$innerGuaranteeValueType);
211+
} else {
212+
$finiteValueType = $array->getIterableValueType()->getFiniteTypes();
213+
if (count($finiteValueType) !== 1) {
214+
return null;
215+
}
216+
217+
$guaranteedValueTypePerArray[] = $finiteValueType[0];
218+
}
219+
}
220+
221+
if (count($guaranteedValueTypePerArray) === 0) {
222+
return null;
223+
}
224+
225+
$guaranteedValueType = TypeCombinator::intersect(...$guaranteedValueTypePerArray);
226+
if (count($guaranteedValueType->getFiniteTypes()) === 0) {
227+
return null;
228+
}
229+
230+
return $guaranteedValueType;
231+
}
232+
174233
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug13421;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
enum Bar
10+
{
11+
case Bar1;
12+
case Bar2;
13+
case Bar3;
14+
}
15+
16+
/**
17+
* @param non-empty-array<Bar> $nonEmptyFilterArray
18+
*/
19+
function test(array $nonEmptyFilterArray): void
20+
{
21+
$bars = [Bar::Bar1, Bar::Bar2, Bar::Bar3];
22+
23+
$filteredBars = array_filter($bars, fn (Bar $bar) => in_array($bar, $nonEmptyFilterArray));
24+
25+
assertType("array{0?: Bug13421\Bar::Bar1, 1?: Bug13421\Bar::Bar2, 2?: Bug13421\Bar::Bar3}", $filteredBars);
26+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug14407;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
enum SomeEnum {
10+
case A;
11+
case B;
12+
}
13+
14+
function () {
15+
$arr = [];
16+
17+
if (rand(0, 1) === 0) {
18+
$arr[] = SomeEnum::A;
19+
} else {
20+
$arr[] = SomeEnum::B;
21+
}
22+
23+
if (rand(0, 1) === 0) {
24+
$x = SomeEnum::A;
25+
} else {
26+
$x = SomeEnum::B;
27+
}
28+
29+
if (!in_array($x, $arr)) {
30+
// either $x=A, $arr=[B] or $x=B, $arr=[A]
31+
assertType('Bug14407\SomeEnum::A|Bug14407\SomeEnum::B', $x);
32+
}
33+
34+
if (!in_array($x, $arr) && $x === SomeEnum::A) {
35+
// $x=A, $arr=[B]
36+
assertType('Bug14407\SomeEnum::A', $x);
37+
}
38+
};
39+
40+
function () {
41+
$arr = [SomeEnum::A, SomeEnum::B];
42+
43+
if (rand(0, 1) === 0) {
44+
$x = SomeEnum::A;
45+
} else {
46+
$x = SomeEnum::B;
47+
}
48+
49+
if (!in_array($x, $arr)) {
50+
// array always contains both A and B, so this is correctly *NEVER*
51+
assertType('*NEVER*', $x);
52+
}
53+
};
54+
55+
function () {
56+
$arr = [];
57+
58+
$r = rand(0, 2);
59+
60+
if ($r === 0) {
61+
$arr[] = SomeEnum::A;
62+
} elseif ($r === 1) {
63+
$arr[] = SomeEnum::B;
64+
}
65+
66+
if (rand(0, 1) === 0) {
67+
$x = SomeEnum::A;
68+
} else {
69+
$x = SomeEnum::B;
70+
}
71+
72+
// arr might be empty, so no narrowing possible
73+
if (!in_array($x, $arr) && $x === SomeEnum::A) {
74+
assertType('Bug14407\SomeEnum::A', $x);
75+
}
76+
};
77+
78+
/**
79+
* @param 'a'|'b'|'c' $x
80+
* @param array{a: 'a', c: 'c'}|array{a?:'a', b: 'b'} $a
81+
*/
82+
function testUnionWithOptionalKeys($x, $a): void
83+
{
84+
assertType("array{a: 'a', c: 'c'}|array{a?: 'a', b: 'b'}", $a);
85+
if (!\in_array($x, $a, true)) {
86+
assertType("'a'|'b'|'c'", $x);
87+
}
88+
};
89+
90+
/**
91+
* @param 'a'|'b'|'c' $x
92+
* @param non-empty-array<'a'|'b'> $a
93+
*/
94+
function testNonConstantArray($x, $a): void
95+
{
96+
assertType("non-empty-array<'a'|'b'>", $a);
97+
if (!\in_array($x, $a, true)) {
98+
assertType("'a'|'b'|'c'", $x);
99+
}
100+
};
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug8864;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* @param array{0: 1, 1?: 2} $a
11+
*/
12+
function test(array $a): void
13+
{
14+
if (in_array(2, $a, true)) {
15+
assertType('array{0: 1, 1?: 2}', $a);
16+
}
17+
18+
if (!in_array(2, $a, true)) {
19+
assertType('array{0: 1, 1?: *NEVER*}', $a);
20+
}
21+
}
22+
23+
/**
24+
* @param 1|2 $x
25+
* @param array{0: 1, 1?: 2} $a
26+
*/
27+
function testNeedle($x, array $a): void
28+
{
29+
if (in_array($x, $a, true)) {
30+
assertType('1|2', $x);
31+
}
32+
33+
if (!in_array($x, $a, true)) {
34+
// 1 is guaranteed in the array, so if not in_array, x must be 2
35+
assertType('2', $x);
36+
}
37+
}

tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,4 +1204,16 @@ public function testBug11054(): void
12041204
]);
12051205
}
12061206

1207+
#[RequiresPhp('>= 8.1')]
1208+
public function testBug14407(): void
1209+
{
1210+
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14407.php'], []);
1211+
}
1212+
1213+
#[RequiresPhp('>= 8.1')]
1214+
public function testBug13421(): void
1215+
{
1216+
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13421.php'], []);
1217+
}
1218+
12071219
}

0 commit comments

Comments
 (0)