mirror of
https://github.com/rectorphp/rector.git
synced 2024-06-25 20:32:35 +00:00
[PHP] Add AssignArrayToStringRector
This commit is contained in:
parent
39c9c6982a
commit
ebd9998b78
|
@ -182,6 +182,7 @@
|
|||
"packages/Php/tests/Rector/Property/TypedPropertyRector/Wrong",
|
||||
"packages/Php/tests/Rector/FunctionLike/ExceptionHandlerTypehintRector/Wrong",
|
||||
"packages/Php/tests/Rector/FunctionLike/Php4ConstructorRector/Wrong",
|
||||
"packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Wrong",
|
||||
"tests/Rector/Psr4/MultipleClassFileToPsr4ClassesRector/Source"
|
||||
]
|
||||
},
|
||||
|
|
|
@ -11,3 +11,4 @@ services:
|
|||
Rector\Php\Rector\List_\ListSwapArrayOrderRector: ~
|
||||
|
||||
Rector\Php\Rector\FuncCall\CallUserFuncRector: ~
|
||||
Rector\Php\Rector\FuncCall\EregToPregMatchRector: ~
|
||||
|
|
|
@ -4,3 +4,4 @@ services:
|
|||
Object: 'BaseObject'
|
||||
|
||||
Rector\Php\Rector\TryCatch\MultiExceptionCatchRector: ~
|
||||
Rector\Php\Rector\Assign\AssignArrayToStringRector: ~
|
||||
|
|
|
@ -19,6 +19,8 @@ final class NodeTypeAnalyzer
|
|||
/** @var Scope $nodeScope */
|
||||
$nodeScope = $node->getAttribute(Attribute::SCOPE);
|
||||
|
||||
return $nodeScope->getType($node) instanceof StringType;
|
||||
$nodeType = $nodeScope->getType($node);
|
||||
|
||||
return $nodeType instanceof StringType;
|
||||
}
|
||||
}
|
||||
|
|
203
packages/Php/src/Rector/Assign/AssignArrayToStringRector.php
Normal file
203
packages/Php/src/Rector/Assign/AssignArrayToStringRector.php
Normal file
|
@ -0,0 +1,203 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Rector\Php\Rector\Assign;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr;
|
||||
use PhpParser\Node\Expr\Array_;
|
||||
use PhpParser\Node\Expr\ArrayDimFetch;
|
||||
use PhpParser\Node\Expr\Assign;
|
||||
use PhpParser\Node\Expr\Cast\Array_ as ArrayCast;
|
||||
use PhpParser\Node\Expr\PropertyFetch;
|
||||
use PhpParser\Node\Expr\StaticPropertyFetch;
|
||||
use PhpParser\Node\Expr\Variable;
|
||||
use PhpParser\Node\Scalar\String_;
|
||||
use PhpParser\Node\Stmt\PropertyProperty;
|
||||
use Rector\NodeTypeResolver\NodeTypeAnalyzer;
|
||||
use Rector\Printer\BetterStandardPrinter;
|
||||
use Rector\Rector\AbstractRector;
|
||||
use Rector\RectorDefinition\CodeSample;
|
||||
use Rector\RectorDefinition\RectorDefinition;
|
||||
use Rector\Utils\BetterNodeFinder;
|
||||
use Rector\Utils\NodeTraverser\CallableNodeTraverser;
|
||||
|
||||
/**
|
||||
* This depends on the context. We need more real app datas.
|
||||
*
|
||||
* @see https://3v4l.org/ABDNv
|
||||
* @see https://stackoverflow.com/a/41000866/1348344
|
||||
*/
|
||||
final class AssignArrayToStringRector extends AbstractRector
|
||||
{
|
||||
/**
|
||||
* @var NodeTypeAnalyzer
|
||||
*/
|
||||
private $nodeTypeAnalyzer;
|
||||
|
||||
/**
|
||||
* @var CallableNodeTraverser
|
||||
*/
|
||||
private $callableNodeTraverser;
|
||||
|
||||
/**
|
||||
* @var PropertyProperty[]
|
||||
*/
|
||||
private $emptyStringPropertyNodes = [];
|
||||
|
||||
/**
|
||||
* @var BetterNodeFinder
|
||||
*/
|
||||
private $betterNodeFinder;
|
||||
|
||||
/**
|
||||
* @var BetterStandardPrinter
|
||||
*/
|
||||
private $betterStandardPrinter;
|
||||
|
||||
public function __construct(
|
||||
NodeTypeAnalyzer $nodeTypeAnalyzer,
|
||||
CallableNodeTraverser $callableNodeTraverser,
|
||||
BetterNodeFinder $betterNodeFinder,
|
||||
BetterStandardPrinter $betterStandardPrinter
|
||||
) {
|
||||
$this->nodeTypeAnalyzer = $nodeTypeAnalyzer;
|
||||
$this->callableNodeTraverser = $callableNodeTraverser;
|
||||
$this->betterNodeFinder = $betterNodeFinder;
|
||||
$this->betterStandardPrinter = $betterStandardPrinter;
|
||||
}
|
||||
|
||||
public function getDefinition(): RectorDefinition
|
||||
{
|
||||
return new RectorDefinition(
|
||||
'String cannot be turned into array by assignment anymore',
|
||||
[new CodeSample(
|
||||
<<<'CODE_SAMPLE'
|
||||
$string = '';
|
||||
$string[] = 1;
|
||||
CODE_SAMPLE
|
||||
,
|
||||
<<<'CODE_SAMPLE'
|
||||
$string = [];
|
||||
$string[] = 1;
|
||||
CODE_SAMPLE
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getNodeTypes(): array
|
||||
{
|
||||
return [Assign::class];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Assign $assignNode
|
||||
*/
|
||||
public function refactor(Node $assignNode): ?Node
|
||||
{
|
||||
// only array with no explicit key assign, e.g. "$value[] = 5";
|
||||
if (! $assignNode->var instanceof ArrayDimFetch || $assignNode->var->dim !== null) {
|
||||
return $assignNode;
|
||||
}
|
||||
|
||||
$arrayDimFetchNode = $assignNode->var;
|
||||
|
||||
/** @var Variable|PropertyFetch|StaticPropertyFetch|Expr $variableNode */
|
||||
$variableNode = $arrayDimFetchNode->var;
|
||||
|
||||
// set default value to property
|
||||
if ($variableNode instanceof PropertyFetch || $variableNode instanceof StaticPropertyFetch) {
|
||||
if ($this->processProperty($variableNode)) {
|
||||
return $assignNode;
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to variable, property or static property = '' set
|
||||
if ($this->processVariable($assignNode, $variableNode)) {
|
||||
return $assignNode;
|
||||
}
|
||||
|
||||
// there is "$string[] = ...;", which would cause error in PHP 7+
|
||||
// fallback - if no array init found, retype to (array)
|
||||
$retypeArrayAssignNode = new Assign($arrayDimFetchNode->var, new ArrayCast($arrayDimFetchNode->var));
|
||||
|
||||
$this->addNodeAfterNode(clone $assignNode, $assignNode);
|
||||
|
||||
return $retypeArrayAssignNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node[] $nodes
|
||||
*/
|
||||
public function beforeTraverse(array $nodes): void
|
||||
{
|
||||
// collect all known "{anything} = '';" assigns
|
||||
|
||||
$this->callableNodeTraverser->traverseNodesWithCallable($nodes, function (Node $node): void {
|
||||
if ($node instanceof PropertyProperty && $node->default && $this->isEmptyStringNode($node->default)) {
|
||||
$this->emptyStringPropertyNodes[] = $node;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function isEmptyStringNode(Node $node): bool
|
||||
{
|
||||
return $node instanceof String_ && $node->value === '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Variable|PropertyFetch|StaticPropertyFetch|Expr $variableNode
|
||||
*/
|
||||
private function processVariable(Assign $assignNode, Expr $variableNode): bool
|
||||
{
|
||||
if (! $this->nodeTypeAnalyzer->isStringType($variableNode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$variableNodeContent = $this->betterStandardPrinter->prettyPrint([$variableNode]);
|
||||
|
||||
$variableAssign = $this->betterNodeFinder->findFirstPrevious($assignNode, function (Node $node) use (
|
||||
$variableNodeContent
|
||||
) {
|
||||
if (! $node instanceof Assign) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->betterStandardPrinter->prettyPrint([$node->var]) !== $variableNodeContent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// we look for variable assign = string
|
||||
if (! $this->isEmptyStringNode($node->expr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if ($variableAssign instanceof Assign) {
|
||||
$variableAssign->expr = new Array_();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PropertyFetch|StaticPropertyFetch $propertyNode
|
||||
*/
|
||||
private function processProperty(Node $propertyNode): bool
|
||||
{
|
||||
foreach ($this->emptyStringPropertyNodes as $emptyStringPropertyNode) {
|
||||
if ((string) $emptyStringPropertyNode->name === (string) $propertyNode->name) {
|
||||
$emptyStringPropertyNode->default = new Array_();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Rector\Php\Tests\Rector\Assign\AssignArrayToStringRector;
|
||||
|
||||
use Iterator;
|
||||
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
|
||||
|
||||
/**
|
||||
* @covers \Rector\Php\Rector\Assign\AssignArrayToStringRector
|
||||
*/
|
||||
final class AssignArrayToStringRectorTest extends AbstractRectorTestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider provideWrongToFixedFiles()
|
||||
*/
|
||||
public function test(string $wrong, string $fixed): void
|
||||
{
|
||||
$this->doTestFileMatchesExpectedContent($wrong, $fixed);
|
||||
}
|
||||
|
||||
public function provideWrongToFixedFiles(): Iterator
|
||||
{
|
||||
// https://www.drupal.org/files/issues/adaptivetheme-php_string_cast_array-2832900-1.patch
|
||||
yield [__DIR__ . '/Wrong/wrong.php.inc', __DIR__ . '/Correct/correct.php.inc'];
|
||||
// https://www.saotn.org/fatal-error-operator-not-supported-strings-php-71/
|
||||
yield [__DIR__ . '/Wrong/wrong2.php.inc', __DIR__ . '/Correct/correct2.php.inc'];
|
||||
yield [__DIR__ . '/Wrong/wrong3.php.inc', __DIR__ . '/Correct/correct3.php.inc'];
|
||||
// https://github.com/benkeen/generatedata/issues/410
|
||||
yield [__DIR__ . '/Wrong/wrong4.php.inc', __DIR__ . '/Correct/correct4.php.inc'];
|
||||
// https://flexicontent.org/forum/30-feature-requests/55502-solved-an-error-has-occurred-0-operator-not-supported-for-strings.html
|
||||
yield [__DIR__ . '/Wrong/wrong5.php.inc', __DIR__ . '/Correct/correct5.php.inc'];
|
||||
yield [__DIR__ . '/Wrong/wrong6.php.inc', __DIR__ . '/Correct/correct6.php.inc'];
|
||||
yield [__DIR__ . '/Wrong/wrong7.php.inc', __DIR__ . '/Correct/correct7.php.inc'];
|
||||
yield [__DIR__ . '/Wrong/wrong8.php.inc', __DIR__ . '/Correct/correct8.php.inc'];
|
||||
}
|
||||
|
||||
protected function provideConfig(): string
|
||||
{
|
||||
return __DIR__ . '/config.yml';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
$string = [];
|
||||
$string[] = 1;
|
||||
|
||||
$string2 = [];
|
||||
$someRubishInBetween = 1;
|
||||
$string2[] = 1;
|
||||
|
||||
$string3 = $string;
|
||||
$string3 = (array) $string3;
|
||||
$string3[] = 1;
|
|
@ -0,0 +1,17 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Rector\Php\Tests\Rector\Assign\AssignArrayToStringRector\Wrong;
|
||||
|
||||
class WithSomeStaticString
|
||||
{
|
||||
static $someStaticString = [];
|
||||
|
||||
static $anotherStaticString = '';
|
||||
|
||||
public function run()
|
||||
{
|
||||
self::$someStaticString[] = 1;
|
||||
|
||||
self::$anotherStaticString[1] = 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Rector\Php\Tests\Rector\Assign\AssignArrayToStringRector\Wrong;
|
||||
|
||||
class WithSomeStaticString
|
||||
{
|
||||
public $someString = [];
|
||||
|
||||
public $anotherString = '';
|
||||
|
||||
public function run()
|
||||
{
|
||||
$this->someString[] = 1;
|
||||
|
||||
$this->anotherString[1] = 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Rector\Php\Tests\Rector\Assign\AssignArrayToStringRector\Wrong;
|
||||
|
||||
class WithSomeInMethod
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
$someString = [];
|
||||
$someString[] = 1;
|
||||
|
||||
$anotherString = '';
|
||||
$anotherString[1] = 1;
|
||||
}
|
||||
|
||||
public function fun()
|
||||
{
|
||||
$array = [1, 2, 3];
|
||||
|
||||
$someString = [];
|
||||
foreach ($array as $item) {
|
||||
$someString[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Rector\Php\Tests\Rector\Assign\AssignArrayToStringRector\Wrong;
|
||||
|
||||
class WithSomeInMethodPropertySetDefaultAbove
|
||||
{
|
||||
private $property;
|
||||
|
||||
public function run()
|
||||
{
|
||||
$this->property = [];
|
||||
$this->property[] = 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
$array = [1, 2, 3];
|
||||
|
||||
$someString = [];
|
||||
foreach ($array as $item) {
|
||||
$someString[] = $item;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
function someFunction() {
|
||||
$array = [1, 2, 3];
|
||||
|
||||
$someString = [];
|
||||
foreach ($array as $item) {
|
||||
$someString[] = $item;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Rector\Php\Tests\Rector\Assign\AssignArrayToStringRector\Wrong;
|
||||
|
||||
class WithSomeInMethodExpressions
|
||||
{
|
||||
public function fun()
|
||||
{
|
||||
$array = [1, 2, 3];
|
||||
|
||||
$someString = [];
|
||||
foreach ($array as $item) {
|
||||
$someString[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
$string = '';
|
||||
$string[] = 1;
|
||||
|
||||
$string2 = '';
|
||||
$someRubishInBetween = 1;
|
||||
$string2[] = 1;
|
||||
|
||||
$string3 = $string;
|
||||
$string3[] = 1;
|
|
@ -0,0 +1,17 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Rector\Php\Tests\Rector\Assign\AssignArrayToStringRector\Wrong;
|
||||
|
||||
class WithSomeStaticString
|
||||
{
|
||||
static $someStaticString = '';
|
||||
|
||||
static $anotherStaticString = '';
|
||||
|
||||
public function run()
|
||||
{
|
||||
self::$someStaticString[] = 1;
|
||||
|
||||
self::$anotherStaticString[1] = 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Rector\Php\Tests\Rector\Assign\AssignArrayToStringRector\Wrong;
|
||||
|
||||
class WithSomeStaticString
|
||||
{
|
||||
public $someString = '';
|
||||
|
||||
public $anotherString = '';
|
||||
|
||||
public function run()
|
||||
{
|
||||
$this->someString[] = 1;
|
||||
|
||||
$this->anotherString[1] = 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Rector\Php\Tests\Rector\Assign\AssignArrayToStringRector\Wrong;
|
||||
|
||||
class WithSomeInMethod
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
$someString = '';
|
||||
$someString[] = 1;
|
||||
|
||||
$anotherString = '';
|
||||
$anotherString[1] = 1;
|
||||
}
|
||||
|
||||
public function fun()
|
||||
{
|
||||
$array = [1, 2, 3];
|
||||
|
||||
$someString = '';
|
||||
foreach ($array as $item) {
|
||||
$someString[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Rector\Php\Tests\Rector\Assign\AssignArrayToStringRector\Wrong;
|
||||
|
||||
class WithSomeInMethodPropertySetDefaultAbove
|
||||
{
|
||||
private $property;
|
||||
|
||||
public function run()
|
||||
{
|
||||
$this->property = '';
|
||||
$this->property[] = 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
$array = [1, 2, 3];
|
||||
|
||||
$someString = '';
|
||||
foreach ($array as $item) {
|
||||
$someString[] = $item;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
function someFunction() {
|
||||
$array = [1, 2, 3];
|
||||
|
||||
$someString = '';
|
||||
foreach ($array as $item) {
|
||||
$someString[] = $item;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Rector\Php\Tests\Rector\Assign\AssignArrayToStringRector\Wrong;
|
||||
|
||||
class WithSomeInMethodExpressions
|
||||
{
|
||||
public function fun()
|
||||
{
|
||||
$array = [1, 2, 3];
|
||||
|
||||
$someString = '';
|
||||
foreach ($array as $item) {
|
||||
$someString[] = $item;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
services:
|
||||
Rector\Php\Rector\Assign\AssignArrayToStringRector: ~
|
|
@ -3,6 +3,7 @@
|
|||
namespace Rector\Utils;
|
||||
|
||||
use PhpParser\Node;
|
||||
use PhpParser\Node\Stmt\Expression;
|
||||
use PhpParser\NodeFinder;
|
||||
use Rector\NodeTypeResolver\Node\Attribute;
|
||||
|
||||
|
@ -80,4 +81,49 @@ final class BetterNodeFinder
|
|||
{
|
||||
return $this->nodeFinder->findFirst($nodes, $filter);
|
||||
}
|
||||
|
||||
public function findFirstPrevious(Node $node, callable $filter): ?Node
|
||||
{
|
||||
if (! $node instanceof Expression) {
|
||||
$expression = $node->getAttribute(Attribute::PARENT_NODE);
|
||||
if ($expression instanceof Expression) {
|
||||
$node = $expression;
|
||||
}
|
||||
}
|
||||
|
||||
$foundNode = $this->findFirst([$node], $filter);
|
||||
// we found what we need
|
||||
if ($foundNode) {
|
||||
return $foundNode;
|
||||
}
|
||||
|
||||
// move to next expression
|
||||
$previousExpression = $this->getPreviousExpression($node);
|
||||
if ($previousExpression === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->findFirstPrevious($previousExpression, $filter);
|
||||
}
|
||||
|
||||
private function getPreviousExpression(Node $node): ?Expression
|
||||
{
|
||||
$previousExpression = $node->getAttribute(Attribute::PREVIOUS_NODE);
|
||||
|
||||
while (! $previousExpression instanceof Expression && $previousExpression !== null) {
|
||||
$previousExpression = $previousExpression->getAttribute(Attribute::PREVIOUS_NODE);
|
||||
if ($previousExpression instanceof Expression) {
|
||||
return $previousExpression;
|
||||
}
|
||||
|
||||
if ($previousExpression instanceof Node) {
|
||||
$previousExpression = $previousExpression->getAttribute(Attribute::PARENT_NODE);
|
||||
if ($previousExpression instanceof Expression) {
|
||||
return $previousExpression;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $previousExpression;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user