From 1db588f48de7e9345ccb5c52de8bf9596482b9fe Mon Sep 17 00:00:00 2001 From: Caleb White Date: Wed, 20 May 2026 21:38:16 -0500 Subject: [PATCH] feat: Add AddNameToNullArgumentRector --- .../AddNameToNullArgumentRectorTest.php | 28 +++ .../Fixture/fixture.php.inc | 29 +++ .../Fixture/skip_already_named.php.inc | 10 + .../Fixture/skip_no_null.php.inc | 10 + .../Source/Service.php | 21 +++ .../config/configured_rule.php | 9 + .../AddNameToBooleanArgumentRector.php | 153 +--------------- .../CallLike/AddNameToNullArgumentRector.php | 69 +++++++ .../CallLikeArgumentNameAdder.php | 173 ++++++++++++++++++ 9 files changed, 355 insertions(+), 147 deletions(-) create mode 100644 rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/AddNameToNullArgumentRectorTest.php create mode 100644 rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/Fixture/fixture.php.inc create mode 100644 rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/Fixture/skip_already_named.php.inc create mode 100644 rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/Fixture/skip_no_null.php.inc create mode 100644 rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/Source/Service.php create mode 100644 rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/config/configured_rule.php create mode 100644 rules/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector.php create mode 100644 src/NodeAnalyzer/CallLikeArgumentNameAdder.php diff --git a/rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/AddNameToNullArgumentRectorTest.php b/rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/AddNameToNullArgumentRectorTest.php new file mode 100644 index 00000000000..9946e08c7ca --- /dev/null +++ b/rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/AddNameToNullArgumentRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/Fixture/fixture.php.inc b/rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/Fixture/fixture.php.inc new file mode 100644 index 00000000000..7ed2ffece1c --- /dev/null +++ b/rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/Fixture/fixture.php.inc @@ -0,0 +1,29 @@ +configure($value, null); + Service::create($value, null); + new Service($value, null); +}; + +?> +----- +configure($value, default: null); + Service::create($value, default: null); + new Service($value, default: null); +}; + +?> diff --git a/rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/Fixture/skip_already_named.php.inc b/rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/Fixture/skip_already_named.php.inc new file mode 100644 index 00000000000..5edb690132b --- /dev/null +++ b/rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/Fixture/skip_already_named.php.inc @@ -0,0 +1,10 @@ +configure($value, default: null); +}; diff --git a/rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/Fixture/skip_no_null.php.inc b/rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/Fixture/skip_no_null.php.inc new file mode 100644 index 00000000000..0d98efbfaa2 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/Fixture/skip_no_null.php.inc @@ -0,0 +1,10 @@ +configure($value, 'some_value'); +}; diff --git a/rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/Source/Service.php b/rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/Source/Service.php new file mode 100644 index 00000000000..6d67a759549 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector/Source/Service.php @@ -0,0 +1,21 @@ +withRules([AddNameToNullArgumentRector::class]); diff --git a/rules/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector.php b/rules/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector.php index 70f0e838cd1..97cb901d6a9 100644 --- a/rules/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector.php +++ b/rules/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector.php @@ -5,17 +5,10 @@ namespace Rector\CodeQuality\Rector\CallLike; use PhpParser\Node; -use PhpParser\Node\Arg; use PhpParser\Node\Expr\CallLike; -use PhpParser\Node\Identifier; -use PHPStan\Reflection\FunctionReflection; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ParameterReflection; -use Rector\NodeTypeResolver\PHPStan\ParametersAcceptorSelectorVariantsWrapper; +use Rector\NodeAnalyzer\CallLikeArgumentNameAdder; use Rector\PhpParser\Node\Value\ValueResolver; -use Rector\PHPStan\ScopeFetcher; use Rector\Rector\AbstractRector; -use Rector\Reflection\ReflectionResolver; use Rector\ValueObject\PhpVersionFeature; use Rector\VersionBonding\Contract\MinPhpVersionInterface; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; @@ -27,7 +20,7 @@ final class AddNameToBooleanArgumentRector extends AbstractRector implements MinPhpVersionInterface { public function __construct( - private readonly ReflectionResolver $reflectionResolver, + private readonly CallLikeArgumentNameAdder $callLikeArgumentNameAdder, private readonly ValueResolver $valueResolver, ) { } @@ -63,148 +56,14 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Node { - if ($this->shouldSkip($node)) { - return null; - } - - $reflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node); - if (! $reflection instanceof FunctionReflection && ! $reflection instanceof MethodReflection) { - return null; - } - - $scope = ScopeFetcher::fetch($node); - $args = $node->getArgs(); - $parameters = ParametersAcceptorSelectorVariantsWrapper::select($reflection, $node, $scope) - ->getParameters(); - - $position = $this->resolveFirstPositionToName($args, $parameters); - if ($position === null) { - return null; - } - - $wasChanged = false; - $counter = count($args); - for ($i = $position; $i < $counter; ++$i) { - $arg = $args[$i]; - if ($arg->name instanceof Identifier) { - continue; - } - - $parameterReflection = $this->resolveParameterReflection($arg, $i, $parameters); - if (! $parameterReflection instanceof ParameterReflection) { - return null; - } - - $arg->name = new Identifier($parameterReflection->getName()); - $wasChanged = true; - } - - if (! $wasChanged) { - return null; - } - - return $node; + return $this->callLikeArgumentNameAdder->addNamesToArgs( + $node, + fn ($expr): bool => $this->valueResolver->isTrueOrFalse($expr), + ); } public function provideMinPhpVersion(): int { return PhpVersionFeature::NAMED_ARGUMENTS; } - - private function shouldSkip(CallLike $callLike): bool - { - if ($callLike->isFirstClassCallable()) { - return true; - } - - $args = $callLike->getArgs(); - if ($args === []) { - return true; - } - - foreach ($args as $arg) { - if ($arg->unpack) { - return true; - } - } - - return false; - } - - /** - * @param Arg[] $args - * @param ParameterReflection[] $parameters - */ - private function resolveFirstPositionToName(array $args, array $parameters): ?int - { - foreach ($args as $position => $arg) { - if ($arg->name instanceof Identifier) { - continue; - } - - if (! $this->valueResolver->isTrueOrFalse($arg->value)) { - continue; - } - - if ($this->canNameArgsFromPosition($args, $parameters, $position)) { - return $position; - } - } - - return null; - } - - /** - * @param Arg[] $args - * @param ParameterReflection[] $parameters - */ - private function canNameArgsFromPosition(array $args, array $parameters, int $position): bool - { - $count = count($args); - for ($i = $position; $i < $count; ++$i) { - $arg = $args[$i]; - if ($arg->name instanceof Identifier) { - continue; - } - - $parameterReflection = $this->resolveParameterReflection($arg, $i, $parameters); - if (! $parameterReflection instanceof ParameterReflection) { - return false; - } - - if ($parameterReflection->isVariadic()) { - return false; - } - } - - return true; - } - - /** - * @param ParameterReflection[] $parameters - */ - private function resolveParameterReflection(Arg $arg, int $position, array $parameters): ?ParameterReflection - { - if ($arg->name instanceof Identifier) { - foreach ($parameters as $parameter) { - if ($parameter->getName() === $arg->name->toString()) { - return $parameter; - } - } - - return null; - } - - $parameter = $parameters[$position] ?? null; - if ($parameter instanceof ParameterReflection) { - return $parameter; - } - - $lastParameter = end($parameters); - if ($lastParameter instanceof ParameterReflection && $lastParameter->isVariadic()) { - return $lastParameter; - } - - return null; - } } diff --git a/rules/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector.php b/rules/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector.php new file mode 100644 index 00000000000..a34b136c5e0 --- /dev/null +++ b/rules/CodeQuality/Rector/CallLike/AddNameToNullArgumentRector.php @@ -0,0 +1,69 @@ +> + */ + public function getNodeTypes(): array + { + return [CallLike::class]; + } + + /** + * @param CallLike $node + */ + public function refactor(Node $node): ?Node + { + return $this->callLikeArgumentNameAdder->addNamesToArgs( + $node, + fn ($expr): bool => $this->valueResolver->isNull($expr), + ); + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::NAMED_ARGUMENTS; + } +} diff --git a/src/NodeAnalyzer/CallLikeArgumentNameAdder.php b/src/NodeAnalyzer/CallLikeArgumentNameAdder.php new file mode 100644 index 00000000000..503737382c4 --- /dev/null +++ b/src/NodeAnalyzer/CallLikeArgumentNameAdder.php @@ -0,0 +1,173 @@ +shouldSkip($node)) { + return null; + } + + $reflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node); + if (! $reflection instanceof FunctionReflection && ! $reflection instanceof MethodReflection) { + return null; + } + + $scope = ScopeFetcher::fetch($node); + $args = $node->getArgs(); + $parameters = ParametersAcceptorSelectorVariantsWrapper::select($reflection, $node, $scope) + ->getParameters(); + + $position = $this->resolveFirstPositionToName($args, $parameters, $shouldNameArgValue); + if ($position === null) { + return null; + } + + $wasChanged = false; + $counter = count($args); + for ($i = $position; $i < $counter; ++$i) { + $arg = $args[$i]; + if ($arg->name instanceof Identifier) { + continue; + } + + $parameterReflection = $this->resolveParameterReflection($arg, $i, $parameters); + if (! $parameterReflection instanceof ParameterReflection) { + return null; + } + + $arg->name = new Identifier($parameterReflection->getName()); + $wasChanged = true; + } + + if (! $wasChanged) { + return null; + } + + return $node; + } + + private function shouldSkip(CallLike $callLike): bool + { + if ($callLike->isFirstClassCallable()) { + return true; + } + + $args = $callLike->getArgs(); + if ($args === []) { + return true; + } + + foreach ($args as $arg) { + if ($arg->unpack) { + return true; + } + } + + return false; + } + + /** + * @param Arg[] $args + * @param ParameterReflection[] $parameters + * @param callable(\PhpParser\Node\Expr): bool $shouldNameArgValue + */ + private function resolveFirstPositionToName(array $args, array $parameters, callable $shouldNameArgValue): ?int + { + foreach ($args as $position => $arg) { + if ($arg->name instanceof Identifier) { + continue; + } + + if (! $shouldNameArgValue($arg->value)) { + continue; + } + + if ($this->canNameArgsFromPosition($args, $parameters, $position)) { + return $position; + } + } + + return null; + } + + /** + * @param Arg[] $args + * @param ParameterReflection[] $parameters + */ + private function canNameArgsFromPosition(array $args, array $parameters, int $position): bool + { + $count = count($args); + for ($i = $position; $i < $count; ++$i) { + $arg = $args[$i]; + if ($arg->name instanceof Identifier) { + continue; + } + + $parameterReflection = $this->resolveParameterReflection($arg, $i, $parameters); + if (! $parameterReflection instanceof ParameterReflection) { + return false; + } + + if ($parameterReflection->isVariadic()) { + return false; + } + } + + return true; + } + + /** + * @param ParameterReflection[] $parameters + */ + private function resolveParameterReflection(Arg $arg, int $position, array $parameters): ?ParameterReflection + { + if ($arg->name instanceof Identifier) { + foreach ($parameters as $parameter) { + if ($parameter->getName() === $arg->name->toString()) { + return $parameter; + } + } + + return null; + } + + $parameter = $parameters[$position] ?? null; + if ($parameter instanceof ParameterReflection) { + return $parameter; + } + + $lastParameter = end($parameters); + if ($lastParameter instanceof ParameterReflection && $lastParameter->isVariadic()) { + return $lastParameter; + } + + return null; + } +}