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 ;
2018-12-03 22:42:57 +01:00
use PhpParser\Node\Stmt\If_ ;
2018-08-14 16:03:13 +02:00
use PhpParser\Node\Stmt\Nop ;
use PhpParser\Node\Stmt\Return_ ;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode ;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode ;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode ;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockAnalyzer ;
2018-12-03 22:42:57 +01:00
use Rector\PhpParser\NodeTraverser\CallableNodeTraverser ;
2018-08-14 16:03:13 +02:00
use Rector\Rector\AbstractRector ;
use Rector\RectorDefinition\CodeSample ;
use Rector\RectorDefinition\RectorDefinition ;
final class MergeIsCandidateRector extends AbstractRector
{
/**
* @ var BuilderFactory
*/
private $builderFactory ;
/**
* @ var DocBlockAnalyzer
*/
private $docBlockAnalyzer ;
/**
* @ var CallableNodeTraverser
*/
2018-12-03 22:42:57 +01:00
private $callableNodeTraverser ;
2018-08-14 16:03:13 +02:00
public function __construct (
BuilderFactory $builderFactory ,
DocBlockAnalyzer $docBlockAnalyzer ,
2018-12-03 22:42:57 +01:00
CallableNodeTraverser $callableNodeTraverser
2018-08-14 16:03:13 +02:00
) {
$this -> builderFactory = $builderFactory ;
$this -> docBlockAnalyzer = $docBlockAnalyzer ;
2018-12-03 22:42:57 +01:00
$this -> callableNodeTraverser = $callableNodeTraverser ;
2018-08-14 16:03:13 +02:00
}
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
}
/**
2018-10-15 12:36:58 +08:00
* @ param Class_ $node
2018-08-14 16:03:13 +02:00
*/
2018-10-15 12:36:58 +08:00
public function refactor ( Node $node ) : ? Node
2018-08-14 16:03:13 +02:00
{
2018-12-03 22:42:57 +01:00
if ( ! $this -> isType ( $node , AbstractRector :: class )) {
2018-10-21 12:19:14 +02:00
return null ;
2018-10-14 18:48:21 +08:00
}
2018-10-15 12:36:58 +08:00
if ( ! $node -> isAbstract ()) {
2018-10-21 12:19:14 +02:00
return null ;
2018-08-14 16:03:13 +02:00
}
// has "isCandidate()" method?
2018-10-15 12:36:58 +08:00
if ( ! $this -> hasClassIsCandidateMethod ( $node )) {
2018-10-21 12:19:14 +02:00
return null ;
2018-08-14 16:03:13 +02:00
}
2018-10-15 12:36:58 +08:00
[ $isCandidateClassMethodPosition , $isCandidateClassMethod ] = $this -> getClassMethodByName ( $node , 'isCandidate' );
[ $refactorClassMethodPosition , $refactorClassMethod ] = $this -> getClassMethodByName ( $node , 'refactor' );
2018-08-14 16:03:13 +02:00
if ( $refactorClassMethod === null ) {
2018-10-21 12:19:14 +02:00
return null ;
2018-08-14 16:03:13 +02:00
}
// 1. replace "isCandidate()" method by "getNodeType()" method
2018-10-15 12:36:58 +08:00
$node -> stmts [ $isCandidateClassMethodPosition ] = $this -> createGetNodeTypeClassMethod ( $refactorClassMethod );
2018-08-14 16:03:13 +02:00
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 );
2018-10-15 12:36:58 +08:00
$node -> stmts [ $refactorClassMethodPosition ] = $refactorClassMethod ;
2018-08-14 16:03:13 +02:00
2018-10-15 12:36:58 +08:00
return $node ;
2018-08-14 16:03:13 +02:00
}
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 ;
}
2018-10-31 22:15:41 +01:00
if ( $this -> isName ( $stmt -> name , $name )) {
2018-08-14 16:03:13 +02:00
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 ;
}
2018-12-03 22:42:57 +01:00
// todo: resolve
2018-08-14 16:03:13 +02:00
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-12-03 22:42:57 +01:00
$nodeToBeReturned -> items [] = $this -> createClassConstFetchFromClassName ( Node :: class );
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
{
2018-12-03 22:42:57 +01:00
$this -> callableNodeTraverser -> traverseNodesWithCallable ([ $classMethod ], function ( Node $node ) : ? Node {
2018-10-21 14:10:19 +02:00
if ( ! $node instanceof Return_ || ! $node -> expr instanceof ConstFetch ) {
2018-08-14 16:03:13 +02:00
return null ;
}
2018-10-21 14:10:19 +02:00
if ( $this -> isFalse ( $node -> expr )) {
2018-08-14 16:03:13 +02:00
return new Return_ ( new ConstFetch ( new Name ( 'null' )));
}
return null ;
});
}
private function renameNodeToParamNode ( ClassMethod $classMethod , string $nodeName ) : void
{
2018-12-03 22:42:57 +01:00
$this -> callableNodeTraverser -> traverseNodesWithCallable ([ $classMethod ], function ( Node $node ) use (
$nodeName
) : ? Node {
2018-10-31 22:15:41 +01:00
if ( ! $node instanceof Variable || ! $this -> isName ( $node , 'node' )) {
2018-08-14 16:03:13 +02:00
return null ;
}
$node -> name = $nodeName ;
return $node ;
});
}
private function replaceLastReturnWithIf ( ClassMethod $classMethod ) : void
{
2018-12-03 22:42:57 +01:00
$this -> callableNodeTraverser -> traverseNodesWithCallable ([ $classMethod ], function ( Node $node ) : ? Node {
2018-08-14 16:03:13 +02:00
if ( ! $node instanceof Return_ ) {
return null ;
}
if ( $node -> expr instanceof ConstFetch ) {
return null ;
}
2018-12-03 22:42:57 +01:00
$identicalCondition = new Identical ( $node -> expr , new ConstFetch ( new Name ( 'false' )));
2018-08-14 16:03:13 +02:00
return new If_ ( $identicalCondition , [
2018-12-03 22:42:57 +01:00
'stmts' => [ new Return_ ( new ConstFetch ( new Name ( 'null' )))],
2018-08-14 16:03:13 +02:00
]);
});
}
private function removeReturnTrue ( ClassMethod $classMethod ) : void
{
2018-12-03 22:42:57 +01:00
$this -> callableNodeTraverser -> traverseNodesWithCallable ([ $classMethod ], function ( Node $node ) : ? Node {
2018-10-31 22:15:41 +01:00
if ( ! $node instanceof Return_ || ! $node -> expr instanceof ConstFetch || ! $this -> isTrue ( $node -> expr )) {
2018-08-14 16:03:13 +02:00
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 ;
}
}