[MagicDisclosure] Fluent refactoring (#3861)

Co-authored-by: rector-bot <tomas@getrector.org>
This commit is contained in:
Tomas Votruba 2020-08-01 16:57:37 +02:00 committed by GitHub
parent 55acb3578a
commit 4624778cb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 329 additions and 154 deletions

View File

@ -98,6 +98,16 @@ trait NodeCommandersTrait
}
}
/**
* @param Node[] $newNodes
*/
protected function addNodesBeforeNode(array $newNodes, Node $positionNode): void
{
foreach ($newNodes as $newNode) {
$this->addNodeBeforeNode($newNode, $positionNode);
}
}
protected function addNodeAfterNode(Node $newNode, Node $positionNode): void
{
$this->nodesToAddCollector->addNodeAfterNode($newNode, $positionNode);

View File

@ -163,7 +163,7 @@ final class ChainMethodCallNodeAnalyzer
$chainMethodCalls = $this->collectAllMethodCallsInChain($methodCall);
foreach ($chainMethodCalls as $key => $chainMethodCall) {
if (! $chainMethodCall->var instanceof MethodCall) {
if (! $chainMethodCall->var instanceof MethodCall && ! $chainMethodCall->var instanceof New_) {
unset($chainMethodCalls[$key]);
break;
}

View File

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Rector\MagicDisclosure\NodeAnalyzer;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\New_;
use PHPStan\Type\MixedType;
use Rector\NodeTypeResolver\NodeTypeResolver;
final class NewChainMethodCallNodeAnalyzer
{
/**
* @var NodeTypeResolver
*/
private $nodeTypeResolver;
public function __construct(NodeTypeResolver $nodeTypeResolver)
{
$this->nodeTypeResolver = $nodeTypeResolver;
}
public function isNewMethodCallReturningSelf(MethodCall $methodCall): bool
{
$newStaticType = $this->nodeTypeResolver->getStaticType($methodCall->var);
$methodCallStaticType = $this->nodeTypeResolver->getStaticType($methodCall);
return $methodCallStaticType->equals($newStaticType);
}
/**
* Method call with "new X", that returns "X"?
* e.g.
*
* $this->setItem(new Item) // → returns "Item"
*/
public function matchNewInFluentSetterMethodCall(MethodCall $methodCall): ?New_
{
if (count($methodCall->args) !== 1) {
return null;
}
$onlyArgValue = $methodCall->args[0]->value;
if (! $onlyArgValue instanceof New_) {
return null;
}
$newType = $this->nodeTypeResolver->resolve($onlyArgValue);
if ($newType instanceof MixedType) {
return null;
}
$parentMethodCallReturnType = $this->nodeTypeResolver->resolve($methodCall);
if (! $newType->equals($parentMethodCallReturnType)) {
return null;
}
return $onlyArgValue;
}
}

View File

@ -8,10 +8,57 @@ use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Stmt\Expression;
use Rector\MagicDisclosure\NodeAnalyzer\ChainMethodCallNodeAnalyzer;
use Rector\MagicDisclosure\ValueObject\AssignAndRootExpr;
use Rector\NetteKdyby\Naming\VariableNaming;
final class NonFluentMethodCallFactory
{
/**
* @var ChainMethodCallNodeAnalyzer
*/
private $chainMethodCallNodeAnalyzer;
/**
* @var VariableNaming
*/
private $variableNaming;
public function __construct(
ChainMethodCallNodeAnalyzer $chainMethodCallNodeAnalyzer,
VariableNaming $variableNaming
) {
$this->chainMethodCallNodeAnalyzer = $chainMethodCallNodeAnalyzer;
$this->variableNaming = $variableNaming;
}
/**
* @return Expression[]
*/
public function createFromNewAndRootMethodCall(New_ $new, MethodCall $rootMethodCall): array
{
$variableName = $this->variableNaming->resolveFromNode($new);
$newVariable = new Variable($variableName);
$newStmts = [];
$newStmts[] = $this->createAssignExpression($newVariable, $new);
// resolve chain calls
$chainMethodCalls = $this->chainMethodCallNodeAnalyzer->collectAllMethodCallsInChainWithoutRootOne(
$rootMethodCall
);
$chainMethodCalls = array_reverse($chainMethodCalls);
foreach ($chainMethodCalls as $chainMethodCall) {
$methodCall = new MethodCall($newVariable, $chainMethodCall->name, $chainMethodCall->args);
$newStmts[] = new Expression($methodCall);
}
return $newStmts;
}
/**
* @param MethodCall[] $chainMethodCalls
*/
@ -66,4 +113,10 @@ final class NonFluentMethodCallFactory
return $assignAndRootExpr->getRootExpr() !== $assignAndRootExpr->getAssignExpr();
}
private function createAssignExpression(Variable $newVariable, New_ $new): Expression
{
$assign = new Assign($newVariable, $new);
return new Expression($assign);
}
}

View File

@ -81,6 +81,7 @@ PHP
*/
public function refactor(Node $node): ?Node
{
// @todo decouple "Return_" completelly to \Rector\MagicDisclosure\Rector\Return_\DefluentReturnMethodCallRector
$methodCall = $this->matchMethodCall($node);
if ($methodCall === null) {
return null;
@ -104,6 +105,7 @@ PHP
return null;
}
// DUPLICATED
$chainMethodCalls = $this->chainMethodCallNodeAnalyzer->collectAllMethodCallsInChain($methodCall);
$assignAndRootExpr = $this->chainMethodCallRootExtractor->extractFromMethodCalls($chainMethodCalls);
@ -177,6 +179,7 @@ PHP
}
/**
* @duplicated
* @param MethodCall|Return_ $node
*/
private function removeCurrentNode(Node $node): void

View File

@ -7,17 +7,19 @@ namespace Rector\MagicDisclosure\Rector\MethodCall;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Stmt\Return_;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\Variable;
use Rector\Core\Contract\Rector\ConfigurableRectorInterface;
use Rector\Core\RectorDefinition\CodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;
use Rector\MagicDisclosure\NodeAnalyzer\ChainMethodCallNodeAnalyzer;
use Rector\MagicDisclosure\NodeAnalyzer\NewChainMethodCallNodeAnalyzer;
use Rector\MagicDisclosure\NodeFactory\NonFluentMethodCallFactory;
use Rector\MagicDisclosure\NodeManipulator\ChainMethodCallRootExtractor;
use Rector\MagicDisclosure\Rector\AbstractRector\AbstractConfigurableMatchTypeRector;
use Rector\MagicDisclosure\ValueObject\AssignAndRootExpr;
use Rector\NetteKdyby\Naming\VariableNaming;
use Rector\NodeTypeResolver\Node\AttributeKey;
/**
@ -32,24 +34,38 @@ final class InArgChainMethodCallToStandaloneMethodCallRector extends AbstractCon
*/
private $chainMethodCallNodeAnalyzer;
/**
* @var ChainMethodCallRootExtractor
*/
private $chainMethodCallRootExtractor;
/**
* @var NonFluentMethodCallFactory
*/
private $nonFluentMethodCallFactory;
/**
* @var VariableNaming
*/
private $variableNaming;
/**
* @var NewChainMethodCallNodeAnalyzer
*/
private $newChainMethodCallNodeAnalyzer;
/**
* @var ChainMethodCallRootExtractor
*/
private $chainMethodCallRootExtractor;
public function __construct(
ChainMethodCallNodeAnalyzer $chainMethodCallNodeAnalyzer,
ChainMethodCallRootExtractor $chainMethodCallRootExtractor,
NonFluentMethodCallFactory $nonFluentMethodCallFactory
NonFluentMethodCallFactory $nonFluentMethodCallFactory,
VariableNaming $variableNaming,
NewChainMethodCallNodeAnalyzer $newChainMethodCallNodeAnalyzer,
ChainMethodCallRootExtractor $chainMethodCallRootExtractor
) {
$this->chainMethodCallNodeAnalyzer = $chainMethodCallNodeAnalyzer;
$this->chainMethodCallRootExtractor = $chainMethodCallRootExtractor;
$this->nonFluentMethodCallFactory = $nonFluentMethodCallFactory;
$this->variableNaming = $variableNaming;
$this->newChainMethodCallNodeAnalyzer = $newChainMethodCallNodeAnalyzer;
$this->chainMethodCallRootExtractor = $chainMethodCallRootExtractor;
}
public function getDefinition(): RectorDefinition
@ -99,24 +115,30 @@ PHP
*/
public function refactor(Node $node): ?Node
{
$methodCall = $this->matchMethodCall($node);
if ($methodCall === null) {
return null;
}
if (! $this->hasParentType($node, Arg::class)) {
return null;
}
if (! $this->chainMethodCallNodeAnalyzer->isLastChainMethodCall($methodCall)) {
/** @var Arg $arg */
$arg = $node->getAttribute(AttributeKey::PARENT_NODE);
/** @var Node|null $parentMethodCall */
$parentMethodCall = $arg->getAttribute(AttributeKey::PARENT_NODE);
if (! $parentMethodCall instanceof MethodCall) {
return null;
}
if ($this->isGetterMethodCall($methodCall)) {
if (! $this->chainMethodCallNodeAnalyzer->isLastChainMethodCall($node)) {
return null;
}
$chainMethodCalls = $this->chainMethodCallNodeAnalyzer->collectAllMethodCallsInChain($methodCall);
// create instances from (new ...)->call, re-use from
if ($node->var instanceof New_) {
$this->refactorNew($node, $node->var);
return null;
}
// DUPLCIATED
$chainMethodCalls = $this->chainMethodCallNodeAnalyzer->collectAllMethodCallsInChain($node);
$assignAndRootExpr = $this->chainMethodCallRootExtractor->extractFromMethodCalls($chainMethodCalls);
if ($assignAndRootExpr === null) {
@ -132,39 +154,58 @@ PHP
$chainMethodCalls
);
$nodesToAdd = $this->addFluentAsArg($node, $assignAndRootExpr, $nodesToAdd);
$this->addNodesBeforeNode($nodesToAdd, $node);
$this->removeCurrentNode($node);
foreach ($nodesToAdd as $nodeToAdd) {
// needed to remove weird spacing
$nodeToAdd->setAttribute(AttributeKey::ORIGINAL_NODE, null);
$this->addNodeAfterNode($nodeToAdd, $node);
}
return $node;
return $assignAndRootExpr->getCallerExpr();
}
/**
* @param MethodCall|Return_ $node
*/
private function matchMethodCall(Node $node): ?MethodCall
private function removeParentParent(MethodCall $methodCall): void
{
if ($node instanceof Return_) {
if ($node->expr === null) {
return null;
}
/** @var Arg $parent */
$parent = $methodCall->getAttribute(AttributeKey::PARENT_NODE);
if ($node->expr instanceof MethodCall) {
return $node->expr;
}
return null;
/** @var MethodCall $parentParent */
$parentParent = $parent->getAttribute(AttributeKey::PARENT_NODE);
$this->removeNode($parentParent);
}
private function createFluentAsArg(MethodCall $methodCall, Variable $variable): MethodCall
{
/** @var Arg $parent */
$parent = $methodCall->getAttribute(AttributeKey::PARENT_NODE);
/** @var MethodCall $parentParent */
$parentParent = $parent->getAttribute(AttributeKey::PARENT_NODE);
$lastMethodCall = new MethodCall($parentParent->var, $parentParent->name);
$lastMethodCall->args[] = new Arg($variable);
return $lastMethodCall;
}
private function crateVariableFromNew(New_ $new): Variable
{
$variableName = $this->variableNaming->resolveFromNode($new);
return new Variable($variableName);
}
private function refactorNew(MethodCall $methodCall, New_ $new): void
{
if (! $this->newChainMethodCallNodeAnalyzer->isNewMethodCallReturningSelf($methodCall)) {
return;
}
return $node;
$nodesToAdd = $this->nonFluentMethodCallFactory->createFromNewAndRootMethodCall($new, $methodCall);
$newVariable = $this->crateVariableFromNew($new);
$nodesToAdd[] = $this->createFluentAsArg($methodCall, $newVariable);
$this->addNodesBeforeNode($nodesToAdd, $methodCall);
$this->removeParentParent($methodCall);
}
/**
* @duplicated
* @param MethodCall[] $chainMethodCalls
*/
private function shouldSkip(AssignAndRootExpr $assignAndRootExpr, array $chainMethodCalls): bool
@ -187,63 +228,4 @@ PHP
return ! $this->isMatchedType($calleeUniqueType);
}
/**
* @param MethodCall|Return_ $node
*/
private function removeCurrentNode(Node $node): void
{
$parentNode = $node->getAttribute(AttributeKey::PARENT_NODE);
if ($parentNode instanceof Assign) {
$this->removeNode($parentNode);
return;
}
// part of method call
if ($parentNode instanceof Arg) {
$parentParent = $parentNode->getAttribute(AttributeKey::PARENT_NODE);
if ($parentParent instanceof MethodCall) {
$this->removeNode($parentParent);
}
return;
}
$this->removeNode($node);
}
/**
* @param Return_|MethodCall $node
* @param Node[] $nodesToAdd
* @return Node[]
*/
private function addFluentAsArg(Node $node, AssignAndRootExpr $assignAndRootExpr, array $nodesToAdd): array
{
$parent = $node->getAttribute(AttributeKey::PARENT_NODE);
if (! $parent instanceof Arg) {
return $nodesToAdd;
}
$parentParent = $parent->getAttribute(AttributeKey::PARENT_NODE);
if (! $parentParent instanceof MethodCall) {
return $nodesToAdd;
}
$lastMethodCall = new MethodCall($parentParent->var, $parentParent->name);
$lastMethodCall->args[] = new Arg($assignAndRootExpr->getRootExpr());
$nodesToAdd[] = $lastMethodCall;
return $nodesToAdd;
}
private function isGetterMethodCall(MethodCall $methodCall): bool
{
if ($methodCall->var instanceof MethodCall) {
return false;
}
$methodCallStaticType = $this->getStaticType($methodCall);
$methodCallVarStaticType = $this->getStaticType($methodCall->var);
// getter short call type
return ! $methodCallStaticType->equals($methodCallVarStaticType);
}
}

View File

@ -6,18 +6,16 @@ namespace Rector\MagicDisclosure\Rector\MethodCall;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Stmt\Expression;
use PHPStan\Type\MixedType;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\RectorDefinition\CodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;
use Rector\MagicDisclosure\NodeAnalyzer\ChainMethodCallNodeAnalyzer;
use Rector\MagicDisclosure\NodeAnalyzer\NewChainMethodCallNodeAnalyzer;
use Rector\MagicDisclosure\NodeFactory\NonFluentMethodCallFactory;
use Rector\NetteKdyby\Naming\VariableNaming;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
/**
* @sponsor Thanks https://amateri.com for sponsoring this rule - visit them on https://www.startupjobs.cz/startup/scrumworks-s-r-o
@ -36,12 +34,26 @@ final class MethodCallOnSetterMethodCallToStandaloneAssignRector extends Abstrac
*/
private $chainMethodCallNodeAnalyzer;
/**
* @var NonFluentMethodCallFactory
*/
private $nonFluentMethodCallFactory;
/**
* @var NewChainMethodCallNodeAnalyzer
*/
private $newChainMethodCallNodeAnalyzer;
public function __construct(
VariableNaming $variableNaming,
ChainMethodCallNodeAnalyzer $chainMethodCallNodeAnalyzer
ChainMethodCallNodeAnalyzer $chainMethodCallNodeAnalyzer,
NonFluentMethodCallFactory $nonFluentMethodCallFactory,
NewChainMethodCallNodeAnalyzer $newChainMethodCallNodeAnalyzer
) {
$this->variableNaming = $variableNaming;
$this->chainMethodCallNodeAnalyzer = $chainMethodCallNodeAnalyzer;
$this->nonFluentMethodCallFactory = $nonFluentMethodCallFactory;
$this->newChainMethodCallNodeAnalyzer = $newChainMethodCallNodeAnalyzer;
}
public function getDefinition(): RectorDefinition
@ -104,67 +116,24 @@ PHP
return null;
}
$new = $this->matchNewInFluentSetterMethodCall($rootMethodCall);
$new = $this->newChainMethodCallNodeAnalyzer->matchNewInFluentSetterMethodCall($rootMethodCall);
if ($new === null) {
return null;
}
$variableName = $this->variableNaming->resolveFromNode($new);
$newVariable = new Variable($variableName);
$assignExpression = $this->createAssignExpression($newVariable, $new);
$this->addNodeBeforeNode($assignExpression, $node);
// resolve chain calls
$chainMethodCalls = $this->chainMethodCallNodeAnalyzer->collectAllMethodCallsInChainWithoutRootOne($node);
$chainMethodCalls = array_reverse($chainMethodCalls);
foreach ($chainMethodCalls as $chainMethodCall) {
$currentMethodCall = new MethodCall($newVariable, $chainMethodCall->name, $chainMethodCall->args);
$this->addNodeBeforeNode($currentMethodCall, $node);
}
$newStmts = $this->nonFluentMethodCallFactory->createFromNewAndRootMethodCall($new, $node);
$this->addNodesBeforeNode($newStmts, $node);
// change new arg to root variable
$newVariable = $this->crateVariableFromNew($new);
$rootMethodCall->args = [new Arg($newVariable)];
return $rootMethodCall;
}
/**
* Method call with "new X", that returns "X"?
* e.g.
*
* $this->setItem(new Item) // → returns "Item"
* @duplicated
*/
private function matchNewInFluentSetterMethodCall(MethodCall $methodCall): ?New_
{
if (count($methodCall->args) !== 1) {
return null;
}
$onlyArgValue = $methodCall->args[0]->value;
if (! $onlyArgValue instanceof New_) {
return null;
}
$newType = $this->getObjectType($onlyArgValue);
if ($newType instanceof MixedType) {
return null;
}
$parentMethodCallReturnType = $this->getObjectType($methodCall);
if (! $newType->equals($parentMethodCallReturnType)) {
return null;
}
return $onlyArgValue;
}
private function createAssignExpression(Variable $newVariable, New_ $new): Expression
{
$assign = new Assign($newVariable, $new);
return new Expression($assign);
}
private function shouldSkip(MethodCall $methodCall): bool
{
if (! $methodCall->var instanceof MethodCall) {
@ -173,4 +142,10 @@ PHP
return ! $this->chainMethodCallNodeAnalyzer->isLastChainMethodCall($methodCall);
}
private function crateVariableFromNew(New_ $new): Variable
{
$variableName = $this->variableNaming->resolveFromNode($new);
return new Variable($variableName);
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Rector\MagicDisclosure\Tests\Rector\MethodCall\InArgChainMethodCallToStandaloneMethodCallRector\Fixture;
use Rector\MagicDisclosure\Tests\Rector\MethodCall\InArgChainMethodCallToStandaloneMethodCallRector\Source\FluentClass;
class NewInArg
{
public function someFunction(FluentClass $someClass)
{
$this->processFluentClass((new FluentClass())->someFunction());
}
public function processFluentClass(FluentClass $someClass)
{
}
}
?>
-----
<?php
namespace Rector\MagicDisclosure\Tests\Rector\MethodCall\InArgChainMethodCallToStandaloneMethodCallRector\Fixture;
use Rector\MagicDisclosure\Tests\Rector\MethodCall\InArgChainMethodCallToStandaloneMethodCallRector\Source\FluentClass;
class NewInArg
{
public function someFunction(FluentClass $someClass)
{
$fluentClass = new FluentClass();
$fluentClass->someFunction();
$this->processFluentClass($fluentClass);
}
public function processFluentClass(FluentClass $someClass)
{
}
}
?>

View File

@ -0,0 +1,14 @@
<?php
namespace Rector\MagicDisclosure\Tests\Rector\MethodCall\InArgChainMethodCallToStandaloneMethodCallRector\Fixture;
use Rector\MagicDisclosure\Tests\Rector\MethodCall\InArgChainMethodCallToStandaloneMethodCallRector\Source\FluentClass;
class SkipNonArg
{
public function someFunction()
{
(new FluentClass())->otherFunction()
->someFunction();
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Rector\MagicDisclosure\Tests\Rector\MethodCall\InArgChainMethodCallToStandaloneMethodCallRector\Fixture;
use Rector\MagicDisclosure\Tests\Rector\MethodCall\InArgChainMethodCallToStandaloneMethodCallRector\Source\NonFluentClass;
class SkipNonFluentNewInArg
{
public function someFunction()
{
$this->processFluentClass((new NonFluentClass())->number());
}
public function processFluentClass(int $number)
{
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Rector\MagicDisclosure\Tests\Rector\MethodCall\InArgChainMethodCallToStandaloneMethodCallRector\Source;
final class NonFluentClass
{
public function number()
{
return 5;
}
public function letter()
{
return 'Z';
}
}