Skip to content

Commit 2a8d675

Browse files
authored
Detect invalid key in multi dimensional array fetch
1 parent a95dd4a commit 2a8d675

8 files changed

+166
-3
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@
156156
use function array_fill_keys;
157157
use function array_filter;
158158
use function array_key_exists;
159+
use function array_key_last;
159160
use function array_keys;
160161
use function array_map;
161162
use function array_merge;
@@ -3593,7 +3594,7 @@ private function processAssignVar(
35933594
$scope = $scope->addConditionalExpressions($exprString, $holders);
35943595
}
35953596
} elseif ($var instanceof ArrayDimFetch) {
3596-
$dimExprStack = [];
3597+
$dimFetchStack = [];
35973598
$originalVar = $var;
35983599
$assignedPropertyExpr = $assignedExpr;
35993600
while ($var instanceof ArrayDimFetch) {
@@ -3606,7 +3607,7 @@ private function processAssignVar(
36063607
$var->dim,
36073608
$assignedPropertyExpr,
36083609
);
3609-
$dimExprStack[] = $var->dim;
3610+
$dimFetchStack[] = $var;
36103611
$var = $var->var;
36113612
}
36123613

@@ -3625,7 +3626,16 @@ private function processAssignVar(
36253626
// 2. eval dimensions
36263627
$offsetTypes = [];
36273628
$offsetNativeTypes = [];
3628-
foreach (array_reverse($dimExprStack) as $dimExpr) {
3629+
$dimFetchStack = array_reverse($dimFetchStack);
3630+
$lastDimKey = array_key_last($dimFetchStack);
3631+
foreach ($dimFetchStack as $key => $dimFetch) {
3632+
$dimExpr = $dimFetch->dim;
3633+
3634+
// Callback was already called for last dim at the beginning of the method.
3635+
if ($key !== $lastDimKey) {
3636+
$nodeCallback($dimFetch, $enterExpressionAssign ? $scope->enterExpressionAssign($dimFetch) : $scope);
3637+
}
3638+
36293639
if ($dimExpr === null) {
36303640
$offsetTypes[] = null;
36313641
$offsetNativeTypes[] = null;

tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHPStan\Rules\Rule;
66
use PHPStan\Rules\RuleLevelHelper;
77
use PHPStan\Testing\RuleTestCase;
8+
use const PHP_VERSION_ID;
89

910
/**
1011
* @extends RuleTestCase<InvalidKeyInArrayDimFetchRule>
@@ -37,6 +38,60 @@ public function testInvalidKey(): void
3738
'Invalid array key type DateTimeImmutable.',
3839
31,
3940
],
41+
[
42+
'Invalid array key type DateTimeImmutable.',
43+
45,
44+
],
45+
[
46+
'Invalid array key type DateTimeImmutable.',
47+
46,
48+
],
49+
[
50+
'Invalid array key type DateTimeImmutable.',
51+
47,
52+
],
53+
[
54+
'Invalid array key type stdClass.',
55+
47,
56+
],
57+
[
58+
'Invalid array key type DateTimeImmutable.',
59+
48,
60+
],
61+
]);
62+
}
63+
64+
public function testBug6315(): void
65+
{
66+
if (PHP_VERSION_ID < 80100) {
67+
$this->markTestSkipped('Test requires PHP 8.1.');
68+
}
69+
70+
$this->analyse([__DIR__ . '/data/bug-6315.php'], [
71+
[
72+
'Invalid array key type Bug6315\FooEnum::A.',
73+
18,
74+
],
75+
[
76+
'Invalid array key type Bug6315\FooEnum::A.',
77+
19,
78+
],
79+
[
80+
'Invalid array key type Bug6315\FooEnum::A.',
81+
20,
82+
],
83+
[
84+
'Invalid array key type Bug6315\FooEnum::B.',
85+
21,
86+
],
87+
[
88+
'Invalid array key type Bug6315\FooEnum::A.',
89+
21,
90+
],
91+
[
92+
'Invalid array key type Bug6315\FooEnum::A.',
93+
22,
94+
],
4095
]);
4196
}
4297

tests/PHPStan/Rules/Arrays/InvalidKeyInArrayItemRuleTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPStan\Rules\Rule;
66
use PHPStan\Testing\RuleTestCase;
7+
use const PHP_VERSION_ID;
78

89
/**
910
* @extends RuleTestCase<InvalidKeyInArrayItemRule>
@@ -62,4 +63,18 @@ public function testInvalidKeyShortArray(): void
6263
]);
6364
}
6465

66+
public function testInvalidKeyEnum(): void
67+
{
68+
if (PHP_VERSION_ID < 80100) {
69+
$this->markTestSkipped('Test requires PHP 8.1.');
70+
}
71+
72+
$this->analyse([__DIR__ . '/data/invalid-key-array-item-enum.php'], [
73+
[
74+
'Invalid array key type InvalidKeyArrayItemEnum\FooEnum::A.',
75+
14,
76+
],
77+
]);
78+
}
79+
6580
}

tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,4 +711,22 @@ public function testBug8356(): void
711711
]);
712712
}
713713

714+
public function testBug6605(): void
715+
{
716+
$this->analyse([__DIR__ . '/data/bug-6605.php'], [
717+
[
718+
"Cannot access offset 'invalidoffset' on Bug6605\\X.",
719+
11,
720+
],
721+
[
722+
"Offset 'invalid' does not exist on array{a: array{b: array{5}}}.",
723+
16,
724+
],
725+
[
726+
"Offset 'invalid' does not exist on array{b: array{5}}.",
727+
17,
728+
],
729+
]);
730+
}
731+
714732
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php // lint >= 8.1
2+
3+
namespace Bug6315;
4+
5+
enum FooEnum
6+
{
7+
case A;
8+
case B;
9+
}
10+
11+
/**
12+
* @param array<int, int> $flatArr
13+
* @param array<int, array<int, int>> $deepArr
14+
* @return void
15+
*/
16+
function foo(array $flatArr, array $deepArr): void
17+
{
18+
var_dump($flatArr[FooEnum::A]);
19+
var_dump($deepArr[FooEnum::A][5]);
20+
var_dump($deepArr[5][FooEnum::A]);
21+
var_dump($deepArr[FooEnum::A][FooEnum::B]);
22+
$deepArr[FooEnum::A][] = 5;
23+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bug6605;
6+
7+
class X {
8+
public static function doFoo(): void
9+
{
10+
$x = new X;
11+
$x['invalidoffset'][0] = [
12+
'foo' => 'bar'
13+
];
14+
15+
$arr = ['a' => ['b' => [5]]];
16+
var_dump($arr['invalid']['c']);
17+
var_dump($arr['a']['invalid']);
18+
}
19+
}

tests/PHPStan/Rules/Arrays/data/invalid-key-array-dim-fetch.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,10 @@
3939
/** @var mixed $mixed */
4040
$mixed = null;
4141
$a[$mixed];
42+
43+
/** @var array<int, array<int, int>> $array */
44+
$array = doFoo();
45+
$array[new \DateTimeImmutable()][5];
46+
$array[5][new \DateTimeImmutable()];
47+
$array[new \stdClass()][new \DateTimeImmutable()];
48+
$array[new \DateTimeImmutable()][] = 5;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php // lint >= 8.1
2+
3+
namespace InvalidKeyArrayItemEnum;
4+
5+
enum FooEnum
6+
{
7+
case A;
8+
case B;
9+
}
10+
11+
function doFoo(): void
12+
{
13+
$a = [
14+
FooEnum::A => 5,
15+
];
16+
}

0 commit comments

Comments
 (0)