rector/examples/MergeIsCandidateRector.php

338 lines
11 KiB
PHP
Raw Normal View History

2018-08-14 16:03:13 +02:00
<?php declare(strict_types=1);
/**
* This class was used to automate huge refactoring in https://github.com/rectorphp/rector/pull/584
*
* It helped with 7-step refactoring of 96 files:
* - 1. replace "isCandidate()" method by "getNodeType()" method
* - 2. rename "return false;" to "return null;" to respect "refactor(Node $node): ?Node" typehint
* - 3. rename used variable "$node" to "$specificTypeParam"
* - 4. turn last return in "isCondition()" with early return
* - 5. return true makes no sense anymore, just continue
* - 6. remove first "instanceof", already covered by getNodeType()
* - 7. add contents of "isCandidate()" method to start of "refactor()" method
*
* It took ~2 hours to setup. Saved work of 5-6 hours and much more stress :)
*/
namespace Rector\Examples;
2018-08-14 16:03:13 +02:00
use PhpParser\BuilderFactory;
use PhpParser\Node;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Expr\BinaryOp\Identical;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Nop;
use PhpParser\Node\Stmt\Return_;
use PhpParser\Node\Stmt\If_;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use Rector\Builder\StatementGlue;
use Rector\NodeTypeResolver\NodeTypeResolver;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockAnalyzer;
use Rector\NodeTypeResolver\PHPStan\Type\TypeToStringResolver;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
use Rector\Utils\NodeTraverser\CallableNodeTraverser;
final class MergeIsCandidateRector extends AbstractRector
{
/**
* @var NodeTypeResolver
*/
private $nodeTypeResolver;
/**
* @var BuilderFactory
*/
private $builderFactory;
/**
* @var DocBlockAnalyzer
*/
private $docBlockAnalyzer;
/**
* @var TypeToStringResolver
*/
private $typeToStringResolver;
/**
* @var StatementGlue
*/
private $statementGlue;
/**
* @var CallableNodeTraverser
*/
private $callbackNodeTraverser;
public function __construct(
NodeTypeResolver $nodeTypeResolver,
BuilderFactory $builderFactory,
DocBlockAnalyzer $docBlockAnalyzer,
TypeToStringResolver $typeToStringResolver,
CallableNodeTraverser $callbackNodeTraverser
) {
$this->nodeTypeResolver = $nodeTypeResolver;
$this->builderFactory = $builderFactory;
$this->docBlockAnalyzer = $docBlockAnalyzer;
$this->typeToStringResolver = $typeToStringResolver;
$this->callbackNodeTraverser = $callbackNodeTraverser;
}
public function getDefinition(): RectorDefinition
{
return new RectorDefinition(
'Finds all "Rector\Rector\AbstractRector" instances, merges "isCandidate()" method to "refactor()" method and creates "getNodeType()" method by @param annotation of "refactor()" method.',
[new CodeSample('', '')]
);
}
2018-08-15 00:12:41 +02:00
/**
* @return string[]
*/
public function getNodeTypes(): array
2018-08-14 16:03:13 +02:00
{
2018-08-15 00:12:41 +02:00
return [Class_::class];
2018-08-14 16:03:13 +02:00
}
/**
* @param Class_ $classNode
*/
public function refactor(Node $classNode): ?Node
{
$nodeTypes = $this->nodeTypeResolver->resolve($classNode);
if (! in_array('Rector\Rector\AbstractRector', $nodeTypes, true) || $classNode->isAbstract()) {
return $classNode;
}
// has "isCandidate()" method?
if (! $this->hasClassIsCandidateMethod($classNode)) {
return $classNode;
}
[$isCandidateClassMethodPosition, $isCandidateClassMethod] = $this->getClassMethodByName($classNode, 'isCandidate');
[$refactorClassMethodPosition, $refactorClassMethod] = $this->getClassMethodByName($classNode, 'refactor');
if ($refactorClassMethod === null) {
return $classNode;
}
// 1. replace "isCandidate()" method by "getNodeType()" method
$classNode->stmts[$isCandidateClassMethodPosition] = $this->createGetNodeTypeClassMethod($refactorClassMethod);
// 2. rename "return false;" to "return null;" to respect "refactor(Node $node): ?Node" typehint
2018-08-14 16:03:13 +02:00
$this->replaceReturnFalseWithReturnNull($isCandidateClassMethod);
// 3. rename used variable "$node" to "$specificTypeParam"
$this->renameNodeToParamNode($isCandidateClassMethod, $refactorClassMethod->params[0]->var->name);
// 4. turn last return in "isCondition()" with early return
$this->replaceLastReturnWithIf($isCandidateClassMethod);
// 5. return true makes no sense anymore, just continue
$this->removeReturnTrue($isCandidateClassMethod);
// 6. remove first "instanceof", already covered by getNodeType()
$isCandidateClassMethod = $this->removeFirstInstanceOf($isCandidateClassMethod);
// 7. add contents of "isCandidate()" method to start of "refactor()" method
2018-08-14 16:03:13 +02:00
$refactorClassMethod->stmts = array_merge($isCandidateClassMethod->stmts, $refactorClassMethod->stmts);
$classNode->stmts[$refactorClassMethodPosition] = $refactorClassMethod;
return $classNode;
}
private function hasClassIsCandidateMethod(Class_ $classNode): bool
{
return (bool) $this->getClassMethodByName($classNode, 'isCandidate');
}
/**
* @return int[]|ClassMethod[]|null
*/
private function getClassMethodByName(Class_ $classNode, string $name)
{
foreach ($classNode->stmts as $i => $stmt) {
if (! $stmt instanceof ClassMethod) {
continue;
}
if ((string) $stmt->name === $name) {
return [$i, $stmt];
}
}
return null;
}
/**
* @return string[]
*/
private function resolveParamTagValueNodeToStrings(ParamTagValueNode $paramTagValueNode): array
{
$types = [];
if ($paramTagValueNode->type instanceof UnionTypeNode) {
foreach ($paramTagValueNode->type->types as $type) {
$types[] = (string) $type;
}
} elseif ($paramTagValueNode->type instanceof IdentifierTypeNode) {
$types[] = $paramTagValueNode->type->name;
} else {
dump($paramTagValueNode->type);
// todo: resolve
}
return $types;
}
private function createGetNodeTypeClassMethod(ClassMethod $refactorClassMethod): ClassMethod
{
$paramTypes = $this->resolveSingleParamTypesFromClassMethod($refactorClassMethod);
2018-08-15 00:12:41 +02:00
$nodeToBeReturned = new Array_();
2018-08-14 16:03:13 +02:00
if (count($paramTypes) > 1) {
foreach ($paramTypes as $paramType) {
$classConstFetchNode = $this->createClassConstFetchFromClassName($paramType);
$nodeToBeReturned->items[] = new ArrayItem($classConstFetchNode);
}
} elseif (count($paramTypes) === 1) {
2018-08-15 00:12:41 +02:00
$nodeToBeReturned->items[] = $this->createClassConstFetchFromClassName($paramTypes[0]);
2018-08-14 16:03:13 +02:00
} else { // fallback to basic node
2018-08-15 00:12:41 +02:00
$nodeToBeReturned->items[] = $this->createClassConstFetchFromClassName('PhpParser\\Node');
2018-08-14 16:03:13 +02:00
}
2018-08-15 00:12:41 +02:00
return $this->builderFactory->method('getNodeTypes')
2018-08-14 16:03:13 +02:00
->makePublic()
2018-08-15 00:12:41 +02:00
->setReturnType('array')
2018-08-14 16:03:13 +02:00
->addStmt(new Return_($nodeToBeReturned))
->getNode();
}
/**
* @return string[]
*/
private function resolveSingleParamTypesFromClassMethod(ClassMethod $classMethod): array
{
// add getNodeType() by $refactorClassMethod "@param" doc type
if (! $this->docBlockAnalyzer->hasTag($classMethod, 'param')) {
2018-08-14 16:03:13 +02:00
return [];
}
$paramNode = $this->docBlockAnalyzer->getTagByName($classMethod, 'param');
2018-08-14 16:03:13 +02:00
/** @var ParamTagValueNode $paramTagValueNode */
$paramTagValueNode = $paramNode->value;
return $this->resolveParamTagValueNodeToStrings($paramTagValueNode);
}
private function createClassConstFetchFromClassName(string $className): ClassConstFetch
{
return new ClassConstFetch(new FullyQualified($className), 'class');
}
private function replaceReturnFalseWithReturnNull(ClassMethod $classMethod): void
{
$this->callbackNodeTraverser->traverseNodesWithCallable([$classMethod], function (Node $node): ?Node {
if (!$node instanceof Return_ || !$node->expr instanceof ConstFetch) {
return null;
}
if ((string) $node->expr->name === 'false') {
return new Return_(new ConstFetch(new Name('null')));
}
return null;
});
}
private function renameNodeToParamNode(ClassMethod $classMethod, string $nodeName): void
{
$this->callbackNodeTraverser->traverseNodesWithCallable([$classMethod], function (Node $node) use ($nodeName): ?Node {
if (! $node instanceof Variable || $node->name !== 'node') {
return null;
}
$node->name = $nodeName;
return $node;
});
}
private function replaceLastReturnWithIf(ClassMethod $classMethod): void
{
$this->callbackNodeTraverser->traverseNodesWithCallable([$classMethod], function (Node $node): ?Node {
if (! $node instanceof Return_) {
return null;
}
if ($node->expr instanceof ConstFetch) {
return null;
}
$identicalCondition = new Identical($node->expr, new ConstFetch(new Name('false')));
return new If_($identicalCondition, [
'stmts' => [
new Return_(new ConstFetch(new Name('null')))
]
]);
});
}
private function removeReturnTrue(ClassMethod $classMethod): void
{
$this->callbackNodeTraverser->traverseNodesWithCallable([$classMethod], function (Node $node): ?Node {
if (! $node instanceof Return_ || ! $node->expr instanceof ConstFetch || (string) $node->expr->name !== 'true') {
return null;
}
return new Nop();
});
}
private function removeFirstInstanceOf(ClassMethod $classMethod): ClassMethod
{
if (! isset($classMethod->stmts[0])) {
return $classMethod;
}
if (! $classMethod->stmts[0] instanceof If_) {
return $classMethod;
}
/** @var If_ $ifNode */
$ifNode = $classMethod->stmts[0];
if (! $ifNode->stmts[0] instanceof Return_) {
return $classMethod;
}
/** @var Return_ $returnNode */
$returnNode = $ifNode->stmts[0];
if (! $returnNode->expr instanceof ConstFetch) {
return $classMethod;
}
$constFetchNode = $returnNode->expr;
if ($constFetchNode->name->toString() === null) {
unset($classMethod->stmts[0]);
}
return $classMethod;
}
}