2018-08-14 16:03:13 +02:00
< ? php declare ( strict_types = 1 );
2018-08-15 11:10:45 +02:00
/**
* 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 );
2018-08-15 11:10:45 +02:00
// 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 );
2018-08-15 11:10:45 +02:00
// 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
2018-09-28 00:11:39 +08:00
if ( ! $this -> docBlockAnalyzer -> hasTag ( $classMethod , 'param' )) {
2018-08-14 16:03:13 +02:00
return [];
}
2018-09-28 00:11:39 +08:00
$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 ;
}
}