[Arguments] Add the missing default value and type for the added arguments (#837)

This commit is contained in:
Zing 2021-09-19 17:02:33 +08:00 committed by GitHub
parent 327929f620
commit 3ff17513c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 296 additions and 10 deletions

View File

@ -0,0 +1,57 @@
<?php
namespace Rector\Tests\Arguments\Rector\ClassMethod\ArgumentAdderRector\Fixture;
use Rector\Tests\Arguments\Rector\ClassMethod\ArgumentAdderRector\Source\SomeClass;
class ArgumentWithoutType extends SomeClass
{
public function withoutTypeOrDefaultValue($arguments = [])
{
}
}
class ArgumentWithoutDefaultValue extends SomeClass
{
public function withoutTypeOrDefaultValue(array $arguments)
{
}
}
class ArgumentWithoutTypeAndDefaultValue extends SomeClass
{
public function withoutTypeOrDefaultValue($arguments)
{
}
}
?>
-----
<?php
namespace Rector\Tests\Arguments\Rector\ClassMethod\ArgumentAdderRector\Fixture;
use Rector\Tests\Arguments\Rector\ClassMethod\ArgumentAdderRector\Source\SomeClass;
class ArgumentWithoutType extends SomeClass
{
public function withoutTypeOrDefaultValue(array $arguments = [])
{
}
}
class ArgumentWithoutDefaultValue extends SomeClass
{
public function withoutTypeOrDefaultValue(array $arguments = [])
{
}
}
class ArgumentWithoutTypeAndDefaultValue extends SomeClass
{
public function withoutTypeOrDefaultValue(array $arguments = [])
{
}
}
?>

View File

@ -0,0 +1,8 @@
<?php
namespace Rector\Tests\Arguments\Rector\ClassMethod\ArgumentAdderRector\Source;
class SomeClass
{
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
use Rector\Arguments\NodeAnalyzer\ArgumentAddingScope;
use Rector\Arguments\Rector\ClassMethod\ArgumentAdderRector;
use Rector\Arguments\ValueObject\ArgumentAdder;
use Rector\Tests\Arguments\Rector\ClassMethod\ArgumentAdderRector\Source\SomeClass;
use Rector\Tests\Arguments\Rector\ClassMethod\ArgumentAdderRector\Source\SomeContainerBuilder;
use Rector\Tests\Arguments\Rector\ClassMethod\ArgumentAdderRector\Source\SomeParentClient;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
@ -45,6 +46,7 @@ return static function (ContainerConfigurator $containerConfigurator): void {
'array',
ArgumentAddingScope::SCOPE_CLASS_METHOD
),
new ArgumentAdder(SomeClass::class, 'withoutTypeOrDefaultValue', 0, 'arguments', [], 'array'),
]),
]]);
};

View File

@ -0,0 +1,47 @@
<?php
namespace Rector\Tests\Arguments\Rector\ClassMethod\ReplaceArgumentDefaultValueRector\Fixture;
use Rector\Tests\Arguments\Rector\ClassMethod\ReplaceArgumentDefaultValueRector\Source\SomeClassWithAnyDefaultValue;
class ParamWithoutDefaultValue extends SomeClassWithAnyDefaultValue
{
public function someMethod($argument)
{
}
}
class ParamWithScalar extends SomeClassWithAnyDefaultValue
{
public function someMethod($argument = 1)
{
}
}
?>
-----
<?php
namespace Rector\Tests\Arguments\Rector\ClassMethod\ReplaceArgumentDefaultValueRector\Fixture;
use Rector\Tests\Arguments\Rector\ClassMethod\ReplaceArgumentDefaultValueRector\Source\SomeClassWithAnyDefaultValue;
class ParamWithoutDefaultValue extends SomeClassWithAnyDefaultValue
{
public function someMethod($argument = [])
{
}
}
class ParamWithScalar extends SomeClassWithAnyDefaultValue
{
public function someMethod($argument = [])
{
}
}
?>

View File

@ -0,0 +1,31 @@
<?php
namespace Rector\Tests\Arguments\Rector\ClassMethod\ReplaceArgumentDefaultValueRector\Fixture;
use Rector\Tests\Arguments\Rector\ClassMethod\ReplaceArgumentDefaultValueRector\Source\SomeClassWithAnyDefaultValue;
class ParamWithNull extends SomeClassWithAnyDefaultValue
{
public function paramWithNull($argument = null)
{
}
}
?>
-----
<?php
namespace Rector\Tests\Arguments\Rector\ClassMethod\ReplaceArgumentDefaultValueRector\Fixture;
use Rector\Tests\Arguments\Rector\ClassMethod\ReplaceArgumentDefaultValueRector\Source\SomeClassWithAnyDefaultValue;
class ParamWithNull extends SomeClassWithAnyDefaultValue
{
public function paramWithNull($argument = [])
{
}
}
?>

View File

@ -0,0 +1,8 @@
<?php
namespace Rector\Tests\Arguments\Rector\ClassMethod\ReplaceArgumentDefaultValueRector\Source;
class SomeClassWithAnyDefaultValue
{
}

View File

@ -49,6 +49,20 @@ return static function (ContainerConfigurator $containerConfigurator): void {
'Symfony\Component\Yaml\Yaml::DUMP_EXCEPTION_ON_INVALID_TYPE'
),
new ReplaceArgumentDefaultValue(
'Rector\Tests\Arguments\Rector\ClassMethod\ReplaceArgumentDefaultValueRector\Source\SomeClassWithAnyDefaultValue',
'someMethod',
0,
ReplaceArgumentDefaultValue::ANY_VALUE_BEFORE,
[]
),
new ReplaceArgumentDefaultValue(
'Rector\Tests\Arguments\Rector\ClassMethod\ReplaceArgumentDefaultValueRector\Source\SomeClassWithAnyDefaultValue',
'paramWithNull',
0,
null,
[]
),
]),
]]);
};

View File

@ -7,11 +7,14 @@ namespace Rector\Arguments;
use PhpParser\BuilderHelpers;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\Arguments\Contract\ReplaceArgumentDefaultValueInterface;
use Rector\Arguments\ValueObject\ReplaceArgumentDefaultValue;
use Rector\Core\PhpParser\Node\NodeFactory;
use Rector\Core\PhpParser\Node\Value\ValueResolver;
@ -31,17 +34,60 @@ final class ArgumentDefaultValueReplacer
if (! isset($node->params[$replaceArgumentDefaultValue->getPosition()])) {
return null;
}
} elseif (isset($node->args[$replaceArgumentDefaultValue->getPosition()])) {
$this->processArgs($node, $replaceArgumentDefaultValue);
return $this->processParams($node, $replaceArgumentDefaultValue);
}
return $node;
if (! isset($node->args[$replaceArgumentDefaultValue->getPosition()])) {
return null;
}
return $this->processArgs($node, $replaceArgumentDefaultValue);
}
/**
* @param mixed $value
*/
public function isDefaultValueMatched(?Expr $expr, $value): bool
{
// allow any values before, also allow param without default value
if ($value === ReplaceArgumentDefaultValue::ANY_VALUE_BEFORE) {
return true;
}
if ($expr === null) {
return false;
}
if ($this->valueResolver->isValue($expr, $value)) {
return true;
}
// ValueResolver::isValue returns false when default value is `null`
return $value === null && $this->valueResolver->isNull($expr);
}
private function processParams(
ClassMethod $classMethod,
ReplaceArgumentDefaultValueInterface $replaceArgumentDefaultValue
): ?ClassMethod {
$position = $replaceArgumentDefaultValue->getPosition();
if (! $this->isDefaultValueMatched(
$classMethod->params[$position]->default,
$replaceArgumentDefaultValue->getValueBefore()
)) {
return null;
}
$classMethod->params[$position]->default = $this->normalizeValue($replaceArgumentDefaultValue->getValueAfter());
return $classMethod;
}
private function processArgs(
MethodCall | StaticCall | FuncCall $expr,
ReplaceArgumentDefaultValueInterface $replaceArgumentDefaultValue
): void {
): Expr {
$position = $replaceArgumentDefaultValue->getPosition();
$argValue = $this->valueResolver->getValue($expr->args[$position]->value);
@ -57,22 +103,30 @@ final class ArgumentDefaultValueReplacer
$expr->args = $newArgs;
}
}
return $expr;
}
/**
* @param mixed $value
*/
private function normalizeValueToArgument($value): Arg
{
return new Arg($this->normalizeValue($value));
}
/**
* @param mixed $value
*/
private function normalizeValue($value): ClassConstFetch|Expr
{
// class constants → turn string to composite
if (is_string($value) && \str_contains($value, '::')) {
[$class, $constant] = explode('::', $value);
$classConstFetch = $this->nodeFactory->createClassConstFetch($class, $constant);
return new Arg($classConstFetch);
return $this->nodeFactory->createClassConstFetch($class, $constant);
}
return new Arg(BuilderHelpers::normalizeValue($value));
return BuilderHelpers::normalizeValue($value);
}
/**

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Rector\Arguments\NodeAnalyzer;
use PhpParser\Node\Param;
use Rector\Core\PhpParser\Node\Value\ValueResolver;
use Rector\NodeNameResolver\NodeNameResolver;
final class ChangedArgumentsDetector
{
public function __construct(
private ValueResolver $valueResolver,
private NodeNameResolver $nodeNameResolver
) {
}
/**
* @param mixed $value
*/
public function isDefaultValueChanged(Param $param, $value): bool
{
if ($param->default === null) {
return false;
}
return ! $this->valueResolver->isValue($param->default, $value);
}
public function isTypeChanged(Param $param, ?string $type): bool
{
if ($param->type === null) {
return false;
}
if ($type === null) {
return true;
}
return ! $this->nodeNameResolver->isName($param->type, $type);
}
}

View File

@ -18,6 +18,7 @@ use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Type\ObjectType;
use Rector\Arguments\NodeAnalyzer\ArgumentAddingScope;
use Rector\Arguments\NodeAnalyzer\ChangedArgumentsDetector;
use Rector\Arguments\ValueObject\ArgumentAdder;
use Rector\Core\Contract\Rector\ConfigurableRectorInterface;
use Rector\Core\Exception\ShouldNotHappenException;
@ -45,7 +46,8 @@ final class ArgumentAdderRector extends AbstractRector implements ConfigurableRe
private bool $haveArgumentsChanged = false;
public function __construct(
private ArgumentAddingScope $argumentAddingScope
private ArgumentAddingScope $argumentAddingScope,
private ChangedArgumentsDetector $changedArgumentsDetector
) {
}
@ -202,7 +204,22 @@ CODE_SAMPLE
return false;
}
return $this->isName($node->params[$position], $argumentName);
$param = $node->params[$position];
// argument added and name has been changed
if (! $this->isName($param, $argumentName)) {
return true;
}
// argument added and default has been changed
if ($this->changedArgumentsDetector->isDefaultValueChanged(
$param,
$argumentAdder->getArgumentDefaultValue()
)) {
return true;
}
// argument added and type has been changed
return $this->changedArgumentsDetector->isTypeChanged($param, $argumentAdder->getArgumentType());
}
if (isset($node->args[$position])) {

View File

@ -9,6 +9,11 @@ use Rector\Arguments\Contract\ReplaceArgumentDefaultValueInterface;
final class ReplaceArgumentDefaultValue implements ReplaceArgumentDefaultValueInterface
{
/**
* @var string
*/
public const ANY_VALUE_BEFORE = '*ANY_VALUE_BEFORE*';
/**
* @param mixed $valueBefore
* @param mixed $valueAfter