Skip to content

Commit

Permalink
Bleeding edge - check array deconstruction
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Feb 11, 2021
1 parent 3f7c015 commit aae34d9
Show file tree
Hide file tree
Showing 10 changed files with 259 additions and 1 deletion.
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ parameters:
checkLogicalOrConstantCondition: true
checkMissingTemplateTypeInParameter: true
wrongVarUsage: true
arrayDeconstruction: true
7 changes: 7 additions & 0 deletions conf/config.level3.neon
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ rules:
- PHPStan\Rules\Variables\ThrowTypeRule
- PHPStan\Rules\Variables\VariableCloningRule

conditionalTags:
PHPStan\Rules\Arrays\ArrayDeconstructionRule:
phpstan.rules.rule: %featureToggles.arrayDeconstruction%

parameters:
checkPhpDocMethodSignatures: true

Expand All @@ -28,6 +32,9 @@ services:
tags:
- phpstan.rules.rule

-
class: PHPStan\Rules\Arrays\ArrayDeconstructionRule

-
class: PHPStan\Rules\Arrays\InvalidKeyInArrayDimFetchRule
arguments:
Expand Down
4 changes: 3 additions & 1 deletion conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ parameters:
checkLogicalOrConstantCondition: false
checkMissingTemplateTypeInParameter: false
wrongVarUsage: false
arrayDeconstruction: false
fileExtensions:
- php
checkAlwaysTrueCheckTypeFunctionCall: false
Expand Down Expand Up @@ -178,7 +179,8 @@ parametersSchema:
checkLogicalAndConstantCondition: bool(),
checkLogicalOrConstantCondition: bool(),
checkMissingTemplateTypeInParameter: bool(),
wrongVarUsage: bool()
wrongVarUsage: bool(),
arrayDeconstruction: bool()
])
fileExtensions: listOf(string())
checkAlwaysTrueCheckTypeFunctionCall: bool()
Expand Down
130 changes: 130 additions & 0 deletions src/Rules/Arrays/ArrayDeconstructionRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Arrays;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Scalar\LNumber;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\ErrorType;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;

/**
* @implements Rule<Assign>
*/
class ArrayDeconstructionRule implements Rule
{

private RuleLevelHelper $ruleLevelHelper;

private NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck;

public function __construct(
RuleLevelHelper $ruleLevelHelper,
NonexistentOffsetInArrayDimFetchCheck $nonexistentOffsetInArrayDimFetchCheck
)
{
$this->ruleLevelHelper = $ruleLevelHelper;
$this->nonexistentOffsetInArrayDimFetchCheck = $nonexistentOffsetInArrayDimFetchCheck;
}

public function getNodeType(): string
{
return Assign::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!$node->var instanceof Node\Expr\List_ && !$node->var instanceof Node\Expr\Array_) {
return [];
}

return $this->getErrors(
$scope,
$node->var,
$node->expr
);
}

/**
* @param Node\Expr\List_|Node\Expr\Array_ $var
* @return RuleError[]
*/
private function getErrors(Scope $scope, Expr $var, Expr $expr): array
{
$exprTypeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$expr,
'',
static function (Type $varType): bool {
return $varType->isArray()->yes();
}
);
$exprType = $exprTypeResult->getType();
if ($exprType instanceof ErrorType) {
return [];
}
if (!$exprType->isArray()->yes()) {
return [
RuleErrorBuilder::message(sprintf('Cannot use array destructuring on %s.', $exprType->describe(VerbosityLevel::typeOnly())))->build(),
];
}

$errors = [];
$i = 0;
foreach ($var->items as $item) {
if ($item === null) {
$i++;
continue;
}

$keyExpr = null;
if ($item->key === null) {
$keyType = new ConstantIntegerType($i);
$keyExpr = new Node\Scalar\LNumber($i);
} else {
$keyType = $scope->getType($item->key);
if ($keyType instanceof ConstantIntegerType) {
$keyExpr = new LNumber($keyType->getValue());
} elseif ($keyType instanceof ConstantStringType) {
$keyExpr = new Node\Scalar\String_($keyType->getValue());
}
}

$itemErrors = $this->nonexistentOffsetInArrayDimFetchCheck->check(
$scope,
$expr,
'',
$keyType
);
$errors = array_merge($errors, $itemErrors);

if ($keyExpr === null) {
$i++;
continue;
}

if (!$item->value instanceof Node\Expr\List_ && !$item->value instanceof Node\Expr\Array_) {
$i++;
continue;
}

$errors = array_merge($errors, $this->getErrors(
$scope,
$item->value,
new Expr\ArrayDimFetch($expr, $keyExpr)
));
}

return $errors;
}

}
1 change: 1 addition & 0 deletions tests/PHPStan/Levels/LevelsIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public function dataTopics(): array
['arrayAccess'],
['typehints'],
['coalesce'],
['arrayDestructuring'],
];
}

Expand Down
12 changes: 12 additions & 0 deletions tests/PHPStan/Levels/data/arrayDestructuring-3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"message": "Cannot use array destructuring on iterable<int, string>.",
"line": 23,
"ignorable": true
},
{
"message": "Offset 3 does not exist on array('a', 'b', 'c').",
"line": 30,
"ignorable": true
}
]
7 changes: 7 additions & 0 deletions tests/PHPStan/Levels/data/arrayDestructuring-8.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"message": "Cannot use array destructuring on array|null.",
"line": 15,
"ignorable": true
}
]
33 changes: 33 additions & 0 deletions tests/PHPStan/Levels/data/arrayDestructuring.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace ArrayDestructuring;

class Foo
{

/**
* @param mixed[] $array
* @param mixed[]|null $arrayOrNull
*/
public function doFoo(array $array, ?array $arrayOrNull): void
{
[$a, $b, $c] = $array;
[$a, $b, $c] = $arrayOrNull;
}

/**
* @param iterable<int, string> $it
*/
public function doBar(iterable $it): void
{
[$a] = $it;
}

public function doBaz(): void
{
$array = ['a', 'b', 'c'];
[$a] = $array;
[$a, , , $d] = $array;
}

}
47 changes: 47 additions & 0 deletions tests/PHPStan/Rules/Arrays/ArrayDeconstructionRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Arrays;

use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<ArrayDeconstructionRule>
*/
class ArrayDeconstructionRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
$ruleLevelHelper = new RuleLevelHelper($this->createReflectionProvider(), true, false, true, false);

return new ArrayDeconstructionRule(
$ruleLevelHelper,
new NonexistentOffsetInArrayDimFetchCheck($ruleLevelHelper, true)
);
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/array-destructuring.php'], [
[
'Cannot use array destructuring on array|null.',
11,
],
[
'Offset 0 does not exist on array().',
12,
],
[
'Cannot use array destructuring on stdClass.',
13,
],
[
'Offset 2 does not exist on array(1, 2).',
15,
],
]);
}

}
18 changes: 18 additions & 0 deletions tests/PHPStan/Rules/Arrays/data/array-destructuring.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace ArrayDestructuring;

class Foo
{

public function doFoo(?array $arrayOrNull): void
{
[$a] = [0, 1, 2];
[$a] = $arrayOrNull;
[$a] = [];
[[$a]] = [new \stdClass()];

[[$a, $b, $c]] = [[1, 2]];
}

}

0 comments on commit aae34d9

Please sign in to comment.