Skip to content

Commit 9db66d0

Browse files
committed
Fix type inference for reportUnsafeArrayStringKeyCasting: detect
1 parent cf5b91e commit 9db66d0

4 files changed

Lines changed: 95 additions & 30 deletions

File tree

‎src/Type/ArrayType.php‎

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace PHPStan\Type;
44

5-
use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle;
65
use PHPStan\Php\PhpVersion;
76
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
87
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
@@ -13,7 +12,6 @@
1312
use PHPStan\ShouldNotHappenException;
1413
use PHPStan\TrinaryLogic;
1514
use PHPStan\Type\Accessory\AccessoryArrayListType;
16-
use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType;
1715
use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
1816
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
1917
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
@@ -37,6 +35,7 @@
3735
use PHPStan\Type\Traits\NonObjectTypeTrait;
3836
use PHPStan\Type\Traits\UndecidedBooleanTypeTrait;
3937
use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
38+
use PHPStan\Type\Traverser\UnsafeArrayStringKeyCastingTraverser;
4039
use function array_map;
4140
use function array_merge;
4241
use function count;
@@ -239,33 +238,7 @@ public function getIterableKeyType(): Type
239238
$keyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
240239
}
241240

242-
$level = ReportUnsafeArrayStringKeyCastingToggle::getLevel();
243-
if ($level === null) {
244-
return $this->cachedIterableKeyType = $keyType;
245-
}
246-
247-
if ($level === ReportUnsafeArrayStringKeyCastingToggle::PREVENT) {
248-
return $this->cachedIterableKeyType = $keyType;
249-
}
250-
251-
if ($level !== ReportUnsafeArrayStringKeyCastingToggle::DETECT) { // @phpstan-ignore notIdentical.alwaysFalse
252-
throw new ShouldNotHappenException();
253-
}
254-
255-
return $this->cachedIterableKeyType = TypeTraverser::map($keyType, static function (Type $type, callable $traverse): Type {
256-
if ($type instanceof UnionType) {
257-
return $traverse($type);
258-
}
259-
260-
if ($type->isString()->yes() && !$type->isDecimalIntegerString()->no()) {
261-
return TypeCombinator::union(
262-
new IntegerType(),
263-
TypeCombinator::intersect($type, new AccessoryDecimalIntegerStringType(inverse: true)),
264-
);
265-
}
266-
267-
return $type;
268-
});
241+
return $this->cachedIterableKeyType = UnsafeArrayStringKeyCastingTraverser::castKeyType($keyType);
269242
}
270243

271244
public function getFirstIterableKeyType(): Type

‎src/Type/Constant/ConstantArrayType.php‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
use PHPStan\Type\Traits\ArrayTypeTrait;
6262
use PHPStan\Type\Traits\NonObjectTypeTrait;
6363
use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
64+
use PHPStan\Type\Traverser\UnsafeArrayStringKeyCastingTraverser;
6465
use PHPStan\Type\Type;
6566
use PHPStan\Type\TypeCombinator;
6667
use PHPStan\Type\UnionType;
@@ -301,7 +302,7 @@ public function getIterableKeyType(): Type
301302
$keyType = TypeCombinator::union($keyType, $unsealedKeyType);
302303
}
303304

304-
return $this->iterableKeyType = $keyType;
305+
return $this->iterableKeyType = UnsafeArrayStringKeyCastingTraverser::castKeyType($keyType);
305306
}
306307

307308
public function getIterableValueType(): Type
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Traverser;
4+
5+
use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle;
6+
use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType;
7+
use PHPStan\Type\IntegerType;
8+
use PHPStan\Type\Type;
9+
use PHPStan\Type\TypeCombinator;
10+
use PHPStan\Type\TypeTraverser;
11+
use PHPStan\Type\TypeTraverserCallable;
12+
use PHPStan\Type\UnionType;
13+
14+
/**
15+
* Under `reportUnsafeArrayStringKeyCasting: detect`, PHP casts a decimal-integer
16+
* string array key ("123") to int when iterating, so the iterable key type widens
17+
* from `string` to `int | non-decimal-int-string`. Shared by ArrayType and
18+
* ConstantArrayType so both representations agree — otherwise comparing a general
19+
* array (cast key) against a constant-array shape (raw key) yields a spurious
20+
* `Maybe`.
21+
*/
22+
final class UnsafeArrayStringKeyCastingTraverser implements TypeTraverserCallable
23+
{
24+
25+
public static function castKeyType(Type $keyType): Type
26+
{
27+
if (ReportUnsafeArrayStringKeyCastingToggle::getLevel() !== ReportUnsafeArrayStringKeyCastingToggle::DETECT) {
28+
return $keyType;
29+
}
30+
31+
return TypeTraverser::map($keyType, new self());
32+
}
33+
34+
/**
35+
* @param callable(Type): Type $traverse
36+
*/
37+
public function traverse(Type $type, callable $traverse): Type
38+
{
39+
if ($type instanceof UnionType) {
40+
return $traverse($type);
41+
}
42+
43+
if ($type->isString()->yes() && !$type->isDecimalIntegerString()->no()) {
44+
return TypeCombinator::union(
45+
new IntegerType(),
46+
TypeCombinator::intersect($type, new AccessoryDecimalIntegerStringType(inverse: true)),
47+
);
48+
}
49+
50+
return $type;
51+
}
52+
53+
}

‎tests/PHPStan/Analyser/data/report-unsafe-array-string-key-casting-detect.php‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,41 @@ public function doArrayCreationAndAssign(string $s): void
8989
}
9090

9191
}
92+
93+
class Unsealed
94+
{
95+
96+
/**
97+
* @param array{a: self, ...<string, self>} $a
98+
*/
99+
public function doFoo(array $a): void
100+
{
101+
assertType('array{a: ReportUnsafeArrayStringKeyCastingDetect\Unsealed, ...<string, ReportUnsafeArrayStringKeyCastingDetect\Unsealed>}', $a);
102+
foreach ($a as $k => $v) {
103+
assertType('int|non-decimal-int-string', $k);
104+
}
105+
}
106+
107+
/**
108+
* @param array{a: self, ...<self>} $a
109+
*/
110+
public function doBar(array $a): void
111+
{
112+
assertType('array{a: ReportUnsafeArrayStringKeyCastingDetect\Unsealed, ...<ReportUnsafeArrayStringKeyCastingDetect\Unsealed>}', $a);
113+
foreach ($a as $k => $v) {
114+
assertType('(int|non-decimal-int-string)', $k);
115+
}
116+
}
117+
118+
/**
119+
* @param array{a: self, ...<int|string, self>} $a
120+
*/
121+
public function doBaz(array $a): void
122+
{
123+
assertType('array{a: ReportUnsafeArrayStringKeyCastingDetect\Unsealed, ...<int|string, ReportUnsafeArrayStringKeyCastingDetect\Unsealed>}', $a);
124+
foreach ($a as $k => $v) {
125+
assertType('int|non-decimal-int-string', $k);
126+
}
127+
}
128+
129+
}

0 commit comments

Comments
 (0)