diff --git a/config/set/coding-style/coding-style.yaml b/config/set/coding-style/coding-style.yaml index 875bdcf60aaa..e86ca88c4db2 100644 --- a/config/set/coding-style/coding-style.yaml +++ b/config/set/coding-style/coding-style.yaml @@ -25,3 +25,4 @@ services: Rector\CodingStyle\Rector\ClassConst\VarConstantCommentRector: ~ Rector\CodingStyle\Rector\Encapsed\EncapsedStringsToSprintfRector: ~ Rector\CodingStyle\Rector\ClassMethod\NewlineBeforeNewAssignSetRector: ~ + Rector\CodingStyle\Rector\String_\ManualJsonStringToJsonEncodeArrayRector: ~ diff --git a/ecs.yaml b/ecs.yaml index 8a33bd23ba9a..ccc4942f5e36 100644 --- a/ecs.yaml +++ b/ecs.yaml @@ -27,6 +27,7 @@ services: - 'PHPStan\Analyser\Scope' - 'PhpParser\NodeVisitor\NameResolver' - 'PhpParser\Node\*' + - '*Data' - 'PhpParser\Comment' - 'PhpParser\Lexer' - 'PhpParser\Comment\Doc' @@ -156,3 +157,7 @@ parameters: Symplify\CodingStandard\Sniffs\DependencyInjection\NoClassInstantiationSniff: # 3rd party api - 'src/PhpParser/Node/Value/ValueResolver.php' + + PhpCsFixer\Fixer\PhpUnit\PhpUnitStrictFixer: + # intentional equals + - 'tests/PhpParser/Node/NodeFactoryTest.php' diff --git a/packages/CodingStyle/src/Node/ConcatJoiner.php b/packages/CodingStyle/src/Node/ConcatJoiner.php new file mode 100644 index 000000000000..7ac9ab087888 --- /dev/null +++ b/packages/CodingStyle/src/Node/ConcatJoiner.php @@ -0,0 +1,59 @@ +getAttribute(AttributeKey::PARENT_NODE) instanceof Concat) { + $this->reset(); + } + + $this->processConcatSide($concat->left); + $this->processConcatSide($concat->right); + + return [$this->content, $this->placeholderNodes]; + } + + private function processConcatSide(Expr $expr): void + { + if ($expr instanceof String_) { + $this->content .= $expr->value; + } elseif ($expr instanceof Concat) { + $this->joinToStringAndPlaceholderNodes($expr); + } else { + $objectHash = spl_object_hash($expr); + $this->placeholderNodes[$objectHash] = $expr; + + $this->content .= $objectHash; + } + } + + private function reset(): void + { + $this->content = ''; + $this->placeholderNodes = []; + } +} diff --git a/packages/CodingStyle/src/Node/ConcatManipulator.php b/packages/CodingStyle/src/Node/ConcatManipulator.php new file mode 100644 index 000000000000..bb461d4dc3b3 --- /dev/null +++ b/packages/CodingStyle/src/Node/ConcatManipulator.php @@ -0,0 +1,67 @@ +betterStandardPrinter = $betterStandardPrinter; + $this->callableNodeTraverser = $callableNodeTraverser; + } + + public function getFirstConcatItem(Concat $concat): Node + { + // go to the deep, until there is no concat + while ($concat->left instanceof Concat) { + $concat = $concat->left; + } + + return $concat->left; + } + + public function removeFirstItemFromConcat(Concat $concat): Node + { + // just 2 items, return right one + if (! $concat->left instanceof Concat) { + return $concat->right; + } + + $newConcat = clone $concat; + $firstConcatItem = $this->getFirstConcatItem($concat); + + $this->callableNodeTraverser->traverseNodesWithCallable([$newConcat], function (Node $node) use ( + $firstConcatItem + ): ?Expr { + if (! $node instanceof Concat) { + return null; + } + + if (! $this->betterStandardPrinter->areNodesEqual($node->left, $firstConcatItem)) { + return null; + } + + return $node->right; + }); + + return $newConcat; + } +} diff --git a/packages/CodingStyle/src/Rector/String_/ManualJsonStringToJsonEncodeArrayRector.php b/packages/CodingStyle/src/Rector/String_/ManualJsonStringToJsonEncodeArrayRector.php new file mode 100644 index 000000000000..a9e1366a250a --- /dev/null +++ b/packages/CodingStyle/src/Rector/String_/ManualJsonStringToJsonEncodeArrayRector.php @@ -0,0 +1,300 @@ +concatJoiner = $concatJoiner; + $this->concatManipulator = $concatManipulator; + } + + public function getDefinition(): RectorDefinition + { + return new RectorDefinition('Add extra space before new assign set', [ + new CodeSample( + <<<'CODE_SAMPLE' +final class SomeClass +{ + public function run() + { + $someJsonAsString = '{"role_name":"admin","numberz":{"id":"10"}}'; + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +final class SomeClass +{ + public function run() + { + $data = [ + 'role_name' => 'admin', + 'numberz' => ['id' => 10] + ]; + + $someJsonAsString = json_encode($data); + } +} +CODE_SAMPLE + ), + ]); + } + + /** + * @return string[] + */ + public function getNodeTypes(): array + { + return [Assign::class]; + } + + /** + * @param Assign $node + */ + public function refactor(Node $node): ?Node + { + if ($node->expr instanceof String_) { + $stringValue = $node->expr->value; + + // A. full json string + $isJsonString = $this->isJsonString($stringValue); + if ($isJsonString) { + return $this->processJsonString($node, $stringValue); + } + + // B. just start of a json? + $currentNode = $node; + + $concatExpressionJoinData = $this->collectContentAndPlaceholderNodesFromNextExpressions( + $node, + $currentNode + ); + + $stringValue .= $concatExpressionJoinData->getString(); + if (! $this->isJsonString($stringValue)) { + return null; + } + + // remove nodes + $this->removeNodes($concatExpressionJoinData->getNodesToRemove()); + + $jsonArray = $this->createArrayNodeFromJsonString($stringValue); + + $this->replaceNodeObjectHashPlaceholdersWithNodes( + $jsonArray, + $concatExpressionJoinData->getPlaceholdersToNodes() + ); + + return $this->createAndReturnJsonEncodeFromArray($node, $jsonArray); + } + + if ($node->expr instanceof Concat) { + // process only first concat + $concatParentNode = $node->getAttribute(AttributeKey::PARENT_NODE); + if ($concatParentNode instanceof Concat) { + return null; + } + + /** @var Expr[] $placeholderNodes */ + [$content, $placeholderNodes] = $this->concatJoiner->joinToStringAndPlaceholderNodes($node->expr); + + /** @var string $content */ + if (! $this->isJsonString($content)) { + return null; + } + + $jsonArray = $this->createArrayNodeFromJsonString($content); + $this->replaceNodeObjectHashPlaceholdersWithNodes($jsonArray, $placeholderNodes); + + return $this->createAndReturnJsonEncodeFromArray($node, $jsonArray); + } + + return null; + } + + private function processJsonString(Assign $assign, string $stringValue): Node + { + $arrayNode = $this->createArrayNodeFromJsonString($stringValue); + + return $this->createAndReturnJsonEncodeFromArray($assign, $arrayNode); + } + + private function isJsonString(string $stringValue): bool + { + if (! (bool) Strings::match($stringValue, '#{(.*?\:.*?)}#')) { + return false; + } + + try { + return (bool) Json::decode($stringValue, Json::FORCE_ARRAY); + } catch (JsonException $jsonException) { + return false; + } + } + + /** + * Creates + adds + * + * $jsonData = ['...']; + * $json = Nette\Utils\Json::encode($jsonData); + */ + private function createAndReturnJsonEncodeFromArray(Assign $assign, Array_ $jsonArray): Assign + { + $jsonDataVariable = new Variable('jsonData'); + + $jsonDataAssign = new Assign($jsonDataVariable, $jsonArray); + $this->addNodeBeforeNode($jsonDataAssign, $assign); + + $assign->expr = $this->createStaticCall('Nette\Utils\Json', 'encode', [$jsonDataVariable]); + + return $assign; + } + + /** + * @param Expr[] $placeholderNodes + */ + private function replaceNodeObjectHashPlaceholdersWithNodes(Array_ $array, array $placeholderNodes): void + { + // traverse and replace placeholdes by original nodes + $this->traverseNodesWithCallable([$array], function (Node $node) use ($placeholderNodes): ?Expr { + if (! $node instanceof String_) { + return null; + } + + $stringValue = $node->value; + + return $placeholderNodes[$stringValue] ?? null; + }); + } + + /** + * @param Assign|ConcatAssign $currentNode + * @return Node[]|null + */ + private function matchNextExpressionAssignConcatToSameVariable(Expr $expr, Node $currentNode): ?array + { + $nextExpression = $this->getNextExpression($currentNode); + if (! $nextExpression instanceof Node\Stmt\Expression) { + return null; + } + + $nextExpressionNode = $nextExpression->expr; + + // $value .= '...'; + if ($nextExpressionNode instanceof ConcatAssign) { + // is assign to same variable? + if (! $this->areNodesEqual($expr, $nextExpressionNode->var)) { + return null; + } + + return [$nextExpressionNode, $nextExpressionNode->expr]; + } + + // $value = $value . '...'; + if ($nextExpressionNode instanceof Assign) { + if (! $nextExpressionNode->expr instanceof Concat) { + return null; + } + + // is assign to same variable? + if (! $this->areNodesEqual($expr, $nextExpressionNode->var)) { + return null; + } + + $firstConcatItem = $this->concatManipulator->getFirstConcatItem($nextExpressionNode->expr); + + // is the first concat the same variable + if (! $this->areNodesEqual($expr, $firstConcatItem)) { + return null; + } + + // return all but first node + $allButFirstConcatItem = $this->concatManipulator->removeFirstItemFromConcat($nextExpressionNode->expr); + + return [$nextExpressionNode, $allButFirstConcatItem]; + } + + return null; + } + + private function createArrayNodeFromJsonString(string $stringValue): Array_ + { + $array = Json::decode($stringValue, Json::FORCE_ARRAY); + + return $this->createArray($array); + } + + private function collectContentAndPlaceholderNodesFromNextExpressions( + Assign $assign, + Node $currentNode + ): ConcatExpressionJoinData { + $concatExpressionJoinData = new ConcatExpressionJoinData(); + + while ([$nodeToRemove, $valueNode] = $this->matchNextExpressionAssignConcatToSameVariable( + $assign->var, + $currentNode + )) { + if ($valueNode instanceof String_) { + $concatExpressionJoinData->addString($valueNode->value); + } elseif ($valueNode instanceof Concat) { + /** @var Expr[] $newPlaceholderNodes */ + [$content, $newPlaceholderNodes] = $this->concatJoiner->joinToStringAndPlaceholderNodes($valueNode); + /** @var string $content */ + $concatExpressionJoinData->addString($content); + + foreach ($newPlaceholderNodes as $placeholder => $expr) { + /** @var string $placeholder */ + $concatExpressionJoinData->addPlaceholderToNode($placeholder, $expr); + } + } elseif ($valueNode instanceof Expr) { + $objectHash = spl_object_hash($valueNode); + $concatExpressionJoinData->addString($objectHash); + $concatExpressionJoinData->addPlaceholderToNode($objectHash, $valueNode); + } + + $concatExpressionJoinData->addNodeToRemove($nodeToRemove); + + // jump to next one + $currentNode = $this->getNextExpression($currentNode); + if ($currentNode === null) { + return $concatExpressionJoinData; + } + } + + return $concatExpressionJoinData; + } +} diff --git a/packages/CodingStyle/src/ValueObject/ConcatExpressionJoinData.php b/packages/CodingStyle/src/ValueObject/ConcatExpressionJoinData.php new file mode 100644 index 000000000000..07f5cfa75bfc --- /dev/null +++ b/packages/CodingStyle/src/ValueObject/ConcatExpressionJoinData.php @@ -0,0 +1,60 @@ +values[] = $value; + } + + public function addNodeToRemove(Node $node): void + { + $this->nodesToRemove[] = $node; + } + + public function getString(): string + { + return implode('', $this->values); + } + + /** + * @return Node[] + */ + public function getNodesToRemove(): array + { + return $this->nodesToRemove; + } + + public function addPlaceholderToNode(string $objectHash, Expr $expr): void + { + $this->placeholdersToNodes[$objectHash] = $expr; + } + + /** + * @return Expr[] + */ + public function getPlaceholdersToNodes(): array + { + return $this->placeholdersToNodes; + } +} diff --git a/packages/CodingStyle/tests/Rector/String_/ManualJsonStringToJsonEncodeArrayRector/Fixture/assign_with_concat.php.inc b/packages/CodingStyle/tests/Rector/String_/ManualJsonStringToJsonEncodeArrayRector/Fixture/assign_with_concat.php.inc new file mode 100644 index 000000000000..1ba7dedd79e6 --- /dev/null +++ b/packages/CodingStyle/tests/Rector/String_/ManualJsonStringToJsonEncodeArrayRector/Fixture/assign_with_concat.php.inc @@ -0,0 +1,41 @@ + +----- + 'admin', 'numberz' => ['id' => 5]]; + $someJsonAsString = \Nette\Utils\Json::encode($jsonData); + } + + public function fun() + { + $jsonData = ['role_name' => 'admin', 'numberz' => ['id' => '5']]; + $someJsonAsString = \Nette\Utils\Json::encode($jsonData); + } +} + +?> diff --git a/packages/CodingStyle/tests/Rector/String_/ManualJsonStringToJsonEncodeArrayRector/Fixture/concat_json.php.inc b/packages/CodingStyle/tests/Rector/String_/ManualJsonStringToJsonEncodeArrayRector/Fixture/concat_json.php.inc new file mode 100644 index 000000000000..0938c2f27c64 --- /dev/null +++ b/packages/CodingStyle/tests/Rector/String_/ManualJsonStringToJsonEncodeArrayRector/Fixture/concat_json.php.inc @@ -0,0 +1,28 @@ + +----- + 'admin', 'numberz' => ['id' => 5]]; + $someJsonAsString = \Nette\Utils\Json::encode($jsonData); + } +} + +?> diff --git a/packages/CodingStyle/tests/Rector/String_/ManualJsonStringToJsonEncodeArrayRector/Fixture/fixture.php.inc b/packages/CodingStyle/tests/Rector/String_/ManualJsonStringToJsonEncodeArrayRector/Fixture/fixture.php.inc new file mode 100644 index 000000000000..e5027c9943a4 --- /dev/null +++ b/packages/CodingStyle/tests/Rector/String_/ManualJsonStringToJsonEncodeArrayRector/Fixture/fixture.php.inc @@ -0,0 +1,28 @@ + +----- + 'admin', 'numberz' => ['id' => '10']]; + $someJsonAsString = \Nette\Utils\Json::encode($jsonData); + } +} + +?> diff --git a/packages/CodingStyle/tests/Rector/String_/ManualJsonStringToJsonEncodeArrayRector/Fixture/multiline_concat_json.php.inc b/packages/CodingStyle/tests/Rector/String_/ManualJsonStringToJsonEncodeArrayRector/Fixture/multiline_concat_json.php.inc new file mode 100644 index 000000000000..da5a2cab7d64 --- /dev/null +++ b/packages/CodingStyle/tests/Rector/String_/ManualJsonStringToJsonEncodeArrayRector/Fixture/multiline_concat_json.php.inc @@ -0,0 +1,41 @@ + +----- + 'admin', 'numberz' => ['id' => 5]]; + $someJsonAsString = \Nette\Utils\Json::encode($jsonData); + } + + public function fun() + { + $jsonData = ['role_name' => 'admin', 'numberz' => ['id' => '5']]; + $someJsonAsString = \Nette\Utils\Json::encode($jsonData); + } +} + +?> diff --git a/packages/CodingStyle/tests/Rector/String_/ManualJsonStringToJsonEncodeArrayRector/Fixture/tripleline_multiline_concat_json.php.inc b/packages/CodingStyle/tests/Rector/String_/ManualJsonStringToJsonEncodeArrayRector/Fixture/tripleline_multiline_concat_json.php.inc new file mode 100644 index 000000000000..596d75336b64 --- /dev/null +++ b/packages/CodingStyle/tests/Rector/String_/ManualJsonStringToJsonEncodeArrayRector/Fixture/tripleline_multiline_concat_json.php.inc @@ -0,0 +1,30 @@ + +----- + 'admin', 'numberz' => ['id' => 5]]; + $someJsonAsString = \Nette\Utils\Json::encode($jsonData); + } +} + +?> diff --git a/packages/CodingStyle/tests/Rector/String_/ManualJsonStringToJsonEncodeArrayRector/ManualJsonStringToJsonEncodeArrayRectorTest.php b/packages/CodingStyle/tests/Rector/String_/ManualJsonStringToJsonEncodeArrayRector/ManualJsonStringToJsonEncodeArrayRectorTest.php new file mode 100644 index 000000000000..e005ed75fe33 --- /dev/null +++ b/packages/CodingStyle/tests/Rector/String_/ManualJsonStringToJsonEncodeArrayRector/ManualJsonStringToJsonEncodeArrayRectorTest.php @@ -0,0 +1,25 @@ +doTestFiles([ + __DIR__ . '/Fixture/fixture.php.inc', + __DIR__ . '/Fixture/concat_json.php.inc', + __DIR__ . '/Fixture/multiline_concat_json.php.inc', + __DIR__ . '/Fixture/tripleline_multiline_concat_json.php.inc', + __DIR__ . '/Fixture/assign_with_concat.php.inc', + ]); + } + + protected function getRectorClass(): string + { + return ManualJsonStringToJsonEncodeArrayRector::class; + } +} diff --git a/packages/Nette/src/Rector/Identical/EndsWithFunctionToNetteUtilsStringsRector.php b/packages/Nette/src/Rector/Identical/EndsWithFunctionToNetteUtilsStringsRector.php index e5a4710a3796..235b6e880f7f 100644 --- a/packages/Nette/src/Rector/Identical/EndsWithFunctionToNetteUtilsStringsRector.php +++ b/packages/Nette/src/Rector/Identical/EndsWithFunctionToNetteUtilsStringsRector.php @@ -3,7 +3,6 @@ namespace Rector\Nette\Rector\Identical; use PhpParser\Node; -use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\BinaryOp\NotIdentical; @@ -84,8 +83,8 @@ public function refactor(Node $node): ?Node // starts with $startsWithStaticCall = $this->createStaticCall('Nette\Utils\Strings', 'endsWith', [ - new Arg($contentNode), - new Arg($needleNode), + $contentNode, + $needleNode, ]); if ($node instanceof NotIdentical) { diff --git a/packages/Nette/src/Rector/Identical/StartsWithFunctionToNetteUtilsStringsRector.php b/packages/Nette/src/Rector/Identical/StartsWithFunctionToNetteUtilsStringsRector.php index 9e21411c4c26..6707d5316c75 100644 --- a/packages/Nette/src/Rector/Identical/StartsWithFunctionToNetteUtilsStringsRector.php +++ b/packages/Nette/src/Rector/Identical/StartsWithFunctionToNetteUtilsStringsRector.php @@ -3,7 +3,6 @@ namespace Rector\Nette\Rector\Identical; use PhpParser\Node; -use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\BinaryOp\NotIdentical; @@ -83,8 +82,8 @@ public function refactor(Node $node): ?Node // starts with $startsWithStaticCall = $this->createStaticCall('Nette\Utils\Strings', 'startsWith', [ - new Arg($contentNode), - new Arg($needleNode), + $contentNode, + $needleNode, ]); if ($node instanceof NotIdentical) { diff --git a/packages/Php/src/Rector/FuncCall/ArrayKeyFirstLastRector.php b/packages/Php/src/Rector/FuncCall/ArrayKeyFirstLastRector.php index 813dc199b38c..f4fb1ffe27f6 100644 --- a/packages/Php/src/Rector/FuncCall/ArrayKeyFirstLastRector.php +++ b/packages/Php/src/Rector/FuncCall/ArrayKeyFirstLastRector.php @@ -5,8 +5,6 @@ use PhpParser\Node; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; -use PhpParser\Node\Stmt\Expression; -use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\Rector\AbstractRector; use Rector\RectorDefinition\CodeSample; use Rector\RectorDefinition\RectorDefinition; @@ -118,17 +116,6 @@ public function refactor(Node $node): ?Node return $node; } - private function getNextExpression(Node $node): ?Node - { - /** @var Expression|null $currentExpression */ - $currentExpression = $node->getAttribute(AttributeKey::CURRENT_EXPRESSION); - if ($currentExpression === null) { - return null; - } - - return $currentExpression->getAttribute(AttributeKey::NEXT_NODE); - } - private function shouldSkip(FuncCall $funcCall): bool { if ($this->isAtLeastPhpVersion('7.3')) { diff --git a/packages/Silverstripe/src/Rector/ConstantToStaticCallRector.php b/packages/Silverstripe/src/Rector/ConstantToStaticCallRector.php index 815e275c79d4..00294ba76138 100644 --- a/packages/Silverstripe/src/Rector/ConstantToStaticCallRector.php +++ b/packages/Silverstripe/src/Rector/ConstantToStaticCallRector.php @@ -4,7 +4,6 @@ use Nette\Utils\Strings; use PhpParser\Node; -use PhpParser\Node\Arg; use PhpParser\Node\Expr\ConstFetch; use PhpParser\Node\Scalar\String_; use Rector\Rector\AbstractRector; @@ -42,6 +41,6 @@ public function refactor(Node $node): ?Node return null; } - return $this->createStaticCall('Environment', 'getEnv', [new Arg(new String_($constantName))]); + return $this->createStaticCall('Environment', 'getEnv', [new String_($constantName)]); } } diff --git a/phpstan.neon b/phpstan.neon index 1013e91730da..c46db93f37df 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -177,6 +177,15 @@ parameters: - '#Parameter \#1 \$error_handler of function set_error_handler expects \(callable\)\|null, Closure\(mixed, mixed, mixed, mixed\)\: void given#' - '#Parameter \#1 \$error_handler of function set_error_handler expects \(callable\)\|null, callable given#' - '#Method Rector\\NodeTypeResolver\\PerNodeTypeResolver\\NameTypeResolver\:\:resolveFullyQualifiedName\(\) should return string\|null but returns PhpParser\\Node\\Name\|null#' + - '#Parameter \#1 \$json of static method Nette\\Utils\\Json\:\:decode\(\) expects string, array\|string given#' + - '#Parameter \#1 \$stringValue of method Rector\\CodingStyle\\Rector\\String_\\ManualJsonStringToJsonEncodeArrayRector\:\:isJsonString\(\) expects string, array\|string given#' + - '#In method "Rector\\(.*?)", (.*?) is "array"\. Please provide a (.*?) annotation to further specify the type of the array\. For instance\: (.*?). More info\: http\://bit\.ly/typehintarray#' + - '#Method Rector\\Rector\\AbstractRector\:\:wrapToArg\(\) should return array but returns array#' + - '#Parameter \#1 \$node of method Rector\\Rector\\AbstractRector\:\:getNextExpression\(\) expects PhpParser\\Node, PhpParser\\Node\|null given#' + - '#Parameter \#2 \$currentNode of method Rector\\CodingStyle\\Rector\\String_\\ManualJsonStringToJsonEncodeArrayRector\:\:matchNextExpressionAssignConcatToSameVariable\(\) expects PhpParser\\Node\\Expr\\Assign\|PhpParser\\Node\\Expr\\AssignOp\\Concat, PhpParser\\Node given#' + - '#Parameter \#1 \$nodes of method Rector\\Rector\\AbstractRector\:\:removeNodes\(\) expects array, array given#' + - '#Parameter \#1 \$node of method Rector\\CodingStyle\\ValueObject\\ConcatExpressionJoinData\:\:addNodeToRemove\(\) expects PhpParser\\Node, PhpParser\\Node\|null given#' + - '#Method Rector\\FileSystemRector\\Rector\\AbstractFileSystemRector\:\:wrapToArg\(\) should return array but returns array#' # array is callable - '#Parameter \#2 \$listener of method Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher\:\:getListenerPriority\(\) expects callable\(\)\: mixed, array given#' diff --git a/src/PhpParser/Node/Commander/NodeAddingCommander.php b/src/PhpParser/Node/Commander/NodeAddingCommander.php index 42eeebbc8af1..dd94797a0a61 100644 --- a/src/PhpParser/Node/Commander/NodeAddingCommander.php +++ b/src/PhpParser/Node/Commander/NodeAddingCommander.php @@ -33,6 +33,11 @@ final class NodeAddingCommander implements CommanderInterface */ private $nodesToAdd = []; + /** + * @var Stmt[][] + */ + private $nodesToAddBefore = []; + /** * @var BetterNodeFinder */ @@ -43,11 +48,19 @@ public function __construct(BetterNodeFinder $betterNodeFinder) $this->betterNodeFinder = $betterNodeFinder; } - public function addNodeAfterNode(Node $node, Node $positionNode): void + public function addNodeBeforeNode(Node $addedNode, Node $positionNode): void + { + $position = $this->resolveNearestExpressionPosition($positionNode); + + // dump($this->wrapToExpression($addedNode)); + $this->nodesToAddBefore[$position][] = $this->wrapToExpression($addedNode); + } + + public function addNodeAfterNode(Node $addedNode, Node $positionNode): void { $position = $this->resolveNearestExpressionPosition($positionNode); - $this->nodesToAdd[$position][] = $this->wrapToExpression($node); + $this->nodesToAdd[$position][] = $this->wrapToExpression($addedNode); } /** @@ -60,14 +73,14 @@ public function traverseNodes(array $nodes): array $nodeTraverser->addVisitor($this->createNodeVisitor()); // new nodes to remove are always per traverse - $this->nodesToAdd = []; + $this->reset(); return $nodeTraverser->traverse($nodes); } public function isActive(): bool { - return count($this->nodesToAdd) > 0; + return count($this->nodesToAdd) > 0 || count($this->nodesToAddBefore) > 0; } private function resolveNearestExpressionPosition(Node $node): string @@ -100,18 +113,25 @@ private function wrapToExpression(Node $node): Stmt private function createNodeVisitor(): NodeVisitor { - return new class($this->nodesToAdd) extends NodeVisitorAbstract { + return new class($this->nodesToAdd, $this->nodesToAddBefore) extends NodeVisitorAbstract { /** * @var Stmt[][] */ private $nodesToAdd = []; + /** + * @var Stmt[][] + */ + private $nodesToAddBefore = []; + /** * @param Stmt[][] $nodesToAdd + * @param Stmt[][] $nodesToAddBefore */ - public function __construct(array $nodesToAdd) + public function __construct(array $nodesToAdd, array $nodesToAddBefore) { $this->nodesToAdd = $nodesToAdd; + $this->nodesToAddBefore = $nodesToAddBefore; } /** @@ -121,16 +141,30 @@ public function leaveNode(Node $node) { $position = spl_object_hash($node); - if (! isset($this->nodesToAdd[$position])) { - return null; + // add node after + if (isset($this->nodesToAdd[$position])) { + $nodes = array_merge([$node], $this->nodesToAdd[$position]); + unset($this->nodesToAdd[$position]); + return $nodes; } - $nodes = array_merge([$node], $this->nodesToAdd[$position]); + // add node before + if (isset($this->nodesToAddBefore[$position])) { + $nodes = array_merge($this->nodesToAddBefore[$position], [$node]); + + unset($this->nodesToAddBefore[$position]); - unset($this->nodesToAdd[$position]); + return $nodes; + } - return $nodes; + return null; } }; } + + private function reset(): void + { + $this->nodesToAdd = []; + $this->nodesToAddBefore = []; + } } diff --git a/src/PhpParser/Node/NodeFactory.php b/src/PhpParser/Node/NodeFactory.php index 18ae699dc215..a76ab4ab9adb 100644 --- a/src/PhpParser/Node/NodeFactory.php +++ b/src/PhpParser/Node/NodeFactory.php @@ -79,8 +79,12 @@ public function createArray(array $items): Array_ { $arrayItems = []; - foreach ($items as $item) { - $arrayItems[] = $this->createArrayItem($item); + $defaultKey = 0; + foreach ($items as $key => $item) { + $customKey = $key !== $defaultKey ? $key : null; + $arrayItems[] = $this->createArrayItem($item, $customKey); + + ++$defaultKey; } return new Array_($arrayItems); @@ -213,26 +217,38 @@ public function createParentConstructWithParams(array $params): StaticCall /** * @param mixed $item + * @param string|int|null $key */ - private function createArrayItem($item): ArrayItem + private function createArrayItem($item, $key = null): ArrayItem { - if ($item instanceof Variable) { - return new ArrayItem($item); - } + $arrayItem = null; - if ($item instanceof Identifier) { + if ($item instanceof Variable) { + $arrayItem = new ArrayItem($item); + } elseif ($item instanceof Identifier) { $string = new String_($item->toString()); - return new ArrayItem($string); + $arrayItem = new ArrayItem($string); + } elseif (is_scalar($item)) { + $itemValue = BuilderHelpers::normalizeValue($item); + $arrayItem = new ArrayItem($itemValue); + } elseif (is_array($item)) { + $arrayItem = new ArrayItem($this->createArray($item)); } - if (is_scalar($item)) { - return new ArrayItem(BuilderHelpers::normalizeValue($item)); + if ($arrayItem !== null) { + if ($key === null) { + return $arrayItem; + } + + $arrayItem->key = BuilderHelpers::normalizeValue($key); + + return $arrayItem; } throw new NotImplementedException(sprintf( 'Not implemented yet. Go to "%s()" and add check for "%s" node.', __METHOD__, - get_class($item) + is_object($item) ? get_class($item) : $item )); } diff --git a/src/Rector/AbstractRector.php b/src/Rector/AbstractRector.php index e530a720d68d..92b4ce972a62 100644 --- a/src/Rector/AbstractRector.php +++ b/src/Rector/AbstractRector.php @@ -147,6 +147,16 @@ protected function addFileWithContent(string $filePath, string $content): void $this->removedAndAddedFilesCollector->addFileWithContent($filePath, $content); } + protected function getNextExpression(Node $node): ?Node + { + $currentExpression = $node->getAttribute(AttributeKey::CURRENT_EXPRESSION); + if (! $currentExpression instanceof Expression) { + return null; + } + + return $currentExpression->getAttribute(AttributeKey::NEXT_NODE); + } + /** * @param Expr[]|null[] $nodes * @param mixed[] $expectedValues diff --git a/src/Rector/AbstractRector/NodeCommandersTrait.php b/src/Rector/AbstractRector/NodeCommandersTrait.php index d392682100ae..c1766541eba4 100644 --- a/src/Rector/AbstractRector/NodeCommandersTrait.php +++ b/src/Rector/AbstractRector/NodeCommandersTrait.php @@ -61,6 +61,13 @@ protected function addNodeAfterNode(Node $newNode, Node $positionNode): void $this->notifyNodeChangeFileInfo($positionNode); } + protected function addNodeBeforeNode(Node $newNode, Node $positionNode): void + { + $this->nodeAddingCommander->addNodeBeforeNode($newNode, $positionNode); + + $this->notifyNodeChangeFileInfo($positionNode); + } + protected function addPropertyToClass(Class_ $classNode, string $propertyType, string $propertyName): void { $variableInfo = new VariableInfo($propertyName, $propertyType); diff --git a/src/Rector/AbstractRector/NodeFactoryTrait.php b/src/Rector/AbstractRector/NodeFactoryTrait.php index 1dea98cb0429..aad708b4b694 100644 --- a/src/Rector/AbstractRector/NodeFactoryTrait.php +++ b/src/Rector/AbstractRector/NodeFactoryTrait.php @@ -37,10 +37,12 @@ public function autowireNodeFactoryTrait(NodeFactory $nodeFactory): void } /** - * @param Arg[] $args + * @param Expr[]|Arg[] $args */ protected function createStaticCall(string $class, string $method, array $args = []): StaticCall { + $args = $this->wrapToArg($args); + if (in_array($class, ['self', 'parent', 'static'], true)) { $class = new Name($class); } else { @@ -129,4 +131,20 @@ protected function createPropertyFetch($variable, string $property): PropertyFet { return $this->nodeFactory->createPropertyFetch($variable, $property); } + + /** + * @param Expr[]|Arg[] $args + * @return Arg[] + */ + private function wrapToArg(array $args): array + { + foreach ($args as $key => $arg) { + if ($arg instanceof Arg) { + continue; + } + + $args[$key] = new Arg($arg); + } + return $args; + } } diff --git a/tests/PhpParser/Node/NodeFactoryTest.php b/tests/PhpParser/Node/NodeFactoryTest.php new file mode 100644 index 000000000000..156411374c6b --- /dev/null +++ b/tests/PhpParser/Node/NodeFactoryTest.php @@ -0,0 +1,51 @@ +bootKernel(RectorKernel::class); + + $this->nodeFactory = self::$container->get(NodeFactory::class); + } + + /** + * @param mixed[] $inputArray + * @dataProvider provideDataForArray() + */ + public function testCreateArray(array $inputArray, Array_ $expectedArrayNode): void + { + $arrayNode = $this->nodeFactory->createArray($inputArray); + + $this->assertEquals($expectedArrayNode, $arrayNode); + } + + public function provideDataForArray(): Iterator + { + $array = new Array_(); + $array->items[] = new ArrayItem(new LNumber(1)); + + yield [[1], $array]; + + $array = new Array_(); + $array->items[] = new ArrayItem(new LNumber(1), new String_('a')); + + yield [['a' => 1], $array]; + } +}