diff --git a/config/set/php80.php b/config/set/php80.php index bab1593481a..34f21864ffa 100644 --- a/config/set/php80.php +++ b/config/set/php80.php @@ -18,6 +18,7 @@ use Rector\Php80\Rector\ClassMethod\FinalPrivateToPrivateVisibilityRector; use Rector\Php80\Rector\ClassMethod\SetStateToStaticRector; use Rector\Php80\Rector\FuncCall\ClassOnObjectRector; +use Rector\Php80\Rector\FuncCall\ConvertAssignToNamedArgumentRector; use Rector\Php80\Rector\Identical\StrEndsWithRector; use Rector\Php80\Rector\Identical\StrStartsWithRector; use Rector\Php80\Rector\NotIdentical\StrContainsRector; @@ -34,6 +35,7 @@ StrEndsWithRector::class, StringableForToStringRector::class, ClassOnObjectRector::class, + ConvertAssignToNamedArgumentRector::class, GetDebugTypeRector::class, RemoveUnusedVariableInCatchRector::class, ClassPropertyAssignToConstructorPromotionRector::class, diff --git a/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/ConvertAssignToNamedArgumentRectorTest.php b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/ConvertAssignToNamedArgumentRectorTest.php new file mode 100644 index 00000000000..66729d6f4dc --- /dev/null +++ b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/ConvertAssignToNamedArgumentRectorTest.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/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/arrow_function.php.inc b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/arrow_function.php.inc new file mode 100644 index 00000000000..41d639c4e45 --- /dev/null +++ b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/arrow_function.php.inc @@ -0,0 +1,15 @@ + in_array('foo', ['bar'], $strict = true); + +?> +----- + in_array('foo', ['bar'], strict: true); + +?> diff --git a/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/basic_function_call.php.inc b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/basic_function_call.php.inc new file mode 100644 index 00000000000..9c415571d03 --- /dev/null +++ b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/basic_function_call.php.inc @@ -0,0 +1,21 @@ + +----- + \ No newline at end of file diff --git a/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/builtin_functions.php.inc b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/builtin_functions.php.inc new file mode 100644 index 00000000000..b7011d9ba76 --- /dev/null +++ b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/builtin_functions.php.inc @@ -0,0 +1,17 @@ + +----- + diff --git a/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/class_methods.php.inc b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/class_methods.php.inc new file mode 100644 index 00000000000..3f431e1bcd1 --- /dev/null +++ b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/class_methods.php.inc @@ -0,0 +1,21 @@ +process($data = 'test', $options = ['key' => 'value']); + +?> +----- +process(data: 'test', options: ['key' => 'value']); + +?> diff --git a/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/closure.php.inc b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/closure.php.inc new file mode 100644 index 00000000000..3b8515ba185 --- /dev/null +++ b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/closure.php.inc @@ -0,0 +1,19 @@ + +----- + diff --git a/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/double_assign.php.inc b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/double_assign.php.inc new file mode 100644 index 00000000000..195ec6475e4 --- /dev/null +++ b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/double_assign.php.inc @@ -0,0 +1,17 @@ + +----- + diff --git a/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/file_without_namespace.php.inc b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/file_without_namespace.php.inc new file mode 100644 index 00000000000..13c5469e099 --- /dev/null +++ b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/file_without_namespace.php.inc @@ -0,0 +1,11 @@ + +----- + diff --git a/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/method_call.php.inc b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/method_call.php.inc new file mode 100644 index 00000000000..15111c5ca36 --- /dev/null +++ b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/method_call.php.inc @@ -0,0 +1,35 @@ +someClass->process($data = 'test', $options = ['key' => 'value']); + } +} + +?> +----- +someClass->process(data: 'test', options: ['key' => 'value']); + } +} + +?> diff --git a/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/nested_arguments.php.inc b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/nested_arguments.php.inc new file mode 100644 index 00000000000..20ec210cba8 --- /dev/null +++ b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/nested_arguments.php.inc @@ -0,0 +1,15 @@ + +----- + diff --git a/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/skip_already_has_named_args.php.inc b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/skip_already_has_named_args.php.inc new file mode 100644 index 00000000000..367f3b28dc4 --- /dev/null +++ b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/skip_already_has_named_args.php.inc @@ -0,0 +1,7 @@ + diff --git a/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/skip_unknown_function.php.inc b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/skip_unknown_function.php.inc new file mode 100644 index 00000000000..f0e1aab00f9 --- /dev/null +++ b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/skip_unknown_function.php.inc @@ -0,0 +1,7 @@ + diff --git a/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/skip_variable_name_doesnt_match.php.inc b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/skip_variable_name_doesnt_match.php.inc new file mode 100644 index 00000000000..22597a217fc --- /dev/null +++ b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/skip_variable_name_doesnt_match.php.inc @@ -0,0 +1,7 @@ + diff --git a/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/skip_variable_used_later.php.inc b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/skip_variable_used_later.php.inc new file mode 100644 index 00000000000..2049257a1db --- /dev/null +++ b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/skip_variable_used_later.php.inc @@ -0,0 +1,8 @@ + diff --git a/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/skip_variadic_function.php.inc b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/skip_variadic_function.php.inc new file mode 100644 index 00000000000..8143587fd59 --- /dev/null +++ b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/skip_variadic_function.php.inc @@ -0,0 +1,7 @@ + diff --git a/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/static_call.php.inc b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/static_call.php.inc new file mode 100644 index 00000000000..645999d7ef8 --- /dev/null +++ b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/static_call.php.inc @@ -0,0 +1,19 @@ + +----- + diff --git a/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/variable_used_prior.php.inc b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/variable_used_prior.php.inc new file mode 100644 index 00000000000..bb3f5ce49c3 --- /dev/null +++ b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Fixture/variable_used_prior.php.inc @@ -0,0 +1,17 @@ + +----- + diff --git a/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Source/SomeClass.php b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Source/SomeClass.php new file mode 100644 index 00000000000..d737676cd27 --- /dev/null +++ b/rules-tests/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector/Source/SomeClass.php @@ -0,0 +1,12 @@ +withRules([ConvertAssignToNamedArgumentRector::class]); diff --git a/rules/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector.php b/rules/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector.php new file mode 100644 index 00000000000..7345e13f974 --- /dev/null +++ b/rules/Php80/Rector/FuncCall/ConvertAssignToNamedArgumentRector.php @@ -0,0 +1,212 @@ +> + */ + public function getNodeTypes(): array + { + return [ClassMethod::class, Function_::class, Namespace_::class, FileWithoutNamespace::class]; + } + + /** + * @param ClassMethod|Function_|Namespace_|FileWithoutNamespace $node + */ + public function refactor(Node $node): ?Node + { + $stmts = $node->stmts; + + if ($stmts === null || $stmts === []) { + return null; + } + + $hasChanged = false; + $stmtIndex = 0; + + $this->traverseNodesWithCallable($stmts, function (Node $subNode) use ( + &$hasChanged, + &$stmtIndex, + $stmts + ): ?int { + if ($subNode instanceof Stmt) { + ++$stmtIndex; + return null; + } + + if (! $subNode instanceof FuncCall && ! $subNode instanceof MethodCall && ! $subNode instanceof StaticCall) { + return null; + } + + if ($subNode->isFirstClassCallable()) { + return null; + } + + $hasChanged |= $this->refactorCall($subNode, array_slice($stmts, $stmtIndex)); + + return null; + }); + + return $hasChanged ? $node : null; + } + + /** + * @param Stmt[] $stmts + */ + private function refactorCall(FuncCall|MethodCall|StaticCall $call, array $stmts): bool + { + $args = $call->getArgs(); + $hasChanged = false; + + if ($this->argsAnalyzer->hasNamedArg($args)) { + return false; + } + + foreach ($args as $position => $arg) { + if (! $arg->value instanceof Assign) { + continue; + } + + $assign = $arg->value; + + if (! $assign->var instanceof Variable) { + continue; + } + + $variable = $this->getName($assign->var); + + if ($variable === null) { + continue; + } + + if ($this->isVariableUsed($assign->var, $stmts)) { + continue; + } + + $parameter = $this->getParameterAtPosition($call, $position); + + if ($parameter === null || $parameter->isVariadic()) { + continue; + } + + if ($variable !== $parameter->getName()) { + continue; + } + + $arg->value = $assign->expr; + $arg->name = new Identifier($variable); + $hasChanged = true; + } + + return $hasChanged; + } + + /** + * @param Node[]|Node $nodes + */ + private function isVariableUsed(Variable $variable, array|Node $nodes): bool + { + $isUsed = false; + $this->traverseNodesWithCallable($nodes, function (Node $subNode) use ($variable, &$isUsed): ?int { + if ($subNode instanceof Assign && $subNode->var instanceof Variable) { + if ($this->getName($subNode->var) !== $this->getName($variable)) { + return null; + } + + if (! $this->isVariableUsed($variable, $subNode->expr)) { + return NodeVisitor::DONT_TRAVERSE_CHILDREN; + } + + $isUsed = true; + + return NodeVisitor::STOP_TRAVERSAL; + } + + if ($this->exprUsedInNodeAnalyzer->isUsed($subNode, $variable)) { + $isUsed = true; + + return NodeVisitor::STOP_TRAVERSAL; + } + + return null; + }); + + return $isUsed; + } + + private function getParameterAtPosition(FuncCall|MethodCall|StaticCall $node, int $position): ?ParameterReflection + { + $reflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node); + + if (! $reflection instanceof FunctionReflection && ! $reflection instanceof MethodReflection) { + return null; + } + + $parametersAcceptor = ParametersAcceptorSelector::combineAcceptors($reflection->getVariants()); + + return $parametersAcceptor->getParameters()[$position] ?? null; + } +} diff --git a/src/ValueObject/PhpVersionFeature.php b/src/ValueObject/PhpVersionFeature.php index 998219643c1..4866810e955 100644 --- a/src/ValueObject/PhpVersionFeature.php +++ b/src/ValueObject/PhpVersionFeature.php @@ -430,6 +430,12 @@ final class PhpVersionFeature */ public const DEPRECATE_HEBREVC = PhpVersion::PHP_74; + /** + * @see https://wiki.php.net/rfc/named_params + * @var int + */ + public const NAMED_ARGUMENTS = PhpVersion::PHP_80; + /** * @var int */