diff --git a/rules/downgrade-php74/src/Rector/Array_/DowngradeArraySpreadRector.php b/rules/downgrade-php74/src/Rector/Array_/DowngradeArraySpreadRector.php new file mode 100644 index 000000000000..47682d9f6f03 --- /dev/null +++ b/rules/downgrade-php74/src/Rector/Array_/DowngradeArraySpreadRector.php @@ -0,0 +1,201 @@ +variableNaming = $variableNaming; + } + + public function getDefinition(): RectorDefinition + { + return new RectorDefinition('Replace array spread with array_merge function', [ + new CodeSample( + <<<'CODE_SAMPLE' +class SomeClass +{ + public function run() + { + $parts = ['apple', 'pear']; + $fruits = ['banana', 'orange', ...$parts, 'watermelon']; + } + + public function runWithIterable() + { + $fruits = ['banana', 'orange', ...new ArrayIterator(['durian', 'kiwi']), 'watermelon']; + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +class SomeClass +{ + public function run() + { + $parts = ['apple', 'pear']; + $fruits = array_merge(['banana', 'orange'], $parts, ['watermelon']); + } + + public function runWithIterable() + { + $item0Unpacked = new ArrayIterator(['durian', 'kiwi']); + $fruits = array_merge(['banana', 'orange'], is_array($item0Unpacked) ? $item0Unpacked : iterator_to_array($item0Unpacked), ['watermelon']); + } +} +CODE_SAMPLE + ), + ]); + } + + /** + * @return string[] + */ + public function getNodeTypes(): array + { + return [Array_::class]; + } + + /** + * @param Array_ $node + */ + public function refactor(Node $node): ?Node + { + if (! $this->shouldRefactor($node)) { + return null; + } + return $this->refactorNode($node); + } + + private function shouldRefactor(Array_ $array): bool + { + // Check that any item in the array is the spread + return count(array_filter($array->items, function (?ArrayItem $item): bool { + return $item !== null && $item->unpack; + })) > 0; + } + + /** + * Iterate all array items: + * 1. If they use the spread, remove it + * 2. If not, make the item part of an accumulating array, + * to be added once the next spread is found, or at the end + */ + private function refactorNode(Array_ $array): Node + { + $newItems = []; + $accumulatedItems = []; + foreach ($array->items as $position => $item) { + if ($item !== null && $item->unpack) { + // Spread operator found + // If it is a not variable, transform it to a variable + if (! $item->value instanceof Variable) { + $item->value = $this->createVariableFromNonVariable($array, $item, $position); + } + if ($accumulatedItems !== []) { + // If previous items were in the new array, add them first + $newItems[] = $this->createArrayItem($accumulatedItems); + // Reset the accumulated items + $accumulatedItems = []; + } + // Add the current item, still with "unpack = true" (it will be removed later on) + $newItems[] = $item; + continue; + } + + // Normal item, it goes into the accumulated array + $accumulatedItems[] = $item; + } + // Add the remaining accumulated items + if ($accumulatedItems !== []) { + $newItems[] = $this->createArrayItem($accumulatedItems); + } + // Replace this array node with an `array_merge` + return $this->createArrayMerge($newItems); + } + + /** + * If it is a variable, we add it directly + * Otherwise it could be a function, method, ternary, traversable, etc + * We must then first extract it into a variable, + * as to invoke it only once and avoid potential bugs, + * such as a method executing some side-effect + * @param int|string $position + */ + private function createVariableFromNonVariable(Array_ $array, ArrayItem $arrayItem, $position): Variable + { + /** @var Scope */ + $nodeScope = $array->getAttribute(AttributeKey::SCOPE); + // The variable name will be item0Unpacked, item1Unpacked, etc, + // depending on their position. + // The number can't be at the end of the var name, or it would + // conflict with the counter (for if that name is already taken) + $variableName = $this->variableNaming->resolveFromNodeWithScopeCountAndFallbackName( + $array, + $nodeScope, + 'item' . $position . 'Unpacked' + ); + // Assign the value to the variable, and replace the element with the variable + $newVariable = new Variable($variableName); + $this->addNodeBeforeNode(new Assign($newVariable, $arrayItem->value), $array); + return $newVariable; + } + + /** + * @param (ArrayItem|null)[] $items + */ + private function createArrayItem(array $items): ArrayItem + { + return new ArrayItem(new Array_($items)); + } + + /** + * @see https://wiki.php.net/rfc/spread_operator_for_array + * @param (ArrayItem|null)[] $items + */ + private function createArrayMerge(array $items): FuncCall + { + return new FuncCall(new Name('array_merge'), array_map(function (ArrayItem $item): Arg { + if ($item !== null && $item->unpack) { + // Do not unpack anymore + $item->unpack = false; + // array_merge only supports array, while spread operator also supports objects implementing Traversable. + return new Arg( + new Ternary( + new FuncCall(new Name('is_array'), [new Arg($item)]), + $item, + new FuncCall(new Name('iterator_to_array'), [new Arg($item)]) + ) + ); + } + return new Arg($item); + }, $items)); + } +} diff --git a/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/DowngradeArraySpreadRectorTest.php b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/DowngradeArraySpreadRectorTest.php new file mode 100644 index 000000000000..57f9c8369417 --- /dev/null +++ b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/DowngradeArraySpreadRectorTest.php @@ -0,0 +1,38 @@ +doTestFileInfo($fileInfo); + } + + public function provideData(): Iterator + { + return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + protected function getRectorClass(): string + { + return DowngradeArraySpreadRector::class; + } + + protected function getPhpVersion(): string + { + return PhpVersionFeature::BEFORE_ARRAY_SPREAD; + } +} diff --git a/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/array_fn_name.php.inc b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/array_fn_name.php.inc new file mode 100644 index 000000000000..ca0b5eb5100b --- /dev/null +++ b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/array_fn_name.php.inc @@ -0,0 +1,29 @@ + +----- + diff --git a/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/combined_items.php.inc b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/combined_items.php.inc new file mode 100644 index 000000000000..c31f65becf93 --- /dev/null +++ b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/combined_items.php.inc @@ -0,0 +1,34 @@ + +----- + diff --git a/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/different_positions.php.inc b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/different_positions.php.inc new file mode 100644 index 000000000000..e4a98426f430 --- /dev/null +++ b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/different_positions.php.inc @@ -0,0 +1,45 @@ + +----- + diff --git a/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/fixture.php.inc b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/fixture.php.inc new file mode 100644 index 000000000000..87c217c638c4 --- /dev/null +++ b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/fixture.php.inc @@ -0,0 +1,29 @@ + +----- + diff --git a/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/multiple_unpacks.php.inc b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/multiple_unpacks.php.inc new file mode 100644 index 000000000000..bb9cea7779dd --- /dev/null +++ b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/multiple_unpacks.php.inc @@ -0,0 +1,33 @@ + +----- + diff --git a/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/no_unpacks.php.inc b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/no_unpacks.php.inc new file mode 100644 index 000000000000..d49b42c81240 --- /dev/null +++ b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/no_unpacks.php.inc @@ -0,0 +1,14 @@ + diff --git a/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/single_unpack.php.inc b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/single_unpack.php.inc new file mode 100644 index 000000000000..15522ad9f436 --- /dev/null +++ b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/single_unpack.php.inc @@ -0,0 +1,29 @@ + +----- + diff --git a/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_array_item.php.inc b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_array_item.php.inc new file mode 100644 index 000000000000..95b771638807 --- /dev/null +++ b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_array_item.php.inc @@ -0,0 +1,29 @@ + +----- + diff --git a/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_array_iterator_item.php.inc b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_array_iterator_item.php.inc new file mode 100644 index 000000000000..5cc7b0a8ce8f --- /dev/null +++ b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_array_iterator_item.php.inc @@ -0,0 +1,32 @@ + +----- + diff --git a/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_function_item.php.inc b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_function_item.php.inc new file mode 100644 index 000000000000..246507003f7c --- /dev/null +++ b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_function_item.php.inc @@ -0,0 +1,38 @@ +getArray(), 'watermelon']; + } +} + +?> +----- +getArray(); + $fruits = array_merge(['banana', 'orange'], is_array($item2Unpacked) ? $item2Unpacked : iterator_to_array($item2Unpacked), ['watermelon']); + } +} + +?> diff --git a/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_function_item_and_existing_var.php.inc b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_function_item_and_existing_var.php.inc new file mode 100644 index 000000000000..f42879faaf07 --- /dev/null +++ b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_function_item_and_existing_var.php.inc @@ -0,0 +1,42 @@ +getArray(), 'watermelon']; + } +} + +?> +----- +getArray(); + $fruits = array_merge(['banana', 'orange'], is_array($item2Unpacked2) ? $item2Unpacked2 : iterator_to_array($item2Unpacked2), ['watermelon']); + } +} + +?> diff --git a/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_multiple_array_iterator_items.php.inc b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_multiple_array_iterator_items.php.inc new file mode 100644 index 000000000000..637ea4e25755 --- /dev/null +++ b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_multiple_array_iterator_items.php.inc @@ -0,0 +1,33 @@ + +----- + diff --git a/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_multiple_function_item.php.inc b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_multiple_function_item.php.inc new file mode 100644 index 000000000000..e2974ee83f91 --- /dev/null +++ b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_multiple_function_item.php.inc @@ -0,0 +1,49 @@ +getFirstArray(), 'watermelon', ...$this->getSecondArray()]; + } +} + +?> +----- +getFirstArray(); + $item4Unpacked = $this->getSecondArray(); + $fruits = array_merge(['banana', 'orange'], is_array($item2Unpacked) ? $item2Unpacked : iterator_to_array($item2Unpacked), ['watermelon'], is_array($item4Unpacked) ? $item4Unpacked : iterator_to_array($item4Unpacked)); + } +} + +?> diff --git a/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_null_item.php.inc b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_null_item.php.inc new file mode 100644 index 000000000000..76d8c44d964d --- /dev/null +++ b/rules/downgrade-php74/tests/Rector/Array_/DowngradeArraySpreadRector/Fixture/with_null_item.php.inc @@ -0,0 +1,33 @@ + +----- + diff --git a/src/ValueObject/PhpVersionFeature.php b/src/ValueObject/PhpVersionFeature.php index 503bf2f1cfb8..7e2d592bac98 100644 --- a/src/ValueObject/PhpVersionFeature.php +++ b/src/ValueObject/PhpVersionFeature.php @@ -166,6 +166,11 @@ final class PhpVersionFeature */ public const BEFORE_LITERAL_SEPARATOR = '7.3'; + /** + * @var string + */ + public const BEFORE_ARRAY_SPREAD = '7.3'; + /** * @var string */