diff --git a/build/target-repository/docs/rector_rules_overview.md b/build/target-repository/docs/rector_rules_overview.md index 492de469499..54ec6559364 100644 --- a/build/target-repository/docs/rector_rules_overview.md +++ b/build/target-repository/docs/rector_rules_overview.md @@ -56,7 +56,7 @@ - [Transform](#transform) (23) -- [TypeDeclaration](#typedeclaration) (43) +- [TypeDeclaration](#typedeclaration) (44) - [Visibility](#visibility) (3) @@ -6406,6 +6406,21 @@ Add param types where needed
+### AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector + +Add param types where needed + +:wrench: **configure it!** + +- class: [`Rector\TypeDeclaration\Rector\FunctionLike\AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector`](../rules/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector.php) + +```diff +-(new SomeClass)->process(function ($parameter) {}); ++(new SomeClass)->process(function (string $parameter) {}); +``` + +
+ ### AddParamTypeFromPropertyTypeRector Adds param type declaration based on property type the value is assigned to PHPUnit provider return type declaration diff --git a/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRectorTest.php b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRectorTest.php new file mode 100644 index 00000000000..5db3f901fc1 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRectorTest.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/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector/Fixture/fixture.php.inc b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector/Fixture/fixture.php.inc new file mode 100644 index 00000000000..ee754278dae --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector/Fixture/fixture.php.inc @@ -0,0 +1,51 @@ +someCall(function ($name) { + return $name; +}); + +(new SomeClass())->someCall(function ($name) { + return $name; +}); + +$var->someCall(fn ($name) => $name); + +SomeClassForNamed::someCall('a', 'b', callback: fn ($var) => $var); + +?> +----- +someCall(function (string $name) { + return $name; +}); + +(new SomeClass())->someCall(function (string $name) { + return $name; +}); + +$var->someCall(fn (string $name) => $name); + +SomeClassForNamed::someCall('a', 'b', callback: fn (string $var) => $var); + +?> diff --git a/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector/Fixture/overrides_previous_type.php.inc b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector/Fixture/overrides_previous_type.php.inc new file mode 100644 index 00000000000..d6c346a51ff --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector/Fixture/overrides_previous_type.php.inc @@ -0,0 +1,19 @@ + $var); + +?> +----- + $var); + +?> diff --git a/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector/Fixture/skip_if_calllike_arg_is_named.php.inc b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector/Fixture/skip_if_calllike_arg_is_named.php.inc new file mode 100644 index 00000000000..b8e32fd01c2 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector/Fixture/skip_if_calllike_arg_is_named.php.inc @@ -0,0 +1,9 @@ + $var, callback: fn($var) => $var); + +?> diff --git a/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector/Fixture/skip_if_non_functionlike_parameter_missing.php.inc b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector/Fixture/skip_if_non_functionlike_parameter_missing.php.inc new file mode 100644 index 00000000000..6736220d6d8 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector/Fixture/skip_if_non_functionlike_parameter_missing.php.inc @@ -0,0 +1,7 @@ + 'test'); diff --git a/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector/Fixture/skip_if_non_functionlike_parameters_in_method_call.php.inc b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector/Fixture/skip_if_non_functionlike_parameters_in_method_call.php.inc new file mode 100644 index 00000000000..bc607559324 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector/Fixture/skip_if_non_functionlike_parameters_in_method_call.php.inc @@ -0,0 +1,7 @@ +ruleWithConfiguration(AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector::class, [ + new AddParamTypeForFunctionLikeWithinCallLikeArgDeclaration( + 'SomeNamespace\SomeClass', + 'someCall', + 0, + 0, + new StringType() + ), + new AddParamTypeForFunctionLikeWithinCallLikeArgDeclaration( + 'SomeNamespace\SomeClassForNamed', + 'someCall', + 'callback', + 0, + new StringType() + ), + ]); + + $rectorConfig->phpVersion(PhpVersionFeature::MIXED_TYPE); +}; diff --git a/rules/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector.php b/rules/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector.php new file mode 100644 index 00000000000..0a72dff38f9 --- /dev/null +++ b/rules/TypeDeclaration/Rector/FunctionLike/AddParamTypeForFunctionLikeWithinCallLikeArgDeclarationRector.php @@ -0,0 +1,225 @@ +process(function ($parameter) {}); +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +(new SomeClass)->process(function (string $parameter) {}); +CODE_SAMPLE + , + [ + new AddParamTypeForFunctionLikeWithinCallLikeArgDeclaration( + 'SomeClass', + 'process', + 0, + 0, + new StringType() + ), + ] + ), + ]); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [MethodCall::class, StaticCall::class]; + } + + /** + * @param CallLike $node + */ + public function refactor(Node $node): ?Node + { + $this->hasChanged = false; + foreach ($this->addParamTypeForFunctionLikeParamDeclarations as $addParamTypeForFunctionLikeParamDeclaration) { + $type = match (true) { + $node instanceof MethodCall => $node->var, + $node instanceof StaticCall => $node->class, + default => null, + }; + + if ($type === null) { + continue; + } + + if (! $this->isObjectType($type, $addParamTypeForFunctionLikeParamDeclaration->getObjectType())) { + continue; + } + + if (($node->name ?? null) === null) { + continue; + } + + if (! $node->name instanceof Identifier) { + continue; + } + + if (! $this->isName($node->name, $addParamTypeForFunctionLikeParamDeclaration->getMethodName())) { + continue; + } + + $this->processFunctionLike($node, $addParamTypeForFunctionLikeParamDeclaration); + } + + if (! $this->hasChanged) { + return null; + } + + return $node; + } + + /** + * @param mixed[] $configuration + */ + public function configure(array $configuration): void + { + Assert::allIsAOf($configuration, AddParamTypeForFunctionLikeWithinCallLikeArgDeclaration::class); + + $this->addParamTypeForFunctionLikeParamDeclarations = $configuration; + } + + private function processFunctionLike( + CallLike $callLike, + AddParamTypeForFunctionLikeWithinCallLikeArgDeclaration $addParamTypeForFunctionLikeWithinCallLikeArgDeclaration + ): void { + if ($callLike->isFirstClassCallable()) { + return; + } + + if (is_int($addParamTypeForFunctionLikeWithinCallLikeArgDeclaration->getCallLikePosition())) { + if ($callLike->getArgs() === []) { + return; + } + + $arg = $callLike->args[$addParamTypeForFunctionLikeWithinCallLikeArgDeclaration->getCallLikePosition()] ?? null; + + if (! $arg instanceof Arg) { + return; + } + + // int positions shouldn't have names + if ($arg->name !== null) { + return; + } + } else { + $args = array_filter($callLike->getArgs(), static function (Arg $arg) use ( + $addParamTypeForFunctionLikeWithinCallLikeArgDeclaration + ): bool { + if ($arg->name === null) { + return false; + } + + return $arg->name->name === $addParamTypeForFunctionLikeWithinCallLikeArgDeclaration->getCallLikePosition(); + }); + + if ($args === []) { + return; + } + + $arg = array_values($args)[0]; + } + + $functionLike = $arg->value; + if (! $functionLike instanceof FunctionLike) { + return; + } + + if (! isset($functionLike->params[$addParamTypeForFunctionLikeWithinCallLikeArgDeclaration->getFunctionLikePosition()])) { + return; + } + + $this->refactorParameter( + $functionLike->params[$addParamTypeForFunctionLikeWithinCallLikeArgDeclaration->getFunctionLikePosition()], + $addParamTypeForFunctionLikeWithinCallLikeArgDeclaration + ); + } + + private function refactorParameter( + Param $param, + AddParamTypeForFunctionLikeWithinCallLikeArgDeclaration $addParamTypeForFunctionLikeWithinCallLikeArgDeclaration + ): void { + // already set → no change + if ($param->type !== null) { + $currentParamType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type); + if ($this->typeComparator->areTypesEqual( + $currentParamType, + $addParamTypeForFunctionLikeWithinCallLikeArgDeclaration->getParamType() + )) { + return; + } + } + + $paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode( + $addParamTypeForFunctionLikeWithinCallLikeArgDeclaration->getParamType(), + TypeKind::PARAM + ); + + $this->hasChanged = true; + + // remove it + if ($addParamTypeForFunctionLikeWithinCallLikeArgDeclaration->getParamType() instanceof MixedType) { + if ($this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::MIXED_TYPE)) { + $param->type = $paramTypeNode; + return; + } + + $param->type = null; + return; + } + + $param->type = $paramTypeNode; + } +} diff --git a/rules/TypeDeclaration/ValueObject/AddParamTypeForFunctionLikeWithinCallLikeArgDeclaration.php b/rules/TypeDeclaration/ValueObject/AddParamTypeForFunctionLikeWithinCallLikeArgDeclaration.php new file mode 100644 index 00000000000..69f99f35908 --- /dev/null +++ b/rules/TypeDeclaration/ValueObject/AddParamTypeForFunctionLikeWithinCallLikeArgDeclaration.php @@ -0,0 +1,60 @@ +|string $callLikePosition + * @param int<0, max> $functionLikePosition + */ + public function __construct( + private string $className, + private string $methodName, + private int|string $callLikePosition, + private int $functionLikePosition, + private Type $paramType + ) { + RectorAssert::className($className); + } + + public function getObjectType(): ObjectType + { + return new ObjectType($this->className); + } + + public function getMethodName(): string + { + return $this->methodName; + } + + /** + * @return int<0, max>|string + */ + public function getCallLikePosition(): int|string + { + return $this->callLikePosition; + } + + /** + * @return int<0, max> + */ + public function getFunctionLikePosition(): int + { + return $this->functionLikePosition; + } + + public function getParamType(): Type + { + return $this->paramType; + } +}