mirror of https://github.com/rectorphp/rector.git
224 lines
9.0 KiB
PHP
224 lines
9.0 KiB
PHP
<?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\MutatingScope;
|
|
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
|
|
{
|
|
/**
|
|
* @var bool
|
|
*/
|
|
private $shouldIncrement = \false;
|
|
/**
|
|
* Handle different result in CI
|
|
*
|
|
* @var array<string, int>
|
|
*/
|
|
private $lastPositionCurrentFile = [];
|
|
/**
|
|
* @readonly
|
|
* @var \Rector\Naming\Naming\VariableNaming
|
|
*/
|
|
private $variableNaming;
|
|
/**
|
|
* @readonly
|
|
* @var \Rector\Core\PhpParser\Node\BetterNodeFinder
|
|
*/
|
|
private $betterNodeFinder;
|
|
/**
|
|
* @readonly
|
|
* @var \Rector\PostRector\Collector\NodesToAddCollector
|
|
*/
|
|
private $nodesToAddCollector;
|
|
/**
|
|
* @readonly
|
|
* @var \Rector\NodeNameResolver\NodeNameResolver
|
|
*/
|
|
private $nodeNameResolver;
|
|
/**
|
|
* @readonly
|
|
* @var \Rector\DowngradePhp81\NodeAnalyzer\ArraySpreadAnalyzer
|
|
*/
|
|
private $arraySpreadAnalyzer;
|
|
public function __construct(VariableNaming $variableNaming, BetterNodeFinder $betterNodeFinder, NodesToAddCollector $nodesToAddCollector, NodeNameResolver $nodeNameResolver, ArraySpreadAnalyzer $arraySpreadAnalyzer)
|
|
{
|
|
$this->variableNaming = $variableNaming;
|
|
$this->betterNodeFinder = $betterNodeFinder;
|
|
$this->nodesToAddCollector = $nodesToAddCollector;
|
|
$this->nodeNameResolver = $nodeNameResolver;
|
|
$this->arraySpreadAnalyzer = $arraySpreadAnalyzer;
|
|
}
|
|
public function createFromArray(Array_ $array, MutatingScope $mutatingScope, 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, $mutatingScope, $file);
|
|
return $this->createArrayMergeFuncCall($newArrayItems, $mutatingScope);
|
|
}
|
|
/**
|
|
* 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, MutatingScope $mutatingScope, 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, $mutatingScope, $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, MutatingScope $mutatingScope) : FuncCall
|
|
{
|
|
$args = \array_map(function (ArrayItem $arrayItem) use($mutatingScope) : Arg {
|
|
if ($arrayItem->unpack) {
|
|
// Do not unpack anymore
|
|
$arrayItem->unpack = \false;
|
|
return $this->createArgFromSpreadArrayItem($mutatingScope, $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, MutatingScope $mutatingScope, 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)
|
|
$filePath = $file->getFilePath();
|
|
$position = $this->lastPositionCurrentFile[$filePath] ?? $position;
|
|
$variableName = $this->variableNaming->resolveFromNodeWithScopeCountAndFallbackName($array, $mutatingScope, 'item' . $position . 'Unpacked');
|
|
if ($this->shouldIncrement) {
|
|
$this->lastPositionCurrentFile[$filePath] = ++$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);
|
|
return $newVariable;
|
|
}
|
|
/**
|
|
* @param array<ArrayItem|null> $items
|
|
*/
|
|
private function createArrayItemFromArray(array $items) : ArrayItem
|
|
{
|
|
$array = new Array_($items);
|
|
return new ArrayItem($array);
|
|
}
|
|
private function createArgFromSpreadArrayItem(MutatingScope $mutatingScope, 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 ($mutatingScope->hasVariableType($variableName)->yes()) {
|
|
$type = $mutatingScope->getVariableType($variableName);
|
|
} else {
|
|
$originalNode = $arrayItem->getAttribute(AttributeKey::ORIGINAL_NODE);
|
|
if ($originalNode instanceof ArrayItem) {
|
|
$type = $mutatingScope->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();
|
|
}
|
|
}
|