From ad9d5d6fe26728dd31005b6c7b6206ba21c74da0 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Sat, 28 Mar 2026 15:54:48 -0500 Subject: [PATCH] feat: create rector to add names to boolean arguments --- .../AddNameToBooleanArgumentRectorTest.php | 28 +++ .../Fixture/fixture.php.inc | 33 +++ .../Fixture/name_following_arguments.php.inc | 15 ++ .../Fixture/skip_already_named.php.inc | 5 + .../Fixture/skip_first_class_callable.php.inc | 5 + .../Fixture/skip_unpack.php.inc | 10 + .../Source/Service.php | 21 ++ .../config/configured_rule.php | 9 + .../AddNameToBooleanArgumentRector.php | 202 ++++++++++++++++++ 9 files changed, 328 insertions(+) create mode 100644 rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/AddNameToBooleanArgumentRectorTest.php create mode 100644 rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/Fixture/fixture.php.inc create mode 100644 rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/Fixture/name_following_arguments.php.inc create mode 100644 rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/Fixture/skip_already_named.php.inc create mode 100644 rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/Fixture/skip_first_class_callable.php.inc create mode 100644 rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/Fixture/skip_unpack.php.inc create mode 100644 rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/Source/Service.php create mode 100644 rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/config/configured_rule.php create mode 100644 rules/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector.php diff --git a/rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/AddNameToBooleanArgumentRectorTest.php b/rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/AddNameToBooleanArgumentRectorTest.php new file mode 100644 index 00000000000..b620c2f8260 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/AddNameToBooleanArgumentRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return AddNameToBooleanArgumentRectorTest::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/Fixture/fixture.php.inc b/rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/Fixture/fixture.php.inc new file mode 100644 index 00000000000..8d9677cae92 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/Fixture/fixture.php.inc @@ -0,0 +1,33 @@ +configure($value, true); + Service::create($value, true); + new Service($value, true); +}; + +?> +----- +configure($value, strict: true); + Service::create($value, strict: true); + new Service($value, strict: true); +}; + +?> diff --git a/rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/Fixture/name_following_arguments.php.inc b/rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/Fixture/name_following_arguments.php.inc new file mode 100644 index 00000000000..b51be838a83 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/Fixture/name_following_arguments.php.inc @@ -0,0 +1,15 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/Fixture/skip_already_named.php.inc b/rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/Fixture/skip_already_named.php.inc new file mode 100644 index 00000000000..3eeb2088bf3 --- /dev/null +++ b/rules-tests/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector/Fixture/skip_already_named.php.inc @@ -0,0 +1,5 @@ +withRules([AddNameToBooleanArgumentRector::class]); diff --git a/rules/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector.php b/rules/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector.php new file mode 100644 index 00000000000..29366f7e920 --- /dev/null +++ b/rules/CodeQuality/Rector/CallLike/AddNameToBooleanArgumentRector.php @@ -0,0 +1,202 @@ +> + */ + public function getNodeTypes(): array + { + return [CallLike::class]; + } + + /** + * @param CallLike $node + */ + 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; + for ($i = $position; $i < count($args); ++$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 + */ + 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; + } +}