From 56e244991805a04b9c29fc218a9f965239cde751 Mon Sep 17 00:00:00 2001 From: TomasVotruba Date: Tue, 2 Jun 2020 09:28:51 +0200 Subject: [PATCH 1/2] [PHP 8.0] Add TokenGetAllToObjectRector --- config/set/php/php80.yaml | 1 + ecs.yaml | 3 + .../src/Rector/NodeRemovingRector.php | 37 +- .../src/NodeManipulator/TokenManipulator.php | 351 ++++++++++++++++++ .../FuncCall/TokenGetAllToObjectRector.php | 172 +++++++++ .../Fixture/code_sniffer_case.php.inc | 45 +++ .../Fixture/fixture.php.inc | 44 +++ .../Fixture/not_array_first.php.inc | 45 +++ .../Fixture/skip_non_token_array.php.inc | 49 +++ .../TokenGetAllToObjectRectorTest.php | 30 ++ 10 files changed, 776 insertions(+), 1 deletion(-) create mode 100644 rules/php80/src/NodeManipulator/TokenManipulator.php create mode 100644 rules/php80/src/Rector/FuncCall/TokenGetAllToObjectRector.php create mode 100644 rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/Fixture/code_sniffer_case.php.inc create mode 100644 rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/Fixture/fixture.php.inc create mode 100644 rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/Fixture/not_array_first.php.inc create mode 100644 rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/Fixture/skip_non_token_array.php.inc create mode 100644 rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/TokenGetAllToObjectRectorTest.php diff --git a/config/set/php/php80.yaml b/config/set/php/php80.yaml index 63ed05d43b7d..2da08b7774c6 100644 --- a/config/set/php/php80.yaml +++ b/config/set/php/php80.yaml @@ -7,3 +7,4 @@ services: Rector\Php80\Rector\Class_\AnnotationToAttributeRector: null Rector\Php80\Rector\FuncCall\ClassOnObjectRector: null Rector\Php80\Rector\Ternary\GetDebugTypeRector: null + Rector\Php80\Rector\FuncCall\TokenGetAllToObjectRector: null diff --git a/ecs.yaml b/ecs.yaml index 14181229ea1a..a029a488c964 100644 --- a/ecs.yaml +++ b/ecs.yaml @@ -75,4 +75,7 @@ parameters: # part of the comparison logic - 'packages/polyfill/src/ConditionEvaluator.php' + # often soo many cases that need manual attention → skip and resole in automated way + SlevomatCodingStandard\Sniffs\Namespaces\ReferenceUsedNamesOnlySniff.PartialUse: null + line_ending: "\n" diff --git a/packages/post-rector/src/Rector/NodeRemovingRector.php b/packages/post-rector/src/Rector/NodeRemovingRector.php index b29db2411d57..98e75ce5b002 100644 --- a/packages/post-rector/src/Rector/NodeRemovingRector.php +++ b/packages/post-rector/src/Rector/NodeRemovingRector.php @@ -5,10 +5,12 @@ namespace Rector\PostRector\Rector; use PhpParser\Node; +use PhpParser\Node\Expr\BinaryOp; use PhpParser\Node\Expr\MethodCall; use PhpParser\NodeTraverser; use Rector\Core\PhpParser\Node\NodeFactory; use Rector\Core\RectorDefinition\RectorDefinition; +use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\PostRector\Collector\NodesToRemoveCollector; final class NodeRemovingRector extends AbstractPostRector @@ -64,7 +66,11 @@ public function enterNode(Node $node): ?Node return $this->nodeFactory->createMethodCall($nestedMethodCall->var, $methodName, $node->args); } - return null; + if (! $node instanceof BinaryOp) { + return null; + } + + return $this->removePartOfBinaryOp($node); } /** @@ -73,6 +79,11 @@ public function enterNode(Node $node): ?Node public function leaveNode(Node $node) { foreach ($this->nodesToRemoveCollector->getNodesToRemove() as $key => $nodeToRemove) { + $nodeToRemoveParent = $nodeToRemove->getAttribute(AttributeKey::PARENT_NODE); + if ($nodeToRemoveParent instanceof BinaryOp) { + continue; + } + if ($node === $nodeToRemove) { $this->nodesToRemoveCollector->unset($key); @@ -101,4 +112,28 @@ private function isChainMethodCallNodeToBeRemoved(Node $node, Node $nodeToRemove return $methodName !== null; } + + private function removePartOfBinaryOp(BinaryOp $binaryOp): ?Node + { + // handle left/right binary remove, e.g. "true && false" → remove false → "true" + foreach ($this->nodesToRemoveCollector->getNodesToRemove() as $key => $nodeToRemove) { + // remove node + $nodeToRemoveParentNode = $nodeToRemove->getAttribute(AttributeKey::PARENT_NODE); + if (! $nodeToRemoveParentNode instanceof BinaryOp) { + continue; + } + + if ($binaryOp->left === $nodeToRemove) { + $this->nodesToRemoveCollector->unset($key); + return $binaryOp->right; + } + + if ($binaryOp->right === $nodeToRemove) { + $this->nodesToRemoveCollector->unset($key); + return $binaryOp->left; + } + } + + return null; + } } diff --git a/rules/php80/src/NodeManipulator/TokenManipulator.php b/rules/php80/src/NodeManipulator/TokenManipulator.php new file mode 100644 index 000000000000..94e62c6c35fb --- /dev/null +++ b/rules/php80/src/NodeManipulator/TokenManipulator.php @@ -0,0 +1,351 @@ +callableNodeTraverser = $callableNodeTraverser; + $this->valueResolver = $valueResolver; + $this->betterStandardPrinter = $betterStandardPrinter; + $this->nodeNameResolver = $nodeNameResolver; + $this->nodeTypeResolver = $nodeTypeResolver; + $this->nodesToRemoveCollector = $nodesToRemoveCollector; + } + + /** + * @param Node[] $nodes + */ + public function refactorArrayToken(array $nodes, Expr $singleTokenExpr): void + { + $this->replaceTokenDimFetchZeroWithGetTokenName($nodes, $singleTokenExpr); + + // replace "$token[1]"; with "$token->value" + $this->callableNodeTraverser->traverseNodesWithCallable($nodes, function (Node $node) { + if (! $this->isArrayDimFetchWithDimIntegerValue($node, 1)) { + return null; + } + + /** @var ArrayDimFetch $node */ + $tokenStaticType = $this->nodeTypeResolver->getStaticType($node->var); + if (! $tokenStaticType instanceof ArrayType) { + return null; + } + + return new PropertyFetch($node->var, 'text'); + }); + } + + /** + * @param Node[] $nodes + */ + public function refactorNonArrayToken(array $nodes, Expr $singleTokenExpr): void + { + // replace "$content = $token;" → "$content = $token->text;" + $this->callableNodeTraverser->traverseNodesWithCallable($nodes, function (Node $node) use ($singleTokenExpr) { + if (! $node instanceof Assign) { + return null; + } + + if (! $this->betterStandardPrinter->areNodesEqual($node->expr, $singleTokenExpr)) { + return null; + } + + $tokenStaticType = $this->nodeTypeResolver->getStaticType($node->expr); + if ($tokenStaticType instanceof ArrayType) { + return null; + } + + $node->expr = new PropertyFetch($singleTokenExpr, 'text'); + }); + + // replace "$name = null;" → "$name = $token->getTokenName();" + $this->callableNodeTraverser->traverseNodesWithCallable($nodes, function (Node $node) use ($singleTokenExpr) { + if (! $node instanceof Assign) { + return null; + } + + $tokenStaticType = $this->nodeTypeResolver->getStaticType($node->expr); + if ($tokenStaticType instanceof ArrayType) { + return null; + } + + if ($this->assignedNameExpr === null) { + return null; + } + + if (! $this->betterStandardPrinter->areNodesEqual($node->var, $this->assignedNameExpr)) { + return null; + } + + if (! $this->valueResolver->isValue($node->expr, 'null')) { + return null; + } + + $node->expr = new MethodCall($singleTokenExpr, 'getTokenName'); + + return $node; + }); + } + + public function refactorTokenIsKind(array $nodes, Expr $singleTokenExpr): void + { + $this->callableNodeTraverser->traverseNodesWithCallable($nodes, function (Node $node) use ($singleTokenExpr) { + if (! $node instanceof Identical) { + return null; + } + + $tokenArrayDimFetchAndTConstantType = $this->matchTokenArrayDimFetchAndTConstantType($node); + if ($tokenArrayDimFetchAndTConstantType === null) { + return null; + } + + [$arrayDimFetch, $constFetch] = $tokenArrayDimFetchAndTConstantType; + + /** @var ArrayDimFetch $arrayDimFetch */ + if (! $this->isArrayDimFetchWithDimIntegerValue($arrayDimFetch, 0)) { + return null; + } + + if (! $this->betterStandardPrinter->areNodesEqual($arrayDimFetch->var, $singleTokenExpr)) { + return null; + } + + /** @var ConstFetch $constFetch */ + $constName = $this->nodeNameResolver->getName($constFetch); + if ($constName === null) { + return null; + } + + if (! Strings::match($constName, '#^T_#')) { + return null; + } + + return $this->createIsTConstTypeMethodCall($arrayDimFetch, $constFetch); + }); + } + + /** + * @param Node[] $nodes + */ + public function removeIsArray(array $nodes, Expr\Variable $singleTokenVariable): void + { + $this->callableNodeTraverser->traverseNodesWithCallable($nodes, function (Node $node) use ( + $singleTokenVariable + ) { + if (! $node instanceof FuncCall) { + return null; + } + + if (! $this->nodeNameResolver->isName($node, 'is_array')) { + return null; + } + + if (! $this->betterStandardPrinter->areNodesEqual($node->args[0]->value, $singleTokenVariable)) { + return null; + } + + if ($this->shouldSkipNodeRemovalForPartOfIf($node)) { + return null; + } + + // remove correct node + $nodeToRemove = $this->matchParentNodeInCaseOfIdenticalTrue($node); + + $this->nodesToRemoveCollector->addNodeToRemove($nodeToRemove); + }); + } + + /** + * Replace $token[0] with $token->getTokenName() call + * + * @param Node[] $nodes + */ + private function replaceTokenDimFetchZeroWithGetTokenName(array $nodes, Expr $singleTokenExpr): void + { + $this->callableNodeTraverser->traverseNodesWithCallable($nodes, function (Node $node) use ($singleTokenExpr) { + if (! $node instanceof FuncCall) { + return null; + } + + if (! $this->nodeNameResolver->isName($node, 'token_name')) { + return null; + } + + $possibleTokenArray = $node->args[0]->value; + if (! $possibleTokenArray instanceof ArrayDimFetch) { + return null; + } + + $tokenStaticType = $this->nodeTypeResolver->getStaticType($possibleTokenArray->var); + if (! $tokenStaticType instanceof ArrayType) { + return null; + } + + if ($possibleTokenArray->dim === null) { + return null; + } + + if (! $this->valueResolver->isValue($possibleTokenArray->dim, 0)) { + return null; + } + + /** @var FuncCall $node */ + if (! $this->betterStandardPrinter->areNodesEqual($possibleTokenArray->var, $singleTokenExpr)) { + return null; + } + + // save token variable name for later + $parentNode = $node->getAttribute(AttributeKey::PARENT_NODE); + if ($parentNode instanceof Assign) { + $this->assignedNameExpr = $parentNode->var; + } + + return new MethodCall($singleTokenExpr, 'getTokenName'); + }); + } + + private function isArrayDimFetchWithDimIntegerValue(Node $node, int $value): bool + { + if (! $node instanceof ArrayDimFetch) { + return false; + } + + if ($node->dim === null) { + return false; + } + + return $this->valueResolver->isValue($node->dim, $value); + } + + private function createIsTConstTypeMethodCall(ArrayDimFetch $arrayDimFetch, ConstFetch $constFetch): MethodCall + { + return new MethodCall($arrayDimFetch->var, 'is', [new Arg($constFetch)]); + } + + private function shouldSkipNodeRemovalForPartOfIf(FuncCall $funcCall): bool + { + $parentNode = $funcCall->getAttribute(AttributeKey::PARENT_NODE); + + // cannot remove x from if(x) + if ($parentNode instanceof If_ && $parentNode->cond === $funcCall) { + return true; + } + + if ($parentNode instanceof Expr\BooleanNot) { + $parentParentNode = $parentNode->getAttribute(AttributeKey::PARENT_NODE); + if ($parentParentNode instanceof If_) { + $parentParentNode->cond = $parentNode; + return true; + } + } + + return false; + } + + /** + * @return ArrayDimFetch[]|ConstFetch[]|null + */ + private function matchTokenArrayDimFetchAndTConstantType(Identical $identical): ?array + { + if ($identical->left instanceof ArrayDimFetch && $identical->right instanceof ConstFetch) { + return [$identical->left, $identical->right]; + } + + if ($identical->right instanceof ArrayDimFetch && $identical->left instanceof ConstFetch) { + return [$identical->right, $identical->left]; + } + + return null; + } + + private function matchParentNodeInCaseOfIdenticalTrue(FuncCall $funcCall): Node + { + $parentNode = $funcCall->getAttribute(AttributeKey::PARENT_NODE); + if ($parentNode instanceof Identical) { + if ($parentNode->left === $funcCall && $this->isTrueConstant($parentNode->right)) { + return $parentNode; + } + + if ($parentNode->right === $funcCall && $this->isTrueConstant($parentNode->left)) { + return $parentNode; + } + } + + return $funcCall; + } + + private function isTrueConstant(Expr $expr): bool + { + if (! $expr instanceof ConstFetch) { + return false; + } + + return $expr->name->toLowerString() === 'true'; + } +} diff --git a/rules/php80/src/Rector/FuncCall/TokenGetAllToObjectRector.php b/rules/php80/src/Rector/FuncCall/TokenGetAllToObjectRector.php new file mode 100644 index 000000000000..68b67c73a8ba --- /dev/null +++ b/rules/php80/src/Rector/FuncCall/TokenGetAllToObjectRector.php @@ -0,0 +1,172 @@ +tokenManipulator = $ifArrayTokenManipulator; + } + + public function getDefinition(): RectorDefinition + { + return new RectorDefinition('Complete missing constructor dependency instance by type', [ + new CodeSample( + <<<'PHP' +final class SomeClass +{ + public function run() + { + $tokens = token_get_all($code); + foreach ($tokens as $token) { + if (is_array($token)) { + $name = token_name($token[0]); + $text = $token[1]; + } else { + $name = null; + $text = $token; + } + } + } +} +PHP +, + <<<'PHP' +final class SomeClass +{ + public function run() + { + $tokens = \PhpToken::getAll($code); + foreach ($tokens as $phpToken) { + $name = $phpToken->getTokenName(); + $text = $phpToken->text; + } + } +} +PHP + + ), + ]); + } + + /** + * @return string[] + */ + public function getNodeTypes(): array + { + return [FuncCall::class]; + } + + /** + * @param FuncCall $node + */ + public function refactor(Node $node): ?Node + { + if (! $this->isFuncCallName($node, 'token_get_all')) { + return null; + } + + $this->refactorTokensVariable($node); + + return $this->createStaticCall('PhpToken', 'getAll', $node->args); + } + + /** + * @param ClassMethod|Function_ $functionLike + */ + private function replaceGetNameOrGetValue(FunctionLike $functionLike, Expr $assignedExpr): void + { + $tokensForeaches = $this->findForeachesOverTokenVariable($functionLike, $assignedExpr); + foreach ($tokensForeaches as $tokensForeach) { + $this->refactorTokenInForeach($tokensForeach); + } + } + + private function refactorTokenInForeach(Foreach_ $tokensForeach): void + { + $singleToken = $tokensForeach->valueVar; + if (! $singleToken instanceof Expr\Variable) { + return; + } + + $this->traverseNodesWithCallable($tokensForeach, function (Node $node) use ($singleToken) { + $this->tokenManipulator->refactorArrayToken([$node], $singleToken); + $this->tokenManipulator->refactorNonArrayToken([$node], $singleToken); + $this->tokenManipulator->refactorTokenIsKind([$node], $singleToken); + + $this->tokenManipulator->removeIsArray([$node], $singleToken); + + // drop if "If_" node not needed + if ($node instanceof If_ && $node->else !== null) { + if (! $this->areNodesEqual($node->stmts, $node->else->stmts)) { + return null; + } + + $this->unwrapStmts($node->stmts, $node); + $this->removeNode($node); + } + }); + } + + private function refactorTokensVariable(FuncCall $funcCall): void + { + $assign = $funcCall->getAttribute(AttributeKey::PARENT_NODE); + if (! $assign instanceof Assign) { + return; + } + + $classMethodOrFunctionNode = $funcCall->getAttribute(AttributeKey::METHOD_NODE) ?: + $funcCall->getAttribute(AttributeKey::FUNCTION_NODE); + + if ($classMethodOrFunctionNode === null) { + return; + } + + // dummy approach, improve when needed + $this->replaceGetNameOrGetValue($classMethodOrFunctionNode, $assign->var); + } + + /** + * @return Foreach_[] + */ + private function findForeachesOverTokenVariable($functionLike, Expr $assignedExpr): array + { + return $this->betterNodeFinder->find((array) $functionLike->stmts, function (Node $node) use ( + $assignedExpr + ): bool { + if (! $node instanceof Foreach_) { + return false; + } + + return $this->areNodesEqual($node->expr, $assignedExpr); + }); + } +} diff --git a/rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/Fixture/code_sniffer_case.php.inc b/rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/Fixture/code_sniffer_case.php.inc new file mode 100644 index 000000000000..870f014d0d89 --- /dev/null +++ b/rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/Fixture/code_sniffer_case.php.inc @@ -0,0 +1,45 @@ + +----- +is(T_VARIABLE)) { + $error = 'Variable "%s" not allowed in double quoted string; use concatenation instead'; + $data = [$token->text]; + return $data; + } + } + } +} + +?> diff --git a/rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/Fixture/fixture.php.inc b/rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/Fixture/fixture.php.inc new file mode 100644 index 000000000000..8d5621237e00 --- /dev/null +++ b/rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/Fixture/fixture.php.inc @@ -0,0 +1,44 @@ + +----- +getTokenName(); + $text = $token->text; + } + } +} + +?> diff --git a/rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/Fixture/not_array_first.php.inc b/rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/Fixture/not_array_first.php.inc new file mode 100644 index 000000000000..fd0573823931 --- /dev/null +++ b/rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/Fixture/not_array_first.php.inc @@ -0,0 +1,45 @@ + +----- +getTokenName(); + $text = $token->text; + } + } +} + +?> diff --git a/rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/Fixture/skip_non_token_array.php.inc b/rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/Fixture/skip_non_token_array.php.inc new file mode 100644 index 000000000000..8dff5e6ebed3 --- /dev/null +++ b/rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/Fixture/skip_non_token_array.php.inc @@ -0,0 +1,49 @@ + +----- + diff --git a/rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/TokenGetAllToObjectRectorTest.php b/rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/TokenGetAllToObjectRectorTest.php new file mode 100644 index 000000000000..1f8f74a5b918 --- /dev/null +++ b/rules/php80/tests/Rector/FuncCall/TokenGetAllToObjectRector/TokenGetAllToObjectRectorTest.php @@ -0,0 +1,30 @@ +doTestFile($file); + } + + public function provideData(): Iterator + { + return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + protected function getRectorClass(): string + { + return TokenGetAllToObjectRector::class; + } +} From b0daf5e50e7a4836dd637c92607e0bd822dcabbc Mon Sep 17 00:00:00 2001 From: TomasVotruba Date: Tue, 2 Jun 2020 18:20:07 +0200 Subject: [PATCH 2/2] fix directory name --- .../ReplaceHttpServerVarsByServerRectorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules/php53/tests/Rector/Variable/ReplaceHttpServerVarsByServerRector/ReplaceHttpServerVarsByServerRectorTest.php b/rules/php53/tests/Rector/Variable/ReplaceHttpServerVarsByServerRector/ReplaceHttpServerVarsByServerRectorTest.php index d0b9fb9a7773..5cf9611cc807 100644 --- a/rules/php53/tests/Rector/Variable/ReplaceHttpServerVarsByServerRector/ReplaceHttpServerVarsByServerRectorTest.php +++ b/rules/php53/tests/Rector/Variable/ReplaceHttpServerVarsByServerRector/ReplaceHttpServerVarsByServerRectorTest.php @@ -24,7 +24,7 @@ public function test(string $file): void */ public function provideDataForTest(): Iterator { - return $this->yieldFilesFromDirectory(__DIR__ . '/Fixtures'); + return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); } protected function getRectorClass(): string