From ebd9998b78a6e286da2164e3491dba3636b17219 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 9 Oct 2018 18:18:42 +0800 Subject: [PATCH] [PHP] Add AssignArrayToStringRector --- composer.json | 1 + config/level/php/php70.yml | 1 + config/level/php/php71.yml | 1 + .../NodeTypeResolver/src/NodeTypeAnalyzer.php | 4 +- .../Assign/AssignArrayToStringRector.php | 203 ++++++++++++++++++ .../AssignArrayToStringRectorTest.php | 41 ++++ .../Correct/correct.php.inc | 12 ++ .../Correct/correct2.php.inc | 17 ++ .../Correct/correct3.php.inc | 17 ++ .../Correct/correct4.php.inc | 25 +++ .../Correct/correct5.php.inc | 14 ++ .../Correct/correct6.php.inc | 8 + .../Correct/correct7.php.inc | 10 + .../Correct/correct8.php.inc | 16 ++ .../Wrong/wrong.php.inc | 11 + .../Wrong/wrong2.php.inc | 17 ++ .../Wrong/wrong3.php.inc | 17 ++ .../Wrong/wrong4.php.inc | 25 +++ .../Wrong/wrong5.php.inc | 14 ++ .../Wrong/wrong6.php.inc | 8 + .../Wrong/wrong7.php.inc | 10 + .../Wrong/wrong8.php.inc | 16 ++ .../AssignArrayToStringRector/config.yml | 2 + packages/Utils/src/BetterNodeFinder.php | 46 ++++ 24 files changed, 535 insertions(+), 1 deletion(-) create mode 100644 packages/Php/src/Rector/Assign/AssignArrayToStringRector.php create mode 100644 packages/Php/tests/Rector/Assign/AssignArrayToStringRector/AssignArrayToStringRectorTest.php create mode 100644 packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Correct/correct.php.inc create mode 100644 packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Correct/correct2.php.inc create mode 100644 packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Correct/correct3.php.inc create mode 100644 packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Correct/correct4.php.inc create mode 100644 packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Correct/correct5.php.inc create mode 100644 packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Correct/correct6.php.inc create mode 100644 packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Correct/correct7.php.inc create mode 100644 packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Correct/correct8.php.inc create mode 100644 packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Wrong/wrong.php.inc create mode 100644 packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Wrong/wrong2.php.inc create mode 100644 packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Wrong/wrong3.php.inc create mode 100644 packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Wrong/wrong4.php.inc create mode 100644 packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Wrong/wrong5.php.inc create mode 100644 packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Wrong/wrong6.php.inc create mode 100644 packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Wrong/wrong7.php.inc create mode 100644 packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Wrong/wrong8.php.inc create mode 100644 packages/Php/tests/Rector/Assign/AssignArrayToStringRector/config.yml diff --git a/composer.json b/composer.json index bcfdc2df9cf..62f26641ec9 100644 --- a/composer.json +++ b/composer.json @@ -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" ] }, diff --git a/config/level/php/php70.yml b/config/level/php/php70.yml index 41300f3d91b..06fd8fd1a99 100644 --- a/config/level/php/php70.yml +++ b/config/level/php/php70.yml @@ -11,3 +11,4 @@ services: Rector\Php\Rector\List_\ListSwapArrayOrderRector: ~ Rector\Php\Rector\FuncCall\CallUserFuncRector: ~ + Rector\Php\Rector\FuncCall\EregToPregMatchRector: ~ diff --git a/config/level/php/php71.yml b/config/level/php/php71.yml index 2811a89182e..a45d72ca708 100644 --- a/config/level/php/php71.yml +++ b/config/level/php/php71.yml @@ -4,3 +4,4 @@ services: Object: 'BaseObject' Rector\Php\Rector\TryCatch\MultiExceptionCatchRector: ~ + Rector\Php\Rector\Assign\AssignArrayToStringRector: ~ diff --git a/packages/NodeTypeResolver/src/NodeTypeAnalyzer.php b/packages/NodeTypeResolver/src/NodeTypeAnalyzer.php index dae746cb0b9..21c21fe6650 100644 --- a/packages/NodeTypeResolver/src/NodeTypeAnalyzer.php +++ b/packages/NodeTypeResolver/src/NodeTypeAnalyzer.php @@ -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; } } diff --git a/packages/Php/src/Rector/Assign/AssignArrayToStringRector.php b/packages/Php/src/Rector/Assign/AssignArrayToStringRector.php new file mode 100644 index 00000000000..d031d4192f7 --- /dev/null +++ b/packages/Php/src/Rector/Assign/AssignArrayToStringRector.php @@ -0,0 +1,203 @@ +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; + } +} diff --git a/packages/Php/tests/Rector/Assign/AssignArrayToStringRector/AssignArrayToStringRectorTest.php b/packages/Php/tests/Rector/Assign/AssignArrayToStringRector/AssignArrayToStringRectorTest.php new file mode 100644 index 00000000000..635ba52a5f3 --- /dev/null +++ b/packages/Php/tests/Rector/Assign/AssignArrayToStringRector/AssignArrayToStringRectorTest.php @@ -0,0 +1,41 @@ +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'; + } +} diff --git a/packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Correct/correct.php.inc b/packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Correct/correct.php.inc new file mode 100644 index 00000000000..940b279607e --- /dev/null +++ b/packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Correct/correct.php.inc @@ -0,0 +1,12 @@ +someString[] = 1; + + $this->anotherString[1] = 1; + } +} diff --git a/packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Correct/correct4.php.inc b/packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Correct/correct4.php.inc new file mode 100644 index 00000000000..23a5039c62e --- /dev/null +++ b/packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Correct/correct4.php.inc @@ -0,0 +1,25 @@ +property = []; + $this->property[] = 1; + } +} diff --git a/packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Correct/correct6.php.inc b/packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Correct/correct6.php.inc new file mode 100644 index 00000000000..10ee8f8c83c --- /dev/null +++ b/packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Correct/correct6.php.inc @@ -0,0 +1,8 @@ +someString[] = 1; + + $this->anotherString[1] = 1; + } +} diff --git a/packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Wrong/wrong4.php.inc b/packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Wrong/wrong4.php.inc new file mode 100644 index 00000000000..465e6c19b4b --- /dev/null +++ b/packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Wrong/wrong4.php.inc @@ -0,0 +1,25 @@ +property = ''; + $this->property[] = 1; + } +} diff --git a/packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Wrong/wrong6.php.inc b/packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Wrong/wrong6.php.inc new file mode 100644 index 00000000000..983ece48695 --- /dev/null +++ b/packages/Php/tests/Rector/Assign/AssignArrayToStringRector/Wrong/wrong6.php.inc @@ -0,0 +1,8 @@ +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; + } }