diff --git a/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/AttributeNamedArgsRectorFirstNamedPositionTest.php b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/AttributeNamedArgsRectorFirstNamedPositionTest.php new file mode 100644 index 00000000000..b6a986a395e --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/AttributeNamedArgsRectorFirstNamedPositionTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/FixtureFirstNamedPosition'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule_first_named_position.php'; + } +} diff --git a/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/AttributeNamedArgsRectorTest.php b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/AttributeNamedArgsRectorTest.php new file mode 100644 index 00000000000..0293e82123b --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/AttributeNamedArgsRectorTest.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/AttributeNamedArgsRector/Fixture/positional_except.php.inc b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/Fixture/positional_except.php.inc new file mode 100644 index 00000000000..a1a5519f2be --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/Fixture/positional_except.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/Fixture/positional_middleware.php.inc b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/Fixture/positional_middleware.php.inc new file mode 100644 index 00000000000..d5e1188a94b --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/Fixture/positional_middleware.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/Fixture/positional_only.php.inc b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/Fixture/positional_only.php.inc new file mode 100644 index 00000000000..3c4bf778795 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/Fixture/positional_only.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/Fixture/skip_already_named.php.inc b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/Fixture/skip_already_named.php.inc new file mode 100644 index 00000000000..7076df4b111 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/Fixture/skip_already_named.php.inc @@ -0,0 +1,11 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/FixtureFirstNamedPosition/positional_except.php.inc b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/FixtureFirstNamedPosition/positional_except.php.inc new file mode 100644 index 00000000000..5d29d159a01 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/FixtureFirstNamedPosition/positional_except.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/FixtureFirstNamedPosition/positional_only.php.inc b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/FixtureFirstNamedPosition/positional_only.php.inc new file mode 100644 index 00000000000..1f048f0173b --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/FixtureFirstNamedPosition/positional_only.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/FixtureFirstNamedPosition/skip_middleware_left_positional.php.inc b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/FixtureFirstNamedPosition/skip_middleware_left_positional.php.inc new file mode 100644 index 00000000000..5f1b08412de --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/FixtureFirstNamedPosition/skip_middleware_left_positional.php.inc @@ -0,0 +1,11 @@ +ruleWithConfiguration(AttributeNamedArgsRector::class, [ + new AttributeNamedArgs(Middleware::class), + new AttributeNamedArgs(MiddlewareWithVariadic::class), + ]); +}; diff --git a/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/config/configured_rule_first_named_position.php b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/config/configured_rule_first_named_position.php new file mode 100644 index 00000000000..f7e976081cd --- /dev/null +++ b/rules-tests/CodeQuality/Rector/Attribute/AttributeNamedArgsRector/config/configured_rule_first_named_position.php @@ -0,0 +1,14 @@ +ruleWithConfiguration(AttributeNamedArgsRector::class, [ + new AttributeNamedArgs(Middleware::class, 1), + ]); +}; diff --git a/rules/CodeQuality/Rector/Attribute/AttributeNamedArgsRector.php b/rules/CodeQuality/Rector/Attribute/AttributeNamedArgsRector.php new file mode 100644 index 00000000000..cd3541b28cf --- /dev/null +++ b/rules/CodeQuality/Rector/Attribute/AttributeNamedArgsRector.php @@ -0,0 +1,169 @@ +> + */ + public function getNodeTypes(): array + { + return [Attribute::class]; + } + + /** + * @param Attribute $node + */ + public function refactor(Node $node): ?Node + { + foreach ($this->attributeNamedArgs as $attributeNamedArg) { + if ($this->isName($node->name, $attributeNamedArg->getAttributeClass())) { + return $this->nameArguments($node, $attributeNamedArg); + } + } + + return null; + } + + /** + * @param mixed[] $configuration + */ + public function configure(array $configuration): void + { + Assert::allIsAOf($configuration, AttributeNamedArgs::class); + + $this->attributeNamedArgs = $configuration; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::NAMED_ARGUMENTS; + } + + private function nameArguments(Attribute $attribute, AttributeNamedArgs $attributeNamedArgs): ?Attribute + { + $methodReflection = $this->reflectionResolver->resolveConstructorReflectionFromAttribute($attribute); + if (! $methodReflection instanceof MethodReflection) { + return null; + } + + $extendedParametersAcceptor = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants()); + $parameters = $extendedParametersAcceptor->getParameters(); + + $namesToApply = $this->resolveArgNamesToApply( + $attribute->args, + $parameters, + $attributeNamedArgs->getFirstNamedPosition() + ); + if ($namesToApply === []) { + return null; + } + + foreach ($namesToApply as $position => $name) { + $attribute->args[$position]->name = new Identifier($name); + } + + return $attribute; + } + + /** + * 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 in the named + * range 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, int $firstNamedPosition): array + { + $namesToApply = []; + + $count = count($args); + for ($position = $firstNamedPosition; $position < $count; ++$position) { + $arg = $args[$position]; + + // 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; + } +} diff --git a/rules/CodeQuality/ValueObject/AttributeNamedArgs.php b/rules/CodeQuality/ValueObject/AttributeNamedArgs.php new file mode 100644 index 00000000000..226aeaaccec --- /dev/null +++ b/rules/CodeQuality/ValueObject/AttributeNamedArgs.php @@ -0,0 +1,31 @@ +attributeClass; + } + + public function getFirstNamedPosition(): int + { + return $this->firstNamedPosition; + } +}