Skip to content

Commit 3012e24

Browse files
committed
reportUnsafeArrayStringKeyCasting - prevent implementation
1 parent bbf5afb commit 3012e24

9 files changed

+244
-12
lines changed

src/DependencyInjection/ValidateIgnoredErrorsExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ public function getRegistry(): UnaryOperatorTypeSpecifyingExtensionRegistry
139139
}
140140

141141
}, new OversizedArrayBuilder(), true),
142+
reportUnsafeArrayStringKeyCasting: null,
142143
),
143144
),
144145
);

src/PhpDoc/TypeNodeResolver.php

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
use PhpParser\Node\Name;
1111
use PHPStan\Analyser\ConstantResolver;
1212
use PHPStan\Analyser\NameScope;
13+
use PHPStan\DependencyInjection\AutowiredParameter;
1314
use PHPStan\DependencyInjection\AutowiredService;
15+
use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle;
1416
use PHPStan\PhpDoc\Tag\TemplateTag;
1517
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode;
1618
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode;
@@ -106,6 +108,7 @@
106108
use PHPStan\Type\TypeAliasResolver;
107109
use PHPStan\Type\TypeAliasResolverProvider;
108110
use PHPStan\Type\TypeCombinator;
111+
use PHPStan\Type\TypeTraverser;
109112
use PHPStan\Type\TypeUtils;
110113
use PHPStan\Type\UnionType;
111114
use PHPStan\Type\ValueOfType;
@@ -128,19 +131,27 @@
128131
use function strtolower;
129132
use function substr;
130133

134+
/**
135+
* @phpstan-import-type Level from ReportUnsafeArrayStringKeyCastingToggle as ReportUnsafeArrayStringKeyCastingLevel
136+
*/
131137
#[AutowiredService]
132138
final class TypeNodeResolver
133139
{
134140

135141
/** @var array<string, true> */
136142
private array $genericTypeResolvingStack = [];
137143

144+
/**
145+
* @param ReportUnsafeArrayStringKeyCastingLevel $reportUnsafeArrayStringKeyCasting
146+
*/
138147
public function __construct(
139148
private TypeNodeResolverExtensionRegistryProvider $extensionRegistryProvider,
140149
private ReflectionProvider\ReflectionProviderProvider $reflectionProviderProvider,
141150
private TypeAliasResolverProvider $typeAliasResolverProvider,
142151
private ConstantResolver $constantResolver,
143152
private InitializerExprTypeResolver $initializerExprTypeResolver,
153+
#[AutowiredParameter]
154+
private ?string $reportUnsafeArrayStringKeyCasting,
144155
)
145156
{
146157
}
@@ -661,7 +672,7 @@ private function resolveConditionalTypeForParameterNode(ConditionalTypeForParame
661672
private function resolveArrayTypeNode(ArrayTypeNode $typeNode, NameScope $nameScope): Type
662673
{
663674
$itemType = $this->resolve($typeNode->type, $nameScope);
664-
return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), $itemType);
675+
return new ArrayType((new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey(), $itemType);
665676
}
666677

667678
private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $nameScope): Type
@@ -686,9 +697,23 @@ static function (string $variance): TemplateTypeVariance {
686697

687698
if (in_array($mainTypeName, ['array', 'non-empty-array'], true)) {
688699
if (count($genericTypes) === 1) { // array<ValueType>
689-
$arrayType = new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), $genericTypes[0]);
700+
$arrayType = new ArrayType((new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey(), $genericTypes[0]);
690701
} elseif (count($genericTypes) === 2) { // array<KeyType, ValueType>
691-
$keyType = TypeCombinator::intersect($genericTypes[0]->toArrayKey(), new UnionType([
702+
$originalKey = $genericTypes[0];
703+
if ($this->reportUnsafeArrayStringKeyCasting === ReportUnsafeArrayStringKeyCastingToggle::PREVENT) {
704+
$originalKey = TypeTraverser::map($originalKey, static function (Type $type, callable $traverse) {
705+
if ($type instanceof UnionType || $type instanceof IntersectionType) {
706+
return $traverse($type);
707+
}
708+
709+
if ($type instanceof StringType) {
710+
return TypeCombinator::intersect($type, new AccessoryDecimalIntegerStringType(inverse: true));
711+
}
712+
713+
return $type;
714+
});
715+
}
716+
$keyType = TypeCombinator::intersect($originalKey->toArrayKey(), new UnionType([
692717
new IntegerType(),
693718
new StringType(),
694719
]))->toArrayKey();

src/Type/Accessory/AccessoryNumericStringType.php

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

33
namespace PHPStan\Type\Accessory;
44

5+
use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle;
56
use PHPStan\Php\PhpVersion;
67
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
78
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
@@ -209,12 +210,20 @@ public function toArray(): Type
209210

210211
public function toArrayKey(): Type
211212
{
213+
$level = ReportUnsafeArrayStringKeyCastingToggle::getLevel();
214+
if ($level !== ReportUnsafeArrayStringKeyCastingToggle::PREVENT) {
215+
new UnionType([
216+
new IntegerType(),
217+
new IntersectionType([
218+
new StringType(),
219+
new AccessoryNumericStringType(),
220+
]),
221+
]);
222+
}
223+
212224
return new UnionType([
213225
new IntegerType(),
214-
new IntersectionType([
215-
new StringType(),
216-
new AccessoryNumericStringType(),
217-
]),
226+
new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType(inverse: true)]),
218227
]);
219228
}
220229

src/Type/ArrayType.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
3535
use function array_merge;
3636
use function count;
37+
use function in_array;
3738
use function sprintf;
3839

3940
/** @api */
@@ -54,11 +55,11 @@ class ArrayType implements Type
5455
/** @api */
5556
public function __construct(Type $keyType, private Type $itemType)
5657
{
57-
if ($keyType->describe(VerbosityLevel::value()) === '(int|string)') {
58+
if (in_array($keyType->describe(VerbosityLevel::value()), ['(int|string)', '(int|non-decimal-int-string)'], true)) {
5859
$keyType = new MixedType();
5960
}
6061
if ($keyType instanceof StrictMixedType && !$keyType instanceof TemplateStrictMixedType) {
61-
$keyType = new UnionType([new StringType(), new IntegerType()]);
62+
$keyType = (new UnionType([new StringType(), new IntegerType()]))->toArrayKey();
6263
}
6364

6465
$this->keyType = $keyType;
@@ -207,10 +208,10 @@ public function getIterableKeyType(): Type
207208
}
208209
$keyType = $this->keyType;
209210
if ($keyType instanceof MixedType && !$keyType instanceof TemplateMixedType) {
210-
$keyType = new BenevolentUnionType([new IntegerType(), new StringType()]);
211+
$keyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
211212
}
212213
if ($keyType instanceof StrictMixedType) {
213-
$keyType = new BenevolentUnionType([new IntegerType(), new StringType()]);
214+
$keyType = (new BenevolentUnionType([new IntegerType(), new StringType()]))->toArrayKey();
214215
}
215216

216217
$level = ReportUnsafeArrayStringKeyCastingToggle::getLevel();

src/Type/StringType.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
namespace PHPStan\Type;
44

5+
use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle;
56
use PHPStan\Php\PhpVersion;
67
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
78
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
89
use PHPStan\Reflection\ReflectionProviderStaticAccessor;
910
use PHPStan\ShouldNotHappenException;
1011
use PHPStan\TrinaryLogic;
12+
use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType;
1113
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
1214
use PHPStan\Type\Constant\ConstantArrayType;
1315
use PHPStan\Type\Constant\ConstantBooleanType;
@@ -177,7 +179,15 @@ public function toArray(): Type
177179

178180
public function toArrayKey(): Type
179181
{
180-
return $this;
182+
$level = ReportUnsafeArrayStringKeyCastingToggle::getLevel();
183+
if ($level !== ReportUnsafeArrayStringKeyCastingToggle::PREVENT) {
184+
return $this;
185+
}
186+
187+
return new UnionType([
188+
new IntegerType(),
189+
TypeCombinator::intersect($this, new AccessoryDecimalIntegerStringType(inverse: true)),
190+
]);
181191
}
182192

183193
public function toCoercedArgumentType(bool $strictTypes): 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\Analyser;
4+
5+
use PHPStan\Rules\Methods\CallMethodsRule;
6+
use PHPStan\Rules\Rule;
7+
use PHPStan\Testing\RuleTestCase;
8+
use function array_merge;
9+
10+
/**
11+
* @extends RuleTestCase<CallMethodsRule>
12+
*/
13+
class ReportUnsafeArrayStringKeyCastingPreventTypeAcceptanceTest extends RuleTestCase
14+
{
15+
16+
public function getRule(): Rule
17+
{
18+
return self::getContainer()->getByType(CallMethodsRule::class);
19+
}
20+
21+
public function testRule(): void
22+
{
23+
$this->analyse([__DIR__ . '/data/report-unsafe-array-string-key-casting-accepts.php'], [
24+
[
25+
'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doFoo() expects array<non-decimal-int-string, stdClass>, non-empty-array<int|non-decimal-int-string, stdClass> given.',
26+
31,
27+
],
28+
[
29+
'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array<non-decimal-int-string, stdClass>, non-empty-array<int|non-decimal-int-string, stdClass> given.',
30+
33,
31+
],
32+
[
33+
'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doFoo() expects array<non-decimal-int-string, stdClass>, non-empty-array<int|non-decimal-int-string, stdClass> given.',
34+
37,
35+
],
36+
[
37+
'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array<non-decimal-int-string, stdClass>, non-empty-array<int|non-decimal-int-string, stdClass> given.',
38+
39,
39+
],
40+
]);
41+
}
42+
43+
public static function getAdditionalConfigFiles(): array
44+
{
45+
return array_merge(
46+
parent::getAdditionalConfigFiles(),
47+
[
48+
__DIR__ . '/reportUnsafeArrayStringKeyCastingPrevent.neon',
49+
],
50+
);
51+
}
52+
53+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
use PHPUnit\Framework\Attributes\DataProvider;
7+
use function array_merge;
8+
9+
class ReportUnsafeArrayStringKeyCastingPreventTypeInferenceTest extends TypeInferenceTestCase
10+
{
11+
12+
public static function dataAsserts(): iterable
13+
{
14+
yield from self::gatherAssertTypes(__DIR__ . '/data/report-unsafe-array-string-key-casting-prevent.php');
15+
}
16+
17+
/**
18+
* @param mixed ...$args
19+
*/
20+
#[DataProvider('dataAsserts')]
21+
public function testAsserts(
22+
string $assertType,
23+
string $file,
24+
...$args,
25+
): void
26+
{
27+
$this->assertFileAsserts($assertType, $file, ...$args);
28+
}
29+
30+
public static function getAdditionalConfigFiles(): array
31+
{
32+
return array_merge(
33+
parent::getAdditionalConfigFiles(),
34+
[
35+
__DIR__ . '/reportUnsafeArrayStringKeyCastingPrevent.neon',
36+
],
37+
);
38+
}
39+
40+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
namespace ReportUnsafeArrayStringKeyCastingPrevent;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
/**
11+
* @param array<string, self> $a
12+
*/
13+
public function doFoo(array $a): void
14+
{
15+
assertType('array<non-decimal-int-string, ReportUnsafeArrayStringKeyCastingPrevent\Foo>', $a);
16+
foreach ($a as $k => $v) {
17+
assertType('non-decimal-int-string', $k);
18+
}
19+
}
20+
21+
/**
22+
* @param array<self> $a
23+
*/
24+
public function doBar(array $a): void
25+
{
26+
assertType('array<ReportUnsafeArrayStringKeyCastingPrevent\Foo>', $a);
27+
foreach ($a as $k => $v) {
28+
assertType('(int|non-decimal-int-string)', $k);
29+
}
30+
}
31+
32+
/**
33+
* @param array<int|string, self> $a
34+
*/
35+
public function doBaz(array $a): void
36+
{
37+
assertType('array<int|non-decimal-int-string, ReportUnsafeArrayStringKeyCastingPrevent\Foo>', $a);
38+
foreach ($a as $k => $v) {
39+
assertType('int|non-decimal-int-string', $k);
40+
}
41+
}
42+
43+
public function doArrayCreationAndAssign(string $s): void
44+
{
45+
$a = [$s => 1];
46+
assertType('non-empty-array<int|non-decimal-int-string, 1>', $a);
47+
48+
$b = [];
49+
$b[$s] = 2;
50+
assertType('non-empty-array<int|non-decimal-int-string, 2>', $b);
51+
}
52+
53+
}
54+
55+
class FooNonDecimalIntString
56+
{
57+
58+
/**
59+
* @param array<non-decimal-int-string, self> $a
60+
*/
61+
public function doFoo(array $a): void
62+
{
63+
assertType('array<non-decimal-int-string, ReportUnsafeArrayStringKeyCastingPrevent\FooNonDecimalIntString>', $a);
64+
foreach ($a as $k => $v) {
65+
assertType('non-decimal-int-string', $k);
66+
}
67+
}
68+
69+
/**
70+
* @param array<int|non-decimal-int-string, self> $a
71+
*/
72+
public function doBaz(array $a): void
73+
{
74+
assertType('array<int|non-decimal-int-string, ReportUnsafeArrayStringKeyCastingPrevent\FooNonDecimalIntString>', $a);
75+
foreach ($a as $k => $v) {
76+
assertType('int|non-decimal-int-string', $k);
77+
}
78+
}
79+
80+
/** @param non-decimal-int-string $s */
81+
public function doArrayCreationAndAssign(string $s): void
82+
{
83+
$a = [$s => 1];
84+
assertType('non-empty-array<non-decimal-int-string, 1>', $a);
85+
86+
$b = [];
87+
$b[$s] = 2;
88+
assertType('non-empty-array<non-decimal-int-string, 2>', $b);
89+
}
90+
91+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
parameters:
2+
reportUnsafeArrayStringKeyCasting: prevent

0 commit comments

Comments
 (0)