[PHP] Add AssignArrayToStringRector

This commit is contained in:
Tomas Votruba 2018-10-09 18:18:42 +08:00
parent 39c9c6982a
commit ebd9998b78
24 changed files with 535 additions and 1 deletions

View File

@ -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"
]
},

View File

@ -11,3 +11,4 @@ services:
Rector\Php\Rector\List_\ListSwapArrayOrderRector: ~
Rector\Php\Rector\FuncCall\CallUserFuncRector: ~
Rector\Php\Rector\FuncCall\EregToPregMatchRector: ~

View File

@ -4,3 +4,4 @@ services:
Object: 'BaseObject'
Rector\Php\Rector\TryCatch\MultiExceptionCatchRector: ~
Rector\Php\Rector\Assign\AssignArrayToStringRector: ~

View File

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

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

View File

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

View File

@ -0,0 +1,12 @@
<?php declare(strict_types=1);
$string = [];
$string[] = 1;
$string2 = [];
$someRubishInBetween = 1;
$string2[] = 1;
$string3 = $string;
$string3 = (array) $string3;
$string3[] = 1;

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
$array = [1, 2, 3];
$someString = [];
foreach ($array as $item) {
$someString[] = $item;
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
function someFunction() {
$array = [1, 2, 3];
$someString = [];
foreach ($array as $item) {
$someString[] = $item;
}
}

View File

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

View File

@ -0,0 +1,11 @@
<?php declare(strict_types=1);
$string = '';
$string[] = 1;
$string2 = '';
$someRubishInBetween = 1;
$string2[] = 1;
$string3 = $string;
$string3[] = 1;

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
<?php declare(strict_types=1);
$array = [1, 2, 3];
$someString = '';
foreach ($array as $item) {
$someString[] = $item;
}

View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
function someFunction() {
$array = [1, 2, 3];
$someString = '';
foreach ($array as $item) {
$someString[] = $item;
}
}

View File

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

View File

@ -0,0 +1,2 @@
services:
Rector\Php\Rector\Assign\AssignArrayToStringRector: ~

View File

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