Skip to content

Commit c6060a3

Browse files
committed
reportUnsafeArrayStringKeyCasting - detect implementation
1 parent c9460fa commit c6060a3

13 files changed

+338
-4
lines changed

conf/config.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ parameters:
8282
reportWrongPhpDocTypeInVarTag: false
8383
reportAnyTypeWideningInVarTag: false
8484
reportNonIntStringArrayKey: false
85+
reportUnsafeArrayStringKeyCasting: null
8586
reportPossiblyNonexistentGeneralArrayOffset: false
8687
reportPossiblyNonexistentConstantArrayOffset: false
8788
checkMissingOverrideMethodAttribute: false

conf/parametersSchema.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ parametersSchema:
9191
reportWrongPhpDocTypeInVarTag: bool()
9292
reportAnyTypeWideningInVarTag: bool()
9393
reportNonIntStringArrayKey: bool()
94+
reportUnsafeArrayStringKeyCasting: schema(string(), pattern('detect|prevent'), nullable())
9495
reportPossiblyNonexistentGeneralArrayOffset: bool()
9596
reportPossiblyNonexistentConstantArrayOffset: bool()
9697
checkMissingOverrideMethodAttribute: bool()

src/DependencyInjection/ContainerFactory.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ public static function postInitializeContainer(Container $container): void
202202
$container->getService('typeSpecifier');
203203

204204
BleedingEdgeToggle::setBleedingEdge($container->getParameter('featureToggles')['bleedingEdge']);
205+
ReportUnsafeArrayStringKeyCastingToggle::setLevel($container->getParameter('reportUnsafeArrayStringKeyCasting'));
205206
}
206207

207208
public function getCurrentWorkingDirectory(): string
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\DependencyInjection;
4+
5+
/**
6+
* @phpstan-type Level = self::DETECT|self::PREVENT|null
7+
*/
8+
final class ReportUnsafeArrayStringKeyCastingToggle
9+
{
10+
11+
public const DETECT = 'detect';
12+
13+
public const PREVENT = 'prevent';
14+
15+
/** @var Level */
16+
private static ?string $level = null;
17+
18+
/**
19+
* @return Level
20+
*/
21+
public static function getLevel(): ?string
22+
{
23+
return self::$level;
24+
}
25+
26+
/**
27+
* @param Level $level
28+
*/
29+
public static function setLevel(?string $level): void
30+
{
31+
self::$level = $level;
32+
}
33+
34+
}

src/Type/Accessory/AccessoryDecimalIntegerStringType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ public function toArray(): Type
246246
public function toArrayKey(): Type
247247
{
248248
if ($this->inverse) {
249-
return new StringType();
249+
return $this;
250250
}
251251

252252
return new IntegerType();

src/Type/ArrayType.php

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

33
namespace PHPStan\Type;
44

5+
use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle;
56
use PHPStan\Php\PhpVersion;
67
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
78
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
@@ -12,6 +13,7 @@
1213
use PHPStan\ShouldNotHappenException;
1314
use PHPStan\TrinaryLogic;
1415
use PHPStan\Type\Accessory\AccessoryArrayListType;
16+
use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType;
1517
use PHPStan\Type\Accessory\HasOffsetValueType;
1618
use PHPStan\Type\Accessory\NonEmptyArrayType;
1719
use PHPStan\Type\Constant\ConstantArrayType;
@@ -47,6 +49,8 @@ class ArrayType implements Type
4749

4850
private Type $keyType;
4951

52+
private ?Type $cachedIterableKeyType = null;
53+
5054
/** @api */
5155
public function __construct(Type $keyType, private Type $itemType)
5256
{
@@ -198,15 +202,44 @@ public function getArraySize(): Type
198202

199203
public function getIterableKeyType(): Type
200204
{
205+
if ($this->cachedIterableKeyType !== null) {
206+
return $this->cachedIterableKeyType;
207+
}
201208
$keyType = $this->keyType;
202209
if ($keyType instanceof MixedType && !$keyType instanceof TemplateMixedType) {
203-
return new BenevolentUnionType([new IntegerType(), new StringType()]);
210+
$keyType = new BenevolentUnionType([new IntegerType(), new StringType()]);
204211
}
205212
if ($keyType instanceof StrictMixedType) {
206-
return new BenevolentUnionType([new IntegerType(), new StringType()]);
213+
$keyType = new BenevolentUnionType([new IntegerType(), new StringType()]);
214+
}
215+
216+
$level = ReportUnsafeArrayStringKeyCastingToggle::getLevel();
217+
if ($level === null) {
218+
return $this->cachedIterableKeyType = $keyType;
219+
}
220+
221+
if ($level === ReportUnsafeArrayStringKeyCastingToggle::PREVENT) {
222+
return $this->cachedIterableKeyType = $keyType;
223+
}
224+
225+
if ($level !== ReportUnsafeArrayStringKeyCastingToggle::DETECT) { // @phpstan-ignore notIdentical.alwaysFalse
226+
throw new ShouldNotHappenException();
207227
}
208228

209-
return $keyType;
229+
return $this->cachedIterableKeyType = TypeTraverser::map($keyType, static function (Type $type, callable $traverse): Type {
230+
if ($type instanceof UnionType) {
231+
return $traverse($type);
232+
}
233+
234+
if ($type->isString()->yes() && !$type->isDecimalIntegerString()->no()) {
235+
return TypeCombinator::union(
236+
new IntegerType(),
237+
TypeCombinator::intersect($type, new AccessoryDecimalIntegerStringType(inverse: true)),
238+
);
239+
}
240+
241+
return $type;
242+
});
210243
}
211244

212245
public function getFirstIterableKeyType(): Type
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 ReportUnsafeArrayStringKeyCastingDetectTypeAcceptanceTest 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::doBaz() expects array<non-decimal-int-string, stdClass>, non-empty-array<string, stdClass> given.',
26+
33,
27+
],
28+
[
29+
'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array<non-decimal-int-string, stdClass>, non-empty-array<string, stdClass> given.',
30+
39,
31+
],
32+
]);
33+
}
34+
35+
public static function getAdditionalConfigFiles(): array
36+
{
37+
return array_merge(
38+
parent::getAdditionalConfigFiles(),
39+
[
40+
__DIR__ . '/reportUnsafeArrayStringKeyCastingDetect.neon',
41+
],
42+
);
43+
}
44+
45+
}
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 ReportUnsafeArrayStringKeyCastingDetectTypeInferenceTest extends TypeInferenceTestCase
10+
{
11+
12+
public static function dataAsserts(): iterable
13+
{
14+
yield from self::gatherAssertTypes(__DIR__ . '/data/report-unsafe-array-string-key-casting-detect.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__ . '/reportUnsafeArrayStringKeyCastingDetect.neon',
36+
],
37+
);
38+
}
39+
40+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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+
9+
/**
10+
* @extends RuleTestCase<CallMethodsRule>
11+
*/
12+
class ReportUnsafeArrayStringKeyCastingUnsafeTypeAcceptanceTest extends RuleTestCase
13+
{
14+
15+
public function getRule(): Rule
16+
{
17+
return self::getContainer()->getByType(CallMethodsRule::class);
18+
}
19+
20+
public function testRule(): void
21+
{
22+
$this->analyse([__DIR__ . '/data/report-unsafe-array-string-key-casting-accepts.php'], [
23+
[
24+
'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array<non-decimal-int-string, stdClass>, non-empty-array<string, stdClass> given.',
25+
33,
26+
],
27+
[
28+
'Parameter #1 $a of method ReportUnsafeArrayStringKeyCastingAccepts\Foo::doBaz() expects array<non-decimal-int-string, stdClass>, non-empty-array<string, stdClass> given.',
29+
39,
30+
],
31+
]);
32+
}
33+
34+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace ReportUnsafeArrayStringKeyCastingAccepts;
4+
5+
use stdClass;
6+
7+
class Foo
8+
{
9+
10+
/** @param array<string, stdClass> $a */
11+
public function doFoo(array $a): void
12+
{
13+
14+
}
15+
16+
/** @param array<int|string, stdClass> $a */
17+
public function doBar(array $a): void
18+
{
19+
20+
}
21+
22+
/** @param array<non-decimal-int-string, stdClass> $a */
23+
public function doBaz(array $a): void
24+
{
25+
26+
}
27+
28+
public function doTest(string $s): void
29+
{
30+
$a = [$s => new stdClass()];
31+
$this->doFoo($a);
32+
$this->doBar($a);
33+
$this->doBaz($a);
34+
35+
$b = [];
36+
$b[$s] = new stdClass();
37+
$this->doFoo($b);
38+
$this->doBar($b);
39+
$this->doBaz($b);
40+
}
41+
42+
}

0 commit comments

Comments
 (0)