From 8adfab3a258260c4ecab7d1555181d3a2c62a7d6 Mon Sep 17 00:00:00 2001 From: Can Vural Date: Tue, 14 Dec 2021 15:11:29 +0100 Subject: [PATCH] Add new ArrayUnpackingRule --- conf/config.level3.neon | 1 + src/Php/PhpVersion.php | 5 ++ src/Rules/Arrays/ArrayUnpackingRule.php | 46 +++++++++++++ .../Rules/Arrays/ArrayUnpackingRuleTest.php | 68 +++++++++++++++++++ .../Rules/Arrays/data/array-unpacking.php | 66 ++++++++++++++++++ 5 files changed, 186 insertions(+) create mode 100644 src/Rules/Arrays/ArrayUnpackingRule.php create mode 100644 tests/PHPStan/Rules/Arrays/ArrayUnpackingRuleTest.php create mode 100644 tests/PHPStan/Rules/Arrays/data/array-unpacking.php diff --git a/conf/config.level3.neon b/conf/config.level3.neon index 18371c07de0..88844c69f35 100644 --- a/conf/config.level3.neon +++ b/conf/config.level3.neon @@ -4,6 +4,7 @@ includes: rules: - PHPStan\Rules\Arrays\AppendedArrayItemTypeRule - PHPStan\Rules\Arrays\ArrayDestructuringRule + - PHPStan\Rules\Arrays\ArrayUnpackingRule - PHPStan\Rules\Arrays\IterableInForeachRule - PHPStan\Rules\Arrays\OffsetAccessAssignmentRule - PHPStan\Rules\Arrays\OffsetAccessAssignOpRule diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index 30e0a4eb66d..ea427dedbe6 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -169,4 +169,9 @@ public function supportsFirstClassCallables(): bool return $this->versionId >= 80100; } + public function supportsArrayUnpackingWithStringKeys(): bool + { + return $this->versionId >= 80100; + } + } diff --git a/src/Rules/Arrays/ArrayUnpackingRule.php b/src/Rules/Arrays/ArrayUnpackingRule.php new file mode 100644 index 00000000000..6f31d6ee7b2 --- /dev/null +++ b/src/Rules/Arrays/ArrayUnpackingRule.php @@ -0,0 +1,46 @@ + + */ +class ArrayUnpackingRule implements Rule +{ + + private PhpVersion $phpVersion; + + public function __construct(PhpVersion $phpVersion) + { + $this->phpVersion = $phpVersion; + } + + public function getNodeType(): string + { + return ArrayItem::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if ($node->unpack === false || $this->phpVersion->supportsArrayUnpackingWithStringKeys()) { + return []; + } + + $valueType = $scope->getType($node->value); + + if ((new StringType())->isSuperTypeOf($valueType->getIterableKeyType())->no()) { + return []; + } + + return [RuleErrorBuilder::message('Array unpacking cannot be used on array that potentially has string keys.')->build()]; + } + +} diff --git a/tests/PHPStan/Rules/Arrays/ArrayUnpackingRuleTest.php b/tests/PHPStan/Rules/Arrays/ArrayUnpackingRuleTest.php new file mode 100644 index 00000000000..5d095037fff --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/ArrayUnpackingRuleTest.php @@ -0,0 +1,68 @@ + + */ +class ArrayUnpackingRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new ArrayUnpackingRule(self::getContainer()->getByType(PhpVersion::class)); + } + + public function testRule(): void + { + if (PHP_VERSION_ID >= 80100) { + $this->markTestSkipped('Test requires PHP version <= 8.0'); + } + + $this->analyse([__DIR__ . '/data/array-unpacking.php'], [ + [ + 'Array unpacking cannot be used on array that potentially has string keys.', + 7, + ], + [ + 'Array unpacking cannot be used on array that potentially has string keys.', + 18, + ], + [ + 'Array unpacking cannot be used on array that potentially has string keys.', + 24, + ], + [ + 'Array unpacking cannot be used on array that potentially has string keys.', + 29, + ], + [ + 'Array unpacking cannot be used on array that potentially has string keys.', + 40, + ], + [ + 'Array unpacking cannot be used on array that potentially has string keys.', + 52, + ], + [ + 'Array unpacking cannot be used on array that potentially has string keys.', + 63, + ], + ]); + } + + public function testRuleOnPHP81(): void + { + if (PHP_VERSION_ID < 80100) { + $this->markTestSkipped('Test requires PHP 8.1+'); + } + + $this->analyse([__DIR__ . '/data/array-unpacking.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/array-unpacking.php b/tests/PHPStan/Rules/Arrays/data/array-unpacking.php new file mode 100644 index 00000000000..9af77362bb7 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/array-unpacking.php @@ -0,0 +1,66 @@ + 'bar', 1, 2, 3]; + +$bar = [...$foo]; + +/** @param array $bar */ +function intKeyedArray(array $bar) +{ + $baz = [...$bar]; +} + +/** @param array $bar */ +function stringKeyedArray(array $bar) +{ + $baz = [...$bar]; +} + +/** @param array $bar */ +function unionKeyedArray(array $bar) +{ + $baz = [...$bar]; +} + +function mixedKeyedArray(array $bar) +{ + $baz = [...$bar]; +} + +/** + * @param array $foo + * @param array $bar + */ +function multipleUnpacking(array $foo, array $bar) +{ + $baz = [ + ...$bar, + ...$foo, + ]; +} + +/** + * @param array $foo + * @param array $bar + */ +function foo(array $foo, array $bar) +{ + $baz = [ + $bar, + ...$foo + ]; +} + +/** + * @param array{foo: string, bar:int} $foo + * @param array{1, 2, 3, 4} $bar + */ +function unpackingArrayShapes(array $foo, array $bar) +{ + $baz = [ + ...$foo, + ...$bar, + ]; +}