Skip to content

Commit 80fdfab

Browse files
authored
Truthy isset($arr[$k]) should narrow $k
1 parent 8b91356 commit 80fdfab

File tree

3 files changed

+308
-0
lines changed

3 files changed

+308
-0
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,60 @@ public function specifyTypesInCondition(
818818
$rootExpr,
819819
),
820820
);
821+
} else {
822+
$varType = $scope->getType($var->var);
823+
if ($varType->isArray()->yes() && !$varType->isIterableAtLeastOnce()->no()) {
824+
$varIterableKeyType = $varType->getIterableKeyType();
825+
826+
if ($varIterableKeyType->isConstantScalarValue()->yes()) {
827+
$narrowedKey = TypeCombinator::union(
828+
$varIterableKeyType,
829+
TypeCombinator::remove($varIterableKeyType->toString(), new ConstantStringType('')),
830+
);
831+
832+
if (!$varType->hasOffsetValueType(new ConstantIntegerType(0))->no()) {
833+
$narrowedKey = TypeCombinator::union(
834+
$narrowedKey,
835+
new ConstantBooleanType(false),
836+
);
837+
}
838+
839+
if (!$varType->hasOffsetValueType(new ConstantIntegerType(1))->no()) {
840+
$narrowedKey = TypeCombinator::union(
841+
$narrowedKey,
842+
new ConstantBooleanType(true),
843+
);
844+
}
845+
846+
if (!$varType->hasOffsetValueType(new ConstantStringType(''))->no()) {
847+
$narrowedKey = TypeCombinator::addNull($narrowedKey);
848+
}
849+
850+
if (!$varIterableKeyType->isNumericString()->no() || !$varIterableKeyType->isInteger()->no()) {
851+
$narrowedKey = TypeCombinator::union($narrowedKey, new FloatType());
852+
}
853+
} else {
854+
$narrowedKey = new MixedType(
855+
false,
856+
new UnionType([
857+
new ArrayType(new MixedType(), new MixedType()),
858+
new ObjectWithoutClassType(),
859+
new ResourceType(),
860+
]),
861+
);
862+
}
863+
864+
$types = $types->unionWith(
865+
$this->create(
866+
$var->dim,
867+
$narrowedKey,
868+
$context,
869+
false,
870+
$scope,
871+
$rootExpr,
872+
),
873+
);
874+
}
821875
}
822876
}
823877

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
<?php // lint >= 8.0
2+
3+
namespace Bug11716;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class TypeExpression
8+
{
9+
/**
10+
* @return '&'|'|'
11+
*/
12+
public function parse(string $glue): string
13+
{
14+
$seenGlues = ['|' => false, '&' => false];
15+
16+
assertType("array{|: false, &: false}", $seenGlues);
17+
18+
if ($glue !== '') {
19+
assertType('non-empty-string', $glue);
20+
21+
\assert(isset($seenGlues[$glue]));
22+
$seenGlues[$glue] = true;
23+
24+
assertType("'&'|'|'", $glue);
25+
assertType('array{|: bool, &: bool}', $seenGlues);
26+
} else {
27+
assertType("''", $glue);
28+
}
29+
30+
assertType("''|'&'|'|'", $glue);
31+
assertType("array{|: bool, &: bool}", $seenGlues);
32+
33+
return array_key_first($seenGlues);
34+
}
35+
}
36+
37+
/**
38+
* @param array<int, string> $arr
39+
*/
40+
function narrowKey($mixed, string $s, int $i, array $generalArr, array $arr): void {
41+
if (isset($generalArr[$mixed])) {
42+
assertType('mixed~(array|object|resource)', $mixed);
43+
} else {
44+
assertType('mixed', $mixed);
45+
}
46+
assertType('mixed', $mixed);
47+
48+
if (isset($generalArr[$i])) {
49+
assertType('int', $i);
50+
} else {
51+
assertType('int', $i);
52+
}
53+
assertType('int', $i);
54+
55+
if (isset($generalArr[$s])) {
56+
assertType('string', $s);
57+
} else {
58+
assertType('string', $s);
59+
}
60+
assertType('string', $s);
61+
62+
if (isset($arr[$mixed])) {
63+
assertType('mixed~(array|object|resource)', $mixed);
64+
} else {
65+
assertType('mixed', $mixed);
66+
}
67+
assertType('mixed', $mixed);
68+
69+
if (isset($arr[$i])) {
70+
assertType('int', $i);
71+
} else {
72+
assertType('int', $i);
73+
}
74+
assertType('int', $i);
75+
76+
if (isset($arr[$s])) {
77+
assertType('string', $s);
78+
} else {
79+
assertType('string', $s);
80+
}
81+
assertType('string', $s);
82+
}
83+
84+
/**
85+
* @param array<int, array<string, float>> $arr
86+
*/
87+
function multiDim($mixed, $mixed2, array $arr) {
88+
if (isset($arr[$mixed])) {
89+
assertType('mixed~(array|object|resource)', $mixed);
90+
} else {
91+
assertType('mixed', $mixed);
92+
}
93+
assertType('mixed', $mixed);
94+
95+
if (isset($arr[$mixed]) && isset($arr[$mixed][$mixed2])) {
96+
assertType('mixed~(array|object|resource)', $mixed);
97+
assertType('mixed~(array|object|resource)', $mixed2);
98+
} else {
99+
assertType('mixed', $mixed);
100+
}
101+
assertType('mixed', $mixed);
102+
103+
if (isset($arr[$mixed][$mixed2])) {
104+
assertType('mixed~(array|object|resource)', $mixed);
105+
assertType('mixed~(array|object|resource)', $mixed2);
106+
} else {
107+
assertType('mixed', $mixed);
108+
assertType('mixed', $mixed2);
109+
}
110+
assertType('mixed', $mixed);
111+
assertType('mixed', $mixed2);
112+
}
113+
114+
/**
115+
* @param array<int, string> $arr
116+
*/
117+
function emptyArrr($mixed, array $arr)
118+
{
119+
if (count($arr) !== 0) {
120+
return;
121+
}
122+
123+
assertType('array{}', $arr);
124+
if (isset($arr[$mixed])) {
125+
assertType('mixed', $mixed);
126+
} else {
127+
assertType('mixed', $mixed);
128+
}
129+
assertType('mixed', $mixed);
130+
}
131+
132+
function emptyString($mixed)
133+
{
134+
// see https://3v4l.org/XHZdr
135+
$arr = ['' => 1, 'a' => 2];
136+
if (isset($arr[$mixed])) {
137+
assertType("''|'a'|null", $mixed);
138+
} else {
139+
assertType('mixed', $mixed); // could be mixed~(''|'a'|null)
140+
}
141+
assertType('mixed', $mixed);
142+
}
143+
144+
function numericString($mixed, int $i, string $s)
145+
{
146+
$arr = ['1' => 1, '2' => 2];
147+
if (isset($arr[$mixed])) {
148+
assertType("1|2|'1'|'2'|float|true", $mixed);
149+
} else {
150+
assertType('mixed', $mixed);
151+
}
152+
assertType('mixed', $mixed);
153+
154+
$arr = ['0' => 1, '2' => 2];
155+
if (isset($arr[$mixed])) {
156+
assertType("0|2|'0'|'2'|float|false", $mixed);
157+
} else {
158+
assertType('mixed', $mixed);
159+
}
160+
assertType('mixed', $mixed);
161+
162+
$arr = ['1' => 1, '2' => 2];
163+
if (isset($arr[$i])) {
164+
assertType("1|2", $i);
165+
} else {
166+
assertType('int', $i);
167+
}
168+
assertType('int', $i);
169+
170+
$arr = ['1' => 1, '2' => 2, 3 => 3];
171+
if (isset($arr[$s])) {
172+
assertType("'1'|'2'|'3'", $s);
173+
} else {
174+
assertType('string', $s);
175+
}
176+
assertType('string', $s);
177+
178+
$arr = ['1' => 1, '2' => 2, 3 => 3];
179+
if (isset($arr[substr($s, 10)])) {
180+
assertType("string", $s);
181+
assertType("'1'|'2'|'3'", substr($s, 10));
182+
} else {
183+
assertType('string', $s);
184+
}
185+
assertType('string', $s);
186+
}
187+
188+
function intKeys($mixed)
189+
{
190+
$arr = [1 => 1, 2 => 2];
191+
if (isset($arr[$mixed])) {
192+
assertType("1|2|'1'|'2'|float|true", $mixed);
193+
} else {
194+
assertType('mixed', $mixed);
195+
}
196+
assertType('mixed', $mixed);
197+
198+
$arr = [0 => 0, 1 => 1, 2 => 2];
199+
if (isset($arr[$mixed])) {
200+
assertType("0|1|2|'0'|'1'|'2'|bool|float", $mixed);
201+
} else {
202+
assertType('mixed', $mixed);
203+
}
204+
assertType('mixed', $mixed);
205+
}
206+
207+
function arrayAccess(\ArrayAccess $arr, $mixed) {
208+
if (isset($arr[$mixed])) {
209+
assertType("mixed", $mixed);
210+
} else {
211+
assertType('mixed', $mixed);
212+
}
213+
assertType('mixed', $mixed);
214+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace Bug8559;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class X
8+
{
9+
const KEYS = ['a' => 1, 'b' => 2];
10+
11+
/**
12+
* @phpstan-assert key-of<self::KEYS> $key
13+
* @return value-of<self::KEYS>
14+
*/
15+
public static function get(string $key): int
16+
{
17+
assert(isset(self::KEYS[$key]));
18+
assertType("'a'|'b'", $key);
19+
return self::KEYS[$key];
20+
}
21+
22+
/**
23+
* @phpstan-assert key-of<self::KEYS> $key
24+
* @return value-of<self::KEYS>
25+
*/
26+
public static function get2(string $key): int
27+
{
28+
assert(in_array($key, array_keys(self::KEYS), true));
29+
assertType("'a'|'b'", $key);
30+
return self::KEYS[$key];
31+
}
32+
}
33+
34+
$key = 'x';
35+
$v = X::get($key);
36+
assertType("*NEVER*", $key);
37+
38+
$key = 'a';
39+
$v = X::get($key);
40+
assertType("'a'", $key);

0 commit comments

Comments
 (0)