[Php70] add a rector to pass only variables as arguments by reference

This commit is contained in:
Lctrs 2020-01-08 09:56:26 +01:00
parent a21b805655
commit 85e73f9827
21 changed files with 697 additions and 23 deletions

View File

@ -23,3 +23,4 @@ services:
Rector\Php70\Rector\Break_\BreakNotInLoopOrSwitchToReturnRector: null
Rector\Php70\Rector\FuncCall\RenameMktimeWithoutArgsToTimeRector: null
Rector\Php70\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector: null

View File

@ -1,4 +1,4 @@
# All 425 Rectors Overview
# All 426 Rectors Overview
- [Projects](#projects)
- [General](#general)
@ -5275,6 +5275,19 @@ Changes multiple dirname() calls to one with nesting level
<br>
### `NonVariableToVariableOnFunctionCallRector`
- class: `Rector\Php70\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector`
Transform non variable like arguments to variable where a function or method expects an argument passed by reference
```diff
-reset(a());
+$a = a(); reset($a);
```
<br>
### `Php4ConstructorRector`
- class: `Rector\Php70\Rector\FunctionLike\Php4ConstructorRector`

View File

@ -90,7 +90,7 @@ PHP
$countInCond = $node;
$valueName = $this->getName($node->args[0]->value);
$countedValueName = $this->createCountedValueName($valueName, $for);
$countedValueName = $this->createCountedValueName($valueName, $for->getAttribute(AttributeKey::SCOPE));
return new Variable($countedValueName);
});
@ -105,7 +105,7 @@ PHP
return $node;
}
private function createCountedValueName(?string $valueName, For_ $for): string
protected function createCountedValueName(?string $valueName, ?Scope $scope): string
{
if ($valueName === null) {
$countedValueName = self::DEFAULT_VARIABLE_COUNT_NAME;
@ -113,25 +113,6 @@ PHP
$countedValueName = $valueName . 'Count';
}
/** @var Scope|null $forScope */
$forScope = $for->getAttribute(AttributeKey::SCOPE);
if ($forScope === null) {
return $countedValueName;
}
// make sure variable name is unique
if (! $forScope->hasVariableType($countedValueName)->yes()) {
return $countedValueName;
}
// we need to add number suffix until the variable is unique
$i = 2;
$countedValueNamePart = $countedValueName;
while ($forScope->hasVariableType($countedValueName)->yes()) {
$countedValueName = $countedValueNamePart . $i;
++$i;
}
return $countedValueName;
return parent::createCountedValueName($countedValueName, $scope);
}
}

View File

@ -8,3 +8,4 @@ services:
exclude:
- '../src/Rector/**/*Rector.php'
- '../src/Exception/*'
- '../src/ValueObject/*'

View File

@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace Rector\Php70\Rector\FuncCall;
use function lcfirst;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\AssignOp;
use PhpParser\Node\Expr\AssignRef;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Expr\StaticPropertyFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Return_;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ParameterReflection;
use Rector\CodingStyle\Naming\ClassNaming;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\Php70\Reflection\CallReflection;
use Rector\Php70\ValueObject\VariableAssignPair;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
/**
* @see https://www.php.net/manual/en/migration70.incompatible.php
*
* @see \Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector\NonVariableToVariableOnFunctionCallRectorTest
*/
final class NonVariableToVariableOnFunctionCallRector extends AbstractRector
{
/**
* @var string
*/
private const DEFAULT_VARIABLE_NAME = 'tmp';
/**
* @var CallReflection
*/
private $callReflection;
/**
* @var ClassNaming
*/
private $classNaming;
public function __construct(CallReflection $callReflection, ClassNaming $classNaming)
{
$this->callReflection = $callReflection;
$this->classNaming = $classNaming;
}
public function getDefinition(): RectorDefinition
{
return new RectorDefinition(
'Transform non variable like arguments to variable where a function or method expects an argument passed by reference',
[new CodeSample('reset(a());', '$a = a(); reset($a);')]
);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [FuncCall::class, MethodCall::class, StaticCall::class];
}
/**
* @param FuncCall|MethodCall|StaticCall $node
*/
public function refactor(Node $node): ?Node
{
$scope = $node->getAttribute(AttributeKey::SCOPE);
if (! $scope instanceof MutatingScope) {
return null;
}
$arguments = $this->getNonVariableArguments($node, $scope);
if ($arguments === []) {
return null;
}
foreach ($arguments as $key => $argument) {
$replacements = $this->getReplacementsFor($argument, $scope);
$current = $node->getAttribute(AttributeKey::CURRENT_STATEMENT);
$this->addNodeBeforeNode($replacements->assign(), $current instanceof Return_ ? $current : $node);
$node->args[$key]->value = $replacements->variable();
$scope = $scope->assignExpression($replacements->variable(), $scope->getType($replacements->variable()));
}
$node->setAttribute(AttributeKey::SCOPE, $scope);
return $node;
}
/**
* @param FuncCall|MethodCall|StaticCall $node
*
* @return array<int, Expr>
*/
private function getNonVariableArguments(Node $node, Scope $scope): array
{
$arguments = [];
/** @var ParameterReflection $parameter */
foreach ($this->callReflection->getParameterReflections($node, $scope) as $key => $parameter) {
// omitted optional parameter
if (! isset($node->args[$key])) {
continue;
}
if ($parameter->passedByReference()->no()) {
continue;
}
$argument = $node->args[$key]->value;
if ($this->isVariableLikeNode($argument)) {
continue;
}
$arguments[$key] = $argument;
}
return $arguments;
}
private function getReplacementsFor(Expr $expr, Scope $scope): VariableAssignPair
{
if (
(
$expr instanceof Assign
|| $expr instanceof AssignRef
|| $expr instanceof AssignOp
)
&& $this->isVariableLikeNode($expr->var)
) {
return new VariableAssignPair($expr->var, $expr);
}
$variable = new Variable($this->getVariableNameFor($expr, $scope));
return new VariableAssignPair($variable, new Assign($variable, $expr));
}
private function isVariableLikeNode(Node $node): bool
{
return $node instanceof Variable
|| $node instanceof ArrayDimFetch
|| $node instanceof PropertyFetch
|| $node instanceof StaticPropertyFetch;
}
private function getVariableNameFor(Expr $expr, Scope $scope): string
{
if ($expr instanceof New_ && $expr->class instanceof Name) {
$name = $this->classNaming->getShortName($expr->class);
} else {
$name = $this->getName($expr);
}
return lcfirst($this->createCountedValueName($name ?? self::DEFAULT_VARIABLE_NAME, $scope));
}
}

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Rector\Php70\Reflection;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Broker\Broker;
use PHPStan\Broker\FunctionNotFoundException;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ParametersAcceptorSelector;
use Rector\NodeTypeResolver\NodeTypeResolver;
use Rector\PhpParser\Node\Resolver\NameResolver;
final class CallReflection
{
/**
* @var Broker
*/
private $broker;
/**
* @var NodeTypeResolver
*/
private $nodeTypeResolver;
/**
* @var NameResolver
*/
private $nameResolver;
public function __construct(Broker $broker, NodeTypeResolver $nodeTypeResolver, NameResolver $nameResolver)
{
$this->broker = $broker;
$this->nodeTypeResolver = $nodeTypeResolver;
$this->nameResolver = $nameResolver;
}
/**
* @param FuncCall|MethodCall|StaticCall $node
*
* @return array<int, ParameterReflection>
*/
public function getParameterReflections(Node $node, Scope $scope): array
{
if ($node instanceof MethodCall || $node instanceof StaticCall) {
$classType = $this->nodeTypeResolver->getObjectType(
$node instanceof MethodCall ? $node->var : $node->class
);
$methodName = $this->nameResolver->getName($node->name);
if ($methodName === null || ! $classType->hasMethod($methodName)->yes()) {
return [];
}
return ParametersAcceptorSelector::selectSingle(
$classType->getMethod($methodName, $scope)->getVariants()
)->getParameters();
}
if ($node->name instanceof Expr) {
return $this->getParametersForExpr($node->name, $scope);
}
return $this->getParametersForName($node->name, $node->args, $scope);
}
/**
* @return array<int, ParameterReflection>
*/
private function getParametersForExpr(Expr $expr, Scope $scope): array
{
$type = $scope->getType($expr);
if (! $type instanceof ParametersAcceptor) {
return [];
}
return $type->getParameters();
}
/**
* @param Arg[] $args
*
* @return array<int, ParameterReflection>
*/
private function getParametersForName(Name $name, array $args, Scope $scope): array
{
try {
return ParametersAcceptorSelector::selectFromArgs(
$scope,
$args,
$this->broker->getFunction($name, $scope)->getVariants()
)->getParameters();
} catch (FunctionNotFoundException $functionNotFoundException) {
return [];
}
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Rector\Php70\ValueObject;
use PhpParser\Node;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\AssignOp;
use PhpParser\Node\Expr\AssignRef;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\StaticPropertyFetch;
use PhpParser\Node\Expr\Variable;
final class VariableAssignPair
{
/**
* @var Variable|ArrayDimFetch|PropertyFetch|StaticPropertyFetch
*/
private $variable;
/**
* @var Assign|AssignOp|AssignRef
*/
private $assign;
/**
* @param Variable|ArrayDimFetch|PropertyFetch|StaticPropertyFetch $variable
* @param Assign|AssignOp|AssignRef $node
*/
public function __construct(Node $variable, Node $node)
{
$this->variable = $variable;
$this->assign = $node;
}
/**
* @return Variable|ArrayDimFetch|PropertyFetch|StaticPropertyFetch
*/
public function variable(): Node
{
return $this->variable;
}
/**
* @return Assign|AssignOp|AssignRef
*/
public function assign(): Node
{
return $this->assign;
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector\Fixture;
function assignment()
{
reset($var = []);
reset($var = [0][0]);
reset($var = (new \stdClass())->bar);
reset($var = \stdClass::$bar);
}
?>
-----
<?php
namespace Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector\Fixture;
function assignment()
{
$var = [];
reset($var);
$var = [0][0];
reset($var);
$var = (new \stdClass())->bar;
reset($var);
$var = \stdClass::$bar;
reset($var);
}
?>

View File

@ -0,0 +1,25 @@
<?php
namespace Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector\Fixture;
function binaryOp()
{
reset(1 + 1);
reset(2 + $var);
}
?>
-----
<?php
namespace Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector\Fixture;
function binaryOp()
{
$tmp = 1 + 1;
reset($tmp);
$tmp = 2 + $var;
reset($tmp);
}
?>

View File

@ -0,0 +1,58 @@
<?php
namespace Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector\Fixture;
function byRef($bar, &$baz) {}
function allByRef(&$bar, &$baz) {}
function funcCalls()
{
reset(bar());
byRef(bar(), baz());
allByRef(bar(), baz());
allByRef(1, 2);
$anonymousFunction = function (&$bar) {};
$staticAnonymousFunction = static function (&$bar) {};
$anonymousFunction(bar());
$staticAnonymousFunction(bar());
return byRef(1, bar());
}
?>
-----
<?php
namespace Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector\Fixture;
function byRef($bar, &$baz) {}
function allByRef(&$bar, &$baz) {}
function funcCalls()
{
$bar = bar();
reset($bar);
$baz = baz();
byRef(bar(), $baz);
$bar = bar();
$baz = baz();
allByRef($bar, $baz);
$tmp = 1;
$tmp2 = 2;
allByRef($tmp, $tmp2);
$anonymousFunction = function (&$bar) {};
$staticAnonymousFunction = static function (&$bar) {};
$bar = bar();
$anonymousFunction($bar);
$bar = bar();
$staticAnonymousFunction($bar);
$bar = bar();
return byRef(1, $bar);
}
?>

View File

@ -0,0 +1,8 @@
<?php
namespace Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector\Fixture;
function arrayDimFetch()
{
reset([1][0]);
}

View File

@ -0,0 +1,8 @@
<?php
namespace Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector\Fixture;
function propertyFetch()
{
reset((new \stdClass())->dummy);
}

View File

@ -0,0 +1,8 @@
<?php
namespace Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector\Fixture;
function staticPropertyFetch()
{
reset(\stdClass::$dummy);
}

View File

@ -0,0 +1,8 @@
<?php
namespace Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector\Fixture;
function variable()
{
reset($var);
}

View File

@ -0,0 +1,25 @@
<?php
namespace Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector\Fixture;
function methodCallAsArgument()
{
reset((new \stdClass())->bar());
reset((new \stdClass())->bar()->baz());
}
?>
-----
<?php
namespace Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector\Fixture;
function methodCallAsArgument()
{
$bar = (new \stdClass())->bar();
reset($bar);
$baz = (new \stdClass())->bar()->baz();
reset($baz);
}
?>

View File

@ -0,0 +1,75 @@
<?php
namespace Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector\Fixture;
class Child
{
public function bar(&$bar) {}
}
class AClass
{
public static function staticMethod(&$bar) {}
public function baz(&$bar) {}
public function child(): Child
{
return new Child();
}
}
function methodCalls()
{
AClass::staticMethod(bar());
$aClass = new AClass();
$aClass->baz(baz());
$aClass->child()->bar(bar());
$anonymousClass = new class {
public function bar(&$baz) {}
};
$anonymousClass->bar(baz());
}
?>
-----
<?php
namespace Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector\Fixture;
class Child
{
public function bar(&$bar) {}
}
class AClass
{
public static function staticMethod(&$bar) {}
public function baz(&$bar) {}
public function child(): Child
{
return new Child();
}
}
function methodCalls()
{
$bar = bar();
AClass::staticMethod($bar);
$aClass = new AClass();
$baz = baz();
$aClass->baz($baz);
$bar = bar();
$aClass->child()->bar($bar);
$anonymousClass = new class {
public function bar(&$baz) {}
};
$baz = baz();
$anonymousClass->bar($baz);
}
?>

View File

@ -0,0 +1,29 @@
<?php
namespace Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector\Fixture;
class Bar {}
function new_()
{
reset(new \stdClass());
reset(new Bar());
}
?>
-----
<?php
namespace Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector\Fixture;
class Bar {}
function new_()
{
$stdClass = new \stdClass();
reset($stdClass);
$bar = new Bar();
reset($bar);
}
?>

View File

@ -0,0 +1,10 @@
<?php
namespace Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector\Fixture;
function withOptionalParameter(&$a = null) {}
function optionalParameter()
{
baz();
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Rector\Php70\Tests\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector;
use Iterator;
use Rector\Php70\Rector\FuncCall\NonVariableToVariableOnFunctionCallRector;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class NonVariableToVariableOnFunctionCallRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideDataForTest()
*/
public function test(string $file): void
{
$this->doTestFile($file);
}
public function provideDataForTest(): Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}
protected function getRectorClass(): string
{
return NonVariableToVariableOnFunctionCallRector::class;
}
}

View File

@ -256,3 +256,6 @@ parameters:
- '#Method (.*?) returns bool type, so the name should start with is/has/was#'
- '#Property Rector\\BetterPhpDocParser\\PhpDocNode\\Gedmo\\BlameableTagValueNode\:\:\$value has no typehint specified#'
# known value
- "#^Parameter \\#1 \\$variable of class Rector\\\\Php70\\\\ValueObject\\\\VariableAssignPair constructor expects PhpParser\\\\Node\\\\Expr\\\\ArrayDimFetch\\|PhpParser\\\\Node\\\\Expr\\\\PropertyFetch\\|PhpParser\\\\Node\\\\Expr\\\\StaticPropertyFetch\\|PhpParser\\\\Node\\\\Expr\\\\Variable, PhpParser\\\\Node\\\\Expr given\\.$#"

View File

@ -13,6 +13,7 @@ use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Expression;
use PhpParser\NodeVisitorAbstract;
use PHPStan\Analyser\Scope;
use Rector\Commander\CommanderCollector;
use Rector\Contract\PhpParser\Node\CommanderInterface;
use Rector\Contract\Rector\PhpRectorInterface;
@ -276,4 +277,26 @@ abstract class AbstractRector extends NodeVisitorAbstract implements PhpRectorIn
{
return ! $this->areNodesEqual($originalNode, $node);
}
protected function createCountedValueName(string $countedValueName, ?Scope $scope): string
{
if ($scope === null) {
return $countedValueName;
}
// make sure variable name is unique
if (! $scope->hasVariableType($countedValueName)->yes()) {
return $countedValueName;
}
// we need to add number suffix until the variable is unique
$i = 2;
$countedValueNamePart = $countedValueName;
while ($scope->hasVariableType($countedValueName)->yes()) {
$countedValueName = $countedValueNamePart . $i;
++$i;
}
return $countedValueName;
}
}