Skip to content

Commit 042c4a1

Browse files
committed
Merge branch 2.1.x into 2.2.x
2 parents 56bfa4e + d2d6328 commit 042c4a1

2 files changed

Lines changed: 223 additions & 5 deletions

File tree

src/Analyser/NodeScopeResolver.php

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,21 +1319,30 @@ public function processStmtNode(
13191319
) {
13201320
$arrayExprDimFetch = new ArrayDimFetch($stmt->expr, $stmt->keyVar);
13211321
$arrayDimFetchLoopTypes = [];
1322+
$keyLoopTypes = [];
13221323
foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) {
13231324
$arrayDimFetchLoopTypes[] = $scopeWithIterableValueType->getType($arrayExprDimFetch);
1325+
$keyLoopTypes[] = $scopeWithIterableValueType->getType($stmt->keyVar);
13241326
}
13251327

13261328
$arrayDimFetchLoopType = TypeCombinator::union(...$arrayDimFetchLoopTypes);
1329+
$keyLoopType = TypeCombinator::union(...$keyLoopTypes);
13271330

13281331
$arrayDimFetchLoopNativeTypes = [];
1332+
$keyLoopNativeTypes = [];
13291333
foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) {
13301334
$arrayDimFetchLoopNativeTypes[] = $scopeWithIterableValueType->getNativeType($arrayExprDimFetch);
1335+
$keyLoopNativeTypes[] = $scopeWithIterableValueType->getNativeType($stmt->keyVar);
13311336
}
13321337

13331338
$arrayDimFetchLoopNativeType = TypeCombinator::union(...$arrayDimFetchLoopNativeTypes);
1339+
$keyLoopNativeType = TypeCombinator::union(...$keyLoopNativeTypes);
13341340

1335-
if (!$arrayDimFetchLoopType->equals($exprType->getIterableValueType())) {
1336-
$newExprType = TypeTraverser::map($exprType, static function (Type $type, callable $traverse) use ($arrayDimFetchLoopType): Type {
1341+
$valueTypeChanged = !$arrayDimFetchLoopType->equals($exprType->getIterableValueType());
1342+
$keyTypeChanged = !$keyLoopType->equals($exprType->getIterableKeyType());
1343+
1344+
if ($valueTypeChanged || $keyTypeChanged) {
1345+
$newExprType = TypeTraverser::map($exprType, static function (Type $type, callable $traverse) use ($arrayDimFetchLoopType, $keyLoopType, $valueTypeChanged, $keyTypeChanged): Type {
13371346
if ($type instanceof UnionType || $type instanceof IntersectionType) {
13381347
return $traverse($type);
13391348
}
@@ -1342,9 +1351,13 @@ public function processStmtNode(
13421351
return $type;
13431352
}
13441353

1345-
return new ArrayType($type->getKeyType(), $arrayDimFetchLoopType);
1354+
return new ArrayType(
1355+
$keyTypeChanged ? $keyLoopType : $type->getKeyType(),
1356+
$valueTypeChanged ? $arrayDimFetchLoopType : $type->getIterableValueType(),
1357+
);
13461358
});
1347-
$newExprNativeType = TypeTraverser::map($scope->getNativeType($stmt->expr), static function (Type $type, callable $traverse) use ($arrayDimFetchLoopNativeType): Type {
1359+
$nativeExprType = $scope->getNativeType($stmt->expr);
1360+
$newExprNativeType = TypeTraverser::map($nativeExprType, static function (Type $type, callable $traverse) use ($arrayDimFetchLoopNativeType, $keyLoopNativeType, $valueTypeChanged, $keyTypeChanged): Type {
13481361
if ($type instanceof UnionType || $type instanceof IntersectionType) {
13491362
return $traverse($type);
13501363
}
@@ -1353,7 +1366,10 @@ public function processStmtNode(
13531366
return $type;
13541367
}
13551368

1356-
return new ArrayType($type->getKeyType(), $arrayDimFetchLoopNativeType);
1369+
return new ArrayType(
1370+
$keyTypeChanged ? $keyLoopNativeType : $type->getKeyType(),
1371+
$valueTypeChanged ? $arrayDimFetchLoopNativeType : $type->getIterableValueType(),
1372+
);
13571373
});
13581374

13591375
if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
<?php
2+
3+
namespace Bug7076;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @param array<int|string, mixed> $arguments
9+
* @return array<string, mixed>
10+
*/
11+
function narrowWithIsString(array $arguments): array
12+
{
13+
foreach ($arguments as $key => $argument) {
14+
if (!is_string($key)) {
15+
throw new \Exception('Key must be a string');
16+
}
17+
}
18+
19+
assertType('array<string, mixed>', $arguments);
20+
21+
return $arguments;
22+
}
23+
24+
/**
25+
* @param array<int|string, mixed> $arguments
26+
* @return array<string, mixed>
27+
*/
28+
function narrowWithIsInt(array $arguments): array
29+
{
30+
foreach ($arguments as $key => $argument) {
31+
if (is_int($key)) {
32+
throw new \Exception('Key must be a string');
33+
}
34+
}
35+
36+
assertType('array<string, mixed>', $arguments);
37+
38+
return $arguments;
39+
}
40+
41+
/**
42+
* @param array<int|string, mixed> $arguments
43+
*/
44+
function narrowToIntKeys(array $arguments): void
45+
{
46+
foreach ($arguments as $key => $argument) {
47+
if (!is_int($key)) {
48+
throw new \Exception('Key must be an int');
49+
}
50+
}
51+
52+
assertType('array<int, mixed>', $arguments);
53+
}
54+
55+
/**
56+
* @param array<int|string, mixed> $arguments
57+
*/
58+
function narrowWithReturn(array $arguments): void
59+
{
60+
foreach ($arguments as $key => $argument) {
61+
if (!is_string($key)) {
62+
return;
63+
}
64+
}
65+
66+
assertType('array<string, mixed>', $arguments);
67+
}
68+
69+
/**
70+
* @param array<int|string, mixed> $arguments
71+
*/
72+
function continueDoesNotNarrow(array $arguments): void
73+
{
74+
foreach ($arguments as $key => $argument) {
75+
if (!is_string($key)) {
76+
continue;
77+
}
78+
}
79+
80+
assertType('array<int|string, mixed>', $arguments);
81+
}
82+
83+
/**
84+
* @param array<int|string, mixed> $arguments
85+
*/
86+
function breakPreventsNarrowing(array $arguments): void
87+
{
88+
foreach ($arguments as $key => $argument) {
89+
if (!is_string($key)) {
90+
throw new \Exception();
91+
}
92+
if (rand(0, 1)) {
93+
break;
94+
}
95+
}
96+
97+
assertType('array<int|string, mixed>', $arguments);
98+
}
99+
100+
/**
101+
* @param array<int|string, string|null> $arguments
102+
*/
103+
function keyAndValueNarrowing(array $arguments): void
104+
{
105+
foreach ($arguments as $key => $argument) {
106+
if (!is_string($key)) {
107+
throw new \Exception();
108+
}
109+
$arguments[$key] = $argument ?? '';
110+
}
111+
112+
assertType('array<string, string>', $arguments);
113+
}
114+
115+
/**
116+
* @param array<int|string, mixed> $arguments
117+
*/
118+
function noKeyVar(array $arguments): void
119+
{
120+
foreach ($arguments as $argument) {
121+
if (!is_string($argument)) {
122+
throw new \Exception();
123+
}
124+
}
125+
126+
assertType('array<int|string, mixed>', $arguments);
127+
}
128+
129+
/**
130+
* @param array<int|string, mixed> $arguments
131+
*/
132+
function keyReassignedPreventsNarrowing(array $arguments): void
133+
{
134+
foreach ($arguments as $key => $argument) {
135+
$key = 'test';
136+
if (!is_string($key)) {
137+
throw new \Exception();
138+
}
139+
}
140+
141+
assertType('array<int|string, mixed>', $arguments);
142+
}
143+
144+
/**
145+
* @param array<int|string, mixed> $arguments
146+
*/
147+
function narrowWithAssert(array $arguments): void
148+
{
149+
foreach ($arguments as $key => $argument) {
150+
assert(is_string($key));
151+
}
152+
153+
assertType('array<string, mixed>', $arguments);
154+
}
155+
156+
/**
157+
* @param non-empty-array<int|string, mixed> $arguments
158+
*/
159+
function narrowNonEmptyArray(array $arguments): void
160+
{
161+
foreach ($arguments as $key => $argument) {
162+
if (!is_string($key)) {
163+
throw new \Exception();
164+
}
165+
}
166+
167+
assertType('non-empty-array<string, mixed>', $arguments);
168+
}
169+
170+
class Foo
171+
{
172+
/** @var array<int|string, mixed> */
173+
private array $prop;
174+
175+
public function narrowPropertyKey(): void
176+
{
177+
foreach ($this->prop as $k => $v) {
178+
if (!is_string($k)) {
179+
throw new \Exception();
180+
}
181+
}
182+
183+
assertType('array<string, mixed>', $this->prop);
184+
}
185+
}
186+
187+
/**
188+
* @param array<int|string, mixed> $arguments
189+
*/
190+
function partialContinueNarrowingDoesNotApply(array $arguments): void
191+
{
192+
foreach ($arguments as $key => $argument) {
193+
if (rand(0, 1)) {
194+
continue;
195+
}
196+
if (!is_string($key)) {
197+
throw new \Exception();
198+
}
199+
}
200+
201+
assertType('array<int|string, mixed>', $arguments);
202+
}

0 commit comments

Comments
 (0)