[Downgrade] Separate multi level dependency in DowngradeArraySpreadRector (#2298)

This commit is contained in:
Tomas Votruba 2022-05-12 10:50:48 +02:00 committed by GitHub
parent 3d499125b8
commit 3bae3b9e75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 415 additions and 254 deletions

View File

@ -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) {

View File

@ -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;
}
}
}

View File

@ -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()
{

View File

@ -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'];
}
}
?>

View File

@ -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);
}
}

View 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;
}
}

View File

@ -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();
}
}

View File

@ -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;

View File

@ -0,0 +1,11 @@
<?php
namespace Rector\Core\Tests\Issues\IssueDowngradeArraySpread\Fixture;
final class SkipPropertyDefault
{
/**
* @var string[]
*/
private $defaultProperty = [];
}

View File

@ -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';
}
}

View File

@ -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);
};