Skip to content

Commit aae34d9

Browse files
committed
Bleeding edge - check array deconstruction
1 parent 3f7c015 commit aae34d9

10 files changed

+259
-1
lines changed

conf/bleedingEdge.neon

+1
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ parameters:
1313
checkLogicalOrConstantCondition: true
1414
checkMissingTemplateTypeInParameter: true
1515
wrongVarUsage: true
16+
arrayDeconstruction: true

conf/config.level3.neon

+7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ rules:
1717
- PHPStan\Rules\Variables\ThrowTypeRule
1818
- PHPStan\Rules\Variables\VariableCloningRule
1919

20+
conditionalTags:
21+
PHPStan\Rules\Arrays\ArrayDeconstructionRule:
22+
phpstan.rules.rule: %featureToggles.arrayDeconstruction%
23+
2024
parameters:
2125
checkPhpDocMethodSignatures: true
2226

@@ -28,6 +32,9 @@ services:
2832
tags:
2933
- phpstan.rules.rule
3034

35+
-
36+
class: PHPStan\Rules\Arrays\ArrayDeconstructionRule
37+
3138
-
3239
class: PHPStan\Rules\Arrays\InvalidKeyInArrayDimFetchRule
3340
arguments:

conf/config.neon

+3-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ parameters:
2626
checkLogicalOrConstantCondition: false
2727
checkMissingTemplateTypeInParameter: false
2828
wrongVarUsage: false
29+
arrayDeconstruction: false
2930
fileExtensions:
3031
- php
3132
checkAlwaysTrueCheckTypeFunctionCall: false
@@ -178,7 +179,8 @@ parametersSchema:
178179
checkLogicalAndConstantCondition: bool(),
179180
checkLogicalOrConstantCondition: bool(),
180181
checkMissingTemplateTypeInParameter: bool(),
181-
wrongVarUsage: bool()
182+
wrongVarUsage: bool(),
183+
arrayDeconstruction: bool()
182184
])
183185
fileExtensions: listOf(string())
184186
checkAlwaysTrueCheckTypeFunctionCall: bool()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Arrays;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr;
7+
use PhpParser\Node\Expr\Assign;
8+
use PhpParser\Node\Scalar\LNumber;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Rules\Rule;
11+
use PHPStan\Rules\RuleError;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
use PHPStan\Rules\RuleLevelHelper;
14+
use PHPStan\Type\Constant\ConstantIntegerType;
15+
use PHPStan\Type\Constant\ConstantStringType;
16+
use PHPStan\Type\ErrorType;
17+
use PHPStan\Type\Type;
18+
use PHPStan\Type\VerbosityLevel;
19+
20+
/**
21+
* @implements Rule<Assign>
22+
*/
23+
class ArrayDeconstructionRule implements Rule
24+
{
25+
26+
private RuleLevelHelper $ruleLevelHelper;
27+
28+
private NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck;
29+
30+
public function __construct(
31+
RuleLevelHelper $ruleLevelHelper,
32+
NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck
33+
)
34+
{
35+
$this->ruleLevelHelper = $ruleLevelHelper;
36+
$this->nonexistentOffsetInArrayDimFetchCheck = $nonexistentOffsetInArrayDimFetchCheck;
37+
}
38+
39+
public function getNodeType(): string
40+
{
41+
return Assign::class;
42+
}
43+
44+
public function processNode(Node $node, Scope $scope): array
45+
{
46+
if (!$node->var instanceof Node\Expr\List_ && !$node->var instanceof Node\Expr\Array_) {
47+
return [];
48+
}
49+
50+
return $this->getErrors(
51+
$scope,
52+
$node->var,
53+
$node->expr
54+
);
55+
}
56+
57+
/**
58+
* @param Node\Expr\List_|Node\Expr\Array_ $var
59+
* @return RuleError[]
60+
*/
61+
private function getErrors(Scope $scope, Expr $var, Expr $expr): array
62+
{
63+
$exprTypeResult = $this->ruleLevelHelper->findTypeToCheck(
64+
$scope,
65+
$expr,
66+
'',
67+
static function (Type $varType): bool {
68+
return $varType->isArray()->yes();
69+
}
70+
);
71+
$exprType = $exprTypeResult->getType();
72+
if ($exprType instanceof ErrorType) {
73+
return [];
74+
}
75+
if (!$exprType->isArray()->yes()) {
76+
return [
77+
RuleErrorBuilder::message(sprintf('Cannot use array destructuring on %s.', $exprType->describe(VerbosityLevel::typeOnly())))->build(),
78+
];
79+
}
80+
81+
$errors = [];
82+
$i = 0;
83+
foreach ($var->items as $item) {
84+
if ($item === null) {
85+
$i++;
86+
continue;
87+
}
88+
89+
$keyExpr = null;
90+
if ($item->key === null) {
91+
$keyType = new ConstantIntegerType($i);
92+
$keyExpr = new Node\Scalar\LNumber($i);
93+
} else {
94+
$keyType = $scope->getType($item->key);
95+
if ($keyType instanceof ConstantIntegerType) {
96+
$keyExpr = new LNumber($keyType->getValue());
97+
} elseif ($keyType instanceof ConstantStringType) {
98+
$keyExpr = new Node\Scalar\String_($keyType->getValue());
99+
}
100+
}
101+
102+
$itemErrors = $this->nonexistentOffsetInArrayDimFetchCheck->check(
103+
$scope,
104+
$expr,
105+
'',
106+
$keyType
107+
);
108+
$errors = array_merge($errors, $itemErrors);
109+
110+
if ($keyExpr === null) {
111+
$i++;
112+
continue;
113+
}
114+
115+
if (!$item->value instanceof Node\Expr\List_ && !$item->value instanceof Node\Expr\Array_) {
116+
$i++;
117+
continue;
118+
}
119+
120+
$errors = array_merge($errors, $this->getErrors(
121+
$scope,
122+
$item->value,
123+
new Expr\ArrayDimFetch($expr, $keyExpr)
124+
));
125+
}
126+
127+
return $errors;
128+
}
129+
130+
}

tests/PHPStan/Levels/LevelsIntegrationTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public function dataTopics(): array
3636
['arrayAccess'],
3737
['typehints'],
3838
['coalesce'],
39+
['arrayDestructuring'],
3940
];
4041
}
4142

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"message": "Cannot use array destructuring on iterable<int, string>.",
4+
"line": 23,
5+
"ignorable": true
6+
},
7+
{
8+
"message": "Offset 3 does not exist on array('a', 'b', 'c').",
9+
"line": 30,
10+
"ignorable": true
11+
}
12+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[
2+
{
3+
"message": "Cannot use array destructuring on array|null.",
4+
"line": 15,
5+
"ignorable": true
6+
}
7+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace ArrayDestructuring;
4+
5+
class Foo
6+
{
7+
8+
/**
9+
* @param mixed[] $array
10+
* @param mixed[]|null $arrayOrNull
11+
*/
12+
public function doFoo(array $array, ?array $arrayOrNull): void
13+
{
14+
[$a, $b, $c] = $array;
15+
[$a, $b, $c] = $arrayOrNull;
16+
}
17+
18+
/**
19+
* @param iterable<int, string> $it
20+
*/
21+
public function doBar(iterable $it): void
22+
{
23+
[$a] = $it;
24+
}
25+
26+
public function doBaz(): void
27+
{
28+
$array = ['a', 'b', 'c'];
29+
[$a] = $array;
30+
[$a, , , $d] = $array;
31+
}
32+
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Arrays;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Rules\RuleLevelHelper;
7+
use PHPStan\Testing\RuleTestCase;
8+
9+
/**
10+
* @extends RuleTestCase<ArrayDeconstructionRule>
11+
*/
12+
class ArrayDeconstructionRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
$ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false);
18+
19+
return new ArrayDeconstructionRule(
20+
$ruleLevelHelper,
21+
new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true)
22+
);
23+
}
24+
25+
public function testRule(): void
26+
{
27+
$this->analyse([__DIR__ . '/data/array-destructuring.php'], [
28+
[
29+
'Cannot use array destructuring on array|null.',
30+
11,
31+
],
32+
[
33+
'Offset 0 does not exist on array().',
34+
12,
35+
],
36+
[
37+
'Cannot use array destructuring on stdClass.',
38+
13,
39+
],
40+
[
41+
'Offset 2 does not exist on array(1, 2).',
42+
15,
43+
],
44+
]);
45+
}
46+
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace ArrayDestructuring;
4+
5+
class Foo
6+
{
7+
8+
public function doFoo(?array $arrayOrNull): void
9+
{
10+
[$a] = [0, 1, 2];
11+
[$a] = $arrayOrNull;
12+
[$a] = [];
13+
[[$a]] = [new \stdClass()];
14+
15+
[[$a, $b, $c]] = [[1, 2]];
16+
}
17+
18+
}

0 commit comments

Comments
 (0)