diff --git a/config/set/named-args.php b/config/set/named-args.php index 34f4859a561..cf4480a4786 100644 --- a/config/set/named-args.php +++ b/config/set/named-args.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Rector\CodeQuality\Rector\Attribute\ExplicitAttributeNamedArgsRector; use Rector\CodeQuality\Rector\Attribute\SortAttributeNamedArgsRector; use Rector\CodeQuality\Rector\CallLike\AddNameToBooleanArgumentRector; use Rector\CodeQuality\Rector\CallLike\AddNameToNullArgumentRector; @@ -17,6 +18,7 @@ RemoveNullNamedArgOnNullDefaultParamRector::class, SortCallLikeNamedArgsRector::class, SortAttributeNamedArgsRector::class, + ExplicitAttributeNamedArgsRector::class, UtilsJsonStaticCallNamedArgRector::class, ]); }; diff --git a/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/ExplicitAttributeNamedArgsRectorTest.php b/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/ExplicitAttributeNamedArgsRectorTest.php new file mode 100644 index 00000000000..cb9c17c8a37 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/ExplicitAttributeNamedArgsRectorTest.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/Attribute/ExplicitAttributeNamedArgsRector/Fixture/other_attribute.php.inc b/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/Fixture/other_attribute.php.inc new file mode 100644 index 00000000000..1a891cb27e1 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/Fixture/other_attribute.php.inc @@ -0,0 +1,25 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/Fixture/positional_except.php.inc b/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/Fixture/positional_except.php.inc new file mode 100644 index 00000000000..dbdeb04caf3 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/Fixture/positional_except.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/Fixture/positional_middleware.php.inc b/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/Fixture/positional_middleware.php.inc new file mode 100644 index 00000000000..8186c5f9aa1 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/Fixture/positional_middleware.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/Fixture/positional_only.php.inc b/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/Fixture/positional_only.php.inc new file mode 100644 index 00000000000..dd9dd8ec645 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/Fixture/positional_only.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/Fixture/skip_already_named.php.inc b/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/Fixture/skip_already_named.php.inc new file mode 100644 index 00000000000..ca9badf0b8a --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/Fixture/skip_already_named.php.inc @@ -0,0 +1,11 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/Source/Middleware.php b/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/Source/Middleware.php new file mode 100644 index 00000000000..87ff9b96a20 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector/Source/Middleware.php @@ -0,0 +1,11 @@ +rule(ExplicitAttributeNamedArgsRector::class); +}; diff --git a/rules/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector.php b/rules/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector.php new file mode 100644 index 00000000000..5ac76052bd8 --- /dev/null +++ b/rules/CodeQuality/Rector/Attribute/ExplicitAttributeNamedArgsRector.php @@ -0,0 +1,131 @@ +> + */ + public function getNodeTypes(): array + { + return [Attribute::class]; + } + + /** + * @param Attribute $node + */ + public function refactor(Node $node): ?Node + { + $methodReflection = $this->reflectionResolver->resolveConstructorReflectionFromAttribute($node); + if (! $methodReflection instanceof MethodReflection) { + return null; + } + + $extendedParametersAcceptor = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants()); + $parameters = $extendedParametersAcceptor->getParameters(); + + $namesToApply = $this->resolveArgNamesToApply($node->args, $parameters); + if ($namesToApply === []) { + return null; + } + + foreach ($namesToApply as $position => $name) { + $node->args[$position]->name = new Identifier($name); + } + + return $node; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::NAMED_ARGUMENTS; + } + + /** + * Resolve the positional arguments to name, as a position => parameter-name map, or [] when + * nothing should change. Naming an argument forces every later positional argument to be named + * too (PHP forbids a positional argument after a named one). So if any argument maps to a + * variadic parameter, or to no parameter at all (overflow past a variadic), the whole attribute + * is left untouched rather than producing invalid PHP. + * + * @param Arg[] $args + * @param ParameterReflection[] $parameters + * @return array + */ + private function resolveArgNamesToApply(array $args, array $parameters): array + { + $namesToApply = []; + + foreach ($args as $position => $arg) { + // already named + if ($arg->name instanceof Identifier) { + continue; + } + + $parameter = $parameters[$position] ?? null; + + // no matching parameter, e.g. overflow past a variadic + if ($parameter === null) { + return []; + } + + // naming a variadic would rebind it or strand later positional arguments + if ($parameter->isVariadic()) { + return []; + } + + $namesToApply[$position] = $parameter->getName(); + } + + return $namesToApply; + } +}