mirror of
https://github.com/rectorphp/rector.git
synced 2024-06-07 03:40:50 +00:00
[Downgrade] Separate multi level dependency in DowngradeArraySpreadRector (#2298)
This commit is contained in:
parent
3d499125b8
commit
3bae3b9e75
|
@ -104,6 +104,16 @@ final class PHPStanNodeScopeResolver
|
|||
$node->valueVar->setAttribute(AttributeKey::SCOPE, $mutatingScope);
|
||||
}
|
||||
|
||||
if ($node instanceof Property) {
|
||||
foreach ($node->props as $propertyProperty) {
|
||||
$propertyProperty->setAttribute(AttributeKey::SCOPE, $mutatingScope);
|
||||
|
||||
if ($propertyProperty->default instanceof Expr) {
|
||||
$propertyProperty->default->setAttribute(AttributeKey::SCOPE, $mutatingScope);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($node instanceof Switch_) {
|
||||
// decorate value as well
|
||||
foreach ($node->cases as $case) {
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace Rector\Tests\DowngradePhp74\Rector\Array_\DowngradeArraySpreadRector\Fixture;
|
||||
|
||||
final class SkipPropertyDefault
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $items = [];
|
||||
|
||||
public function run()
|
||||
{
|
||||
if (in_array(1, $this->items)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace Rector\Tests\DowngradePhp81\Rector\Array_\DowngradeArraySpreadStringKeyRector\Fixture;
|
||||
|
||||
class ArraySpreadStringKey
|
||||
final class ArraySpreadStringKey
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
|
@ -19,7 +19,7 @@ class ArraySpreadStringKey
|
|||
|
||||
namespace Rector\Tests\DowngradePhp81\Rector\Array_\DowngradeArraySpreadStringKeyRector\Fixture;
|
||||
|
||||
class ArraySpreadStringKey
|
||||
final class ArraySpreadStringKey
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
|
|
|
@ -2,17 +2,25 @@
|
|||
|
||||
namespace Rector\Tests\DowngradePhp81\Rector\Array_\DowngradeArraySpreadStringKeyRector\Fixture;
|
||||
|
||||
class ArraySpreadStringKeyByDoc
|
||||
final class ArraySpreadStringKeyByDoc
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
/** @var array<string, string> $parts */
|
||||
$parts = $this->data();
|
||||
/** @var array<string, string> $parts2 */
|
||||
$parts2 = $this->data2();
|
||||
$parts2 = $this->data();
|
||||
|
||||
$result = [...$parts, ...$parts2];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function data(): array
|
||||
{
|
||||
return ['your' => 'name'];
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
@ -21,17 +29,25 @@ class ArraySpreadStringKeyByDoc
|
|||
|
||||
namespace Rector\Tests\DowngradePhp81\Rector\Array_\DowngradeArraySpreadStringKeyRector\Fixture;
|
||||
|
||||
class ArraySpreadStringKeyByDoc
|
||||
final class ArraySpreadStringKeyByDoc
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
/** @var array<string, string> $parts */
|
||||
$parts = $this->data();
|
||||
/** @var array<string, string> $parts2 */
|
||||
$parts2 = $this->data2();
|
||||
$parts2 = $this->data();
|
||||
|
||||
$result = array_merge($parts, $parts2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function data(): array
|
||||
{
|
||||
return ['your' => 'name'];
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
|
|
@ -5,45 +5,24 @@ declare(strict_types=1);
|
|||
namespace Rector\DowngradePhp74\Rector\Array_;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Arg;
|
||||
use PhpParser\Node\Expr\Array_;
|
||||
use PhpParser\Node\Expr\ArrayItem;
|
||||
use PhpParser\Node\Expr\Assign;
|
||||
use PhpParser\Node\Expr\FuncCall;
|
||||
use PhpParser\Node\Expr\Ternary;
|
||||
use PhpParser\Node\Expr\Variable;
|
||||
use PhpParser\Node\Name;
|
||||
use PHPStan\Analyser\Scope;
|
||||
use PHPStan\Type\ArrayType;
|
||||
use PHPStan\Type\IterableType;
|
||||
use PHPStan\Type\ObjectType;
|
||||
use PHPStan\Type\Type;
|
||||
use Rector\Core\Exception\ShouldNotHappenException;
|
||||
use Rector\Core\Rector\AbstractScopeAwareRector;
|
||||
use Rector\Naming\Naming\VariableNaming;
|
||||
use Rector\NodeTypeResolver\Node\AttributeKey;
|
||||
use Rector\DowngradePhp81\NodeAnalyzer\ArraySpreadAnalyzer;
|
||||
use Rector\DowngradePhp81\NodeFactory\ArrayMergeFromArraySpreadFactory;
|
||||
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
|
||||
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* @changelog https://wiki.php.net/rfc/spread_operator_for_array
|
||||
*
|
||||
* @see \Rector\Tests\DowngradePhp74\Rector\Array_\DowngradeArraySpreadRector\DowngradeArraySpreadRectorTest
|
||||
*/
|
||||
class DowngradeArraySpreadRector extends AbstractScopeAwareRector
|
||||
final class DowngradeArraySpreadRector extends AbstractScopeAwareRector
|
||||
{
|
||||
private bool $shouldIncrement = false;
|
||||
|
||||
/**
|
||||
* Handle different result in CI
|
||||
*
|
||||
* @var array<string, int>
|
||||
*/
|
||||
private array $lastPositionCurrentFile = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly VariableNaming $variableNaming
|
||||
private readonly ArrayMergeFromArraySpreadFactory $arrayMergeFromArraySpreadFactory,
|
||||
private readonly ArraySpreadAnalyzer $arraySpreadAnalyzer,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -103,209 +82,18 @@ CODE_SAMPLE
|
|||
*/
|
||||
public function refactorWithScope(Node $node, Scope $scope): ?Node
|
||||
{
|
||||
if (! $this->shouldRefactor($node)) {
|
||||
if (! $this->arraySpreadAnalyzer->isArrayWithUnpack($node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->shouldIncrement = (bool) $this->betterNodeFinder->findFirstNext($node, function (Node $subNode): bool {
|
||||
$shouldIncrement = (bool) $this->betterNodeFinder->findFirstNext($node, function (Node $subNode): bool {
|
||||
if (! $subNode instanceof Array_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->shouldRefactor($subNode);
|
||||
return $this->arraySpreadAnalyzer->isArrayWithUnpack($subNode);
|
||||
});
|
||||
|
||||
return $this->refactorArray($node, $scope);
|
||||
}
|
||||
|
||||
private function shouldRefactor(Array_ $array): bool
|
||||
{
|
||||
// Check that any item in the array is the spread
|
||||
foreach ($array->items as $item) {
|
||||
if (! $item instanceof ArrayItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($item->unpack) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function refactorArray(Array_ $array, Scope $scope): FuncCall
|
||||
{
|
||||
$newItems = $this->createArrayItems($array, $scope);
|
||||
// Replace this array node with an `array_merge`
|
||||
return $this->createArrayMerge($newItems, $scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return ArrayItem[]
|
||||
*/
|
||||
private function createArrayItems(Array_ $array, Scope $scope): array
|
||||
{
|
||||
$newItems = [];
|
||||
$accumulatedItems = [];
|
||||
foreach ($array->items as $position => $item) {
|
||||
if ($item !== null && $item->unpack) {
|
||||
// Spread operator found
|
||||
if (! $item->value instanceof Variable) {
|
||||
// If it is a not variable, transform it to a variable
|
||||
$item->value = $this->createVariableFromNonVariable($array, $item, $position, $scope);
|
||||
}
|
||||
|
||||
if ($accumulatedItems !== []) {
|
||||
// If previous items were in the new array, add them first
|
||||
$newItems[] = $this->createArrayItemFromArray($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->createArrayItemFromArray($accumulatedItems);
|
||||
}
|
||||
|
||||
return $newItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param (ArrayItem|null)[] $items
|
||||
*/
|
||||
private function createArrayMerge(array $items, Scope $scope): FuncCall
|
||||
{
|
||||
$args = array_map(function (ArrayItem|null $arrayItem) use ($scope): Arg {
|
||||
if ($arrayItem === null) {
|
||||
throw new ShouldNotHappenException();
|
||||
}
|
||||
|
||||
if ($arrayItem->unpack) {
|
||||
// Do not unpack anymore
|
||||
$arrayItem->unpack = false;
|
||||
return $this->createArgFromSpreadArrayItem($scope, $arrayItem);
|
||||
}
|
||||
|
||||
return new Arg($arrayItem);
|
||||
}, $items);
|
||||
|
||||
return new FuncCall(new Name('array_merge'), $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private function createVariableFromNonVariable(
|
||||
Array_ $array,
|
||||
ArrayItem $arrayItem,
|
||||
int $position,
|
||||
Scope $scope
|
||||
): Variable {
|
||||
// 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)
|
||||
$smartFileInfo = $this->file->getSmartFileInfo();
|
||||
$realPath = $smartFileInfo->getRealPath();
|
||||
$position = $this->lastPositionCurrentFile[$realPath] ?? $position;
|
||||
|
||||
$variableName = $this->variableNaming->resolveFromNodeWithScopeCountAndFallbackName(
|
||||
$array,
|
||||
$scope,
|
||||
'item' . $position . 'Unpacked'
|
||||
);
|
||||
|
||||
if ($this->shouldIncrement) {
|
||||
$this->lastPositionCurrentFile[$realPath] = ++$position;
|
||||
}
|
||||
|
||||
// Assign the value to the variable, and replace the element with the variable
|
||||
$newVariable = new Variable($variableName);
|
||||
|
||||
$newVariableAssign = new Assign($newVariable, $arrayItem->value);
|
||||
$this->nodesToAddCollector->addNodeBeforeNode($newVariableAssign, $array, $this->file->getSmartFileInfo());
|
||||
|
||||
return $newVariable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<ArrayItem|null> $items
|
||||
*/
|
||||
private function createArrayItemFromArray(array $items): ArrayItem
|
||||
{
|
||||
$array = new Array_($items);
|
||||
return new ArrayItem($array);
|
||||
}
|
||||
|
||||
private function createArgFromSpreadArrayItem(Scope $nodeScope, ArrayItem $arrayItem): Arg
|
||||
{
|
||||
// By now every item is a variable
|
||||
/** @var Variable $variable */
|
||||
$variable = $arrayItem->value;
|
||||
$variableName = $this->getName($variable) ?? '';
|
||||
|
||||
// If the variable is not in scope, it's one we just added.
|
||||
// Then get the type from the attribute
|
||||
if ($nodeScope->hasVariableType($variableName)->yes()) {
|
||||
$type = $nodeScope->getVariableType($variableName);
|
||||
} else {
|
||||
$originalNode = $arrayItem->getAttribute(AttributeKey::ORIGINAL_NODE);
|
||||
if ($originalNode instanceof ArrayItem) {
|
||||
$type = $nodeScope->getType($originalNode->value);
|
||||
} else {
|
||||
throw new ShouldNotHappenException();
|
||||
}
|
||||
}
|
||||
|
||||
$iteratorToArrayFuncCall = new FuncCall(new Name('iterator_to_array'), [new Arg($arrayItem)]);
|
||||
|
||||
// If we know it is an array, then print it directly
|
||||
// Otherwise PHPStan throws an error:
|
||||
// "Else branch is unreachable because ternary operator condition is always true."
|
||||
if ($type instanceof ArrayType) {
|
||||
return new Arg($arrayItem);
|
||||
}
|
||||
|
||||
// If it is iterable, then directly return `iterator_to_array`
|
||||
if ($this->isIterableType($type)) {
|
||||
return new Arg($iteratorToArrayFuncCall);
|
||||
}
|
||||
|
||||
// Print a ternary, handling either an array or an iterator
|
||||
$inArrayFuncCall = new FuncCall(new Name('is_array'), [new Arg($arrayItem)]);
|
||||
return new Arg(new Ternary($inArrayFuncCall, $arrayItem, $iteratorToArrayFuncCall));
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterables: objects declaring the interface Traversable,
|
||||
* For "iterable" type, it can be array
|
||||
*/
|
||||
private function isIterableType(Type $type): bool
|
||||
{
|
||||
if ($type instanceof IterableType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$traversableObjectType = new ObjectType('Traversable');
|
||||
return $traversableObjectType->isSuperTypeOf($type)
|
||||
->yes();
|
||||
return $this->arrayMergeFromArraySpreadFactory->createFromArray($node, $scope, $this->file, $shouldIncrement);
|
||||
}
|
||||
}
|
||||
|
|
27
rules/DowngradePhp81/NodeAnalyzer/ArraySpreadAnalyzer.php
Normal file
27
rules/DowngradePhp81/NodeAnalyzer/ArraySpreadAnalyzer.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\DowngradePhp81\NodeAnalyzer;
|
||||
|
||||
use PhpParser\Node\Expr\Array_;
|
||||
use PhpParser\Node\Expr\ArrayItem;
|
||||
|
||||
final class ArraySpreadAnalyzer
|
||||
{
|
||||
public function isArrayWithUnpack(Array_ $array): bool
|
||||
{
|
||||
// Check that any item in the array is the spread
|
||||
foreach ($array->items as $item) {
|
||||
if (! $item instanceof ArrayItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($item->unpack) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\DowngradePhp81\NodeFactory;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Arg;
|
||||
use PhpParser\Node\Expr\Array_;
|
||||
use PhpParser\Node\Expr\ArrayItem;
|
||||
use PhpParser\Node\Expr\Assign;
|
||||
use PhpParser\Node\Expr\FuncCall;
|
||||
use PhpParser\Node\Expr\Ternary;
|
||||
use PhpParser\Node\Expr\Variable;
|
||||
use PhpParser\Node\Name;
|
||||
use PHPStan\Analyser\Scope;
|
||||
use PHPStan\Type\ArrayType;
|
||||
use PHPStan\Type\IterableType;
|
||||
use PHPStan\Type\ObjectType;
|
||||
use PHPStan\Type\Type;
|
||||
use Rector\Core\Exception\ShouldNotHappenException;
|
||||
use Rector\Core\PhpParser\Node\BetterNodeFinder;
|
||||
use Rector\Core\ValueObject\Application\File;
|
||||
use Rector\DowngradePhp81\NodeAnalyzer\ArraySpreadAnalyzer;
|
||||
use Rector\Naming\Naming\VariableNaming;
|
||||
use Rector\NodeNameResolver\NodeNameResolver;
|
||||
use Rector\NodeTypeResolver\Node\AttributeKey;
|
||||
use Rector\PostRector\Collector\NodesToAddCollector;
|
||||
|
||||
final class ArrayMergeFromArraySpreadFactory
|
||||
{
|
||||
private bool $shouldIncrement = false;
|
||||
|
||||
/**
|
||||
* Handle different result in CI
|
||||
*
|
||||
* @var array<string, int>
|
||||
*/
|
||||
private array $lastPositionCurrentFile = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly VariableNaming $variableNaming,
|
||||
private readonly BetterNodeFinder $betterNodeFinder,
|
||||
private readonly NodesToAddCollector $nodesToAddCollector,
|
||||
private readonly NodeNameResolver $nodeNameResolver,
|
||||
private readonly ArraySpreadAnalyzer $arraySpreadAnalyzer
|
||||
) {
|
||||
}
|
||||
|
||||
public function createFromArray(Array_ $array, Scope $scope, File $file, ?bool $shouldIncrement = null): ?Node
|
||||
{
|
||||
if (! $this->arraySpreadAnalyzer->isArrayWithUnpack($array)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($shouldIncrement !== null) {
|
||||
$this->shouldIncrement = $shouldIncrement;
|
||||
} else {
|
||||
$this->shouldIncrement = (bool) $this->betterNodeFinder->findFirstNext(
|
||||
$array,
|
||||
function (Node $subNode): bool {
|
||||
if (! $subNode instanceof Array_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->arraySpreadAnalyzer->isArrayWithUnpack($subNode);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$newArrayItems = $this->disolveArrayItems($array, $scope, $file);
|
||||
|
||||
return $this->createArrayMergeFuncCall($newArrayItems, $scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return ArrayItem[]
|
||||
*/
|
||||
private function disolveArrayItems(Array_ $array, Scope $scope, File $file): array
|
||||
{
|
||||
$newItems = [];
|
||||
|
||||
$accumulatedItems = [];
|
||||
foreach ($array->items as $position => $item) {
|
||||
if ($item !== null && $item->unpack) {
|
||||
// Spread operator found
|
||||
if (! $item->value instanceof Variable) {
|
||||
// If it is a not variable, transform it to a variable
|
||||
$item->value = $this->createVariableFromNonVariable($array, $item, $position, $scope, $file);
|
||||
}
|
||||
|
||||
if ($accumulatedItems !== []) {
|
||||
// If previous items were in the new array, add them first
|
||||
$newItems[] = $this->createArrayItemFromArray($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->createArrayItemFromArray($accumulatedItems);
|
||||
}
|
||||
|
||||
return $newItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ArrayItem[] $arrayItems
|
||||
*/
|
||||
private function createArrayMergeFuncCall(array $arrayItems, Scope $scope): FuncCall
|
||||
{
|
||||
$args = array_map(function (ArrayItem $arrayItem) use ($scope): Arg {
|
||||
if ($arrayItem->unpack) {
|
||||
// Do not unpack anymore
|
||||
$arrayItem->unpack = false;
|
||||
return $this->createArgFromSpreadArrayItem($scope, $arrayItem);
|
||||
}
|
||||
|
||||
return new Arg($arrayItem);
|
||||
}, $arrayItems);
|
||||
|
||||
return new FuncCall(new Name('array_merge'), $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private function createVariableFromNonVariable(
|
||||
Array_ $array,
|
||||
ArrayItem $arrayItem,
|
||||
int $position,
|
||||
Scope $scope,
|
||||
File $file
|
||||
): Variable {
|
||||
// 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)
|
||||
$smartFileInfo = $file->getSmartFileInfo();
|
||||
$realPath = $smartFileInfo->getRealPath();
|
||||
$position = $this->lastPositionCurrentFile[$realPath] ?? $position;
|
||||
|
||||
$variableName = $this->variableNaming->resolveFromNodeWithScopeCountAndFallbackName(
|
||||
$array,
|
||||
$scope,
|
||||
'item' . $position . 'Unpacked'
|
||||
);
|
||||
|
||||
if ($this->shouldIncrement) {
|
||||
$this->lastPositionCurrentFile[$realPath] = ++$position;
|
||||
}
|
||||
|
||||
// Assign the value to the variable, and replace the element with the variable
|
||||
$newVariable = new Variable($variableName);
|
||||
|
||||
$newVariableAssign = new Assign($newVariable, $arrayItem->value);
|
||||
$this->nodesToAddCollector->addNodeBeforeNode($newVariableAssign, $array, $file->getSmartFileInfo());
|
||||
|
||||
return $newVariable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<ArrayItem|null> $items
|
||||
*/
|
||||
private function createArrayItemFromArray(array $items): ArrayItem
|
||||
{
|
||||
$array = new Array_($items);
|
||||
return new ArrayItem($array);
|
||||
}
|
||||
|
||||
private function createArgFromSpreadArrayItem(Scope $nodeScope, ArrayItem $arrayItem): Arg
|
||||
{
|
||||
// By now every item is a variable
|
||||
/** @var Variable $variable */
|
||||
$variable = $arrayItem->value;
|
||||
|
||||
$variableName = $this->nodeNameResolver->getName($variable) ?? '';
|
||||
|
||||
// If the variable is not in scope, it's one we just added.
|
||||
// Then get the type from the attribute
|
||||
if ($nodeScope->hasVariableType($variableName)->yes()) {
|
||||
$type = $nodeScope->getVariableType($variableName);
|
||||
} else {
|
||||
$originalNode = $arrayItem->getAttribute(AttributeKey::ORIGINAL_NODE);
|
||||
if ($originalNode instanceof ArrayItem) {
|
||||
$type = $nodeScope->getType($originalNode->value);
|
||||
} else {
|
||||
throw new ShouldNotHappenException();
|
||||
}
|
||||
}
|
||||
|
||||
$iteratorToArrayFuncCall = new FuncCall(new Name('iterator_to_array'), [new Arg($arrayItem)]);
|
||||
|
||||
// If we know it is an array, then print it directly
|
||||
// Otherwise PHPStan throws an error:
|
||||
// "Else branch is unreachable because ternary operator condition is always true."
|
||||
if ($type instanceof ArrayType) {
|
||||
return new Arg($arrayItem);
|
||||
}
|
||||
|
||||
// If it is iterable, then directly return `iterator_to_array`
|
||||
if ($this->isIterableType($type)) {
|
||||
return new Arg($iteratorToArrayFuncCall);
|
||||
}
|
||||
|
||||
// Print a ternary, handling either an array or an iterator
|
||||
$inArrayFuncCall = new FuncCall(new Name('is_array'), [new Arg($arrayItem)]);
|
||||
return new Arg(new Ternary($inArrayFuncCall, $arrayItem, $iteratorToArrayFuncCall));
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterables: objects declaring the interface Traversable,
|
||||
* For "iterable" type, it can be array
|
||||
*/
|
||||
private function isIterableType(Type $type): bool
|
||||
{
|
||||
if ($type instanceof IterableType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$traversableObjectType = new ObjectType('Traversable');
|
||||
return $traversableObjectType->isSuperTypeOf($type)
|
||||
->yes();
|
||||
}
|
||||
}
|
|
@ -10,16 +10,25 @@ use PhpParser\Node\Expr\ArrayItem;
|
|||
use PHPStan\Analyser\Scope;
|
||||
use PHPStan\Type\ArrayType;
|
||||
use PHPStan\Type\IntegerType;
|
||||
use Rector\DowngradePhp74\Rector\Array_\DowngradeArraySpreadRector;
|
||||
use Rector\Core\Rector\AbstractScopeAwareRector;
|
||||
use Rector\DowngradePhp81\NodeAnalyzer\ArraySpreadAnalyzer;
|
||||
use Rector\DowngradePhp81\NodeFactory\ArrayMergeFromArraySpreadFactory;
|
||||
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
|
||||
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
|
||||
|
||||
/**
|
||||
* @changelog https://wiki.php.net/rfc/array_unpacking_string_keys
|
||||
*
|
||||
* @see \Rector\Tests\DowngradePhp81\Rector\Array_\DowngradeArraySpreadStringKeyRector\DowngradeArraySpreadStringKeyRectorTest
|
||||
*/
|
||||
final class DowngradeArraySpreadStringKeyRector extends DowngradeArraySpreadRector
|
||||
final class DowngradeArraySpreadStringKeyRector extends AbstractScopeAwareRector
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ArrayMergeFromArraySpreadFactory $arrayMergeFromArraySpreadFactory,
|
||||
private readonly ArraySpreadAnalyzer $arraySpreadAnalyzer,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getRuleDefinition(): RuleDefinition
|
||||
{
|
||||
return new RuleDefinition(
|
||||
|
@ -27,29 +36,17 @@ final class DowngradeArraySpreadStringKeyRector extends DowngradeArraySpreadRect
|
|||
[
|
||||
new CodeSample(
|
||||
<<<'CODE_SAMPLE'
|
||||
class SomeClass
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
$parts = ['a' => 'b'];
|
||||
$parts2 = ['c' => 'd'];
|
||||
$parts = ['a' => 'b'];
|
||||
$parts2 = ['c' => 'd'];
|
||||
|
||||
$result = [...$parts, ...$parts2];
|
||||
}
|
||||
}
|
||||
$result = [...$parts, ...$parts2];
|
||||
CODE_SAMPLE
|
||||
,
|
||||
<<<'CODE_SAMPLE'
|
||||
class SomeClass
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
$parts = ['a' => 'b'];
|
||||
$parts2 = ['c' => 'd'];
|
||||
$parts = ['a' => 'b'];
|
||||
$parts2 = ['c' => 'd'];
|
||||
|
||||
$result = array_merge($parts, $parts2);
|
||||
}
|
||||
}
|
||||
$result = array_merge($parts, $parts2);
|
||||
CODE_SAMPLE
|
||||
),
|
||||
]
|
||||
|
@ -69,24 +66,24 @@ CODE_SAMPLE
|
|||
*/
|
||||
public function refactorWithScope(Node $node, Scope $scope): ?Node
|
||||
{
|
||||
if ($this->shouldSkip($node)) {
|
||||
if ($this->shouldSkipArray($node)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return parent::refactorWithScope($node, $scope);
|
||||
return $this->arrayMergeFromArraySpreadFactory->createFromArray($node, $scope, $this->file);
|
||||
}
|
||||
|
||||
private function shouldSkip(Array_ $array): bool
|
||||
private function shouldSkipArray(Array_ $array): bool
|
||||
{
|
||||
if (! $this->arraySpreadAnalyzer->isArrayWithUnpack($array)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($array->items as $item) {
|
||||
if (! $item instanceof ArrayItem) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $item->unpack) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $this->nodeTypeResolver->getType($item->value);
|
||||
if (! $type instanceof ArrayType) {
|
||||
continue;
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace Rector\Core\Tests\Issues\IssueDowngradeArraySpread\Fixture;
|
||||
|
||||
final class SkipPropertyDefault
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $defaultProperty = [];
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\Core\Tests\Issues\IssueDowngradeArraySpread;
|
||||
|
||||
use Iterator;
|
||||
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
|
||||
use Symplify\SmartFileSystem\SmartFileInfo;
|
||||
|
||||
/**
|
||||
* @see https://github.com/rectorphp/rector/issues/7112
|
||||
*/
|
||||
final class IssueDowngradeArraySpreadTest extends AbstractRectorTestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider provideData()
|
||||
*/
|
||||
public function test(SmartFileInfo $fileInfo): void
|
||||
{
|
||||
$this->doTestFileInfo($fileInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Iterator<SmartFileInfo>
|
||||
*/
|
||||
public function provideData(): Iterator
|
||||
{
|
||||
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
|
||||
}
|
||||
|
||||
public function provideConfigFilePath(): string
|
||||
{
|
||||
return __DIR__ . '/config/configured_rule.php';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Rector\Config\RectorConfig;
|
||||
use Rector\Core\ValueObject\PhpVersion;
|
||||
use Rector\DowngradePhp74\Rector\Array_\DowngradeArraySpreadRector;
|
||||
use Rector\DowngradePhp81\Rector\Array_\DowngradeArraySpreadStringKeyRector;
|
||||
|
||||
return static function (RectorConfig $rectorConfig): void {
|
||||
$rectorConfig->rule(DowngradeArraySpreadStringKeyRector::class);
|
||||
$rectorConfig->rule(DowngradeArraySpreadRector::class);
|
||||
|
||||
$rectorConfig->phpVersion(PhpVersion::PHP_81);
|
||||
};
|
Loading…
Reference in New Issue
Block a user