diff --git a/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector/AddClosureParamTypeFromObjectRectorTest.php b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector/AddClosureParamTypeFromObjectRectorTest.php new file mode 100644 index 00000000000..f917e091800 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector/AddClosureParamTypeFromObjectRectorTest.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/AddClosureParamTypeFromObjectRector/Fixture/fixture.php.inc b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector/Fixture/fixture.php.inc new file mode 100644 index 00000000000..460ad8285a1 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector/Fixture/fixture.php.inc @@ -0,0 +1,23 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector/Fixture/overrides_previous_type.php.inc b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector/Fixture/overrides_previous_type.php.inc new file mode 100644 index 00000000000..162debdb415 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector/Fixture/overrides_previous_type.php.inc @@ -0,0 +1,21 @@ + $var); + +?> +----- + $var); + +?> diff --git a/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector/Fixture/skip_if_calllike_arg_is_named.php.inc b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector/Fixture/skip_if_calllike_arg_is_named.php.inc new file mode 100644 index 00000000000..9bc78adc1ee --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector/Fixture/skip_if_calllike_arg_is_named.php.inc @@ -0,0 +1,7 @@ + $var, callback: fn($var) => $var); diff --git a/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector/Fixture/skip_if_non_functionlike_parameter_missing.php.inc b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector/Fixture/skip_if_non_functionlike_parameter_missing.php.inc new file mode 100644 index 00000000000..4482214ba0c --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector/Fixture/skip_if_non_functionlike_parameter_missing.php.inc @@ -0,0 +1,7 @@ + 'test'); diff --git a/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector/Fixture/skip_if_non_functionlike_parameters_in_method_call.php.inc b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector/Fixture/skip_if_non_functionlike_parameters_in_method_call.php.inc new file mode 100644 index 00000000000..a6ffb56d782 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector/Fixture/skip_if_non_functionlike_parameters_in_method_call.php.inc @@ -0,0 +1,7 @@ +ruleWithConfiguration(AddClosureParamTypeFromObjectRector::class, [ + new AddClosureParamTypeFromObject(SimpleContainer::class, 'someCall', 1, 0), + ]); + + $rectorConfig->phpVersion(PhpVersionFeature::MIXED_TYPE); +}; diff --git a/rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector.php b/rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector.php new file mode 100644 index 00000000000..6a47127a7f2 --- /dev/null +++ b/rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromObjectRector.php @@ -0,0 +1,187 @@ +when(true, function ($request) {}); +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +$request = new Request(); +$request->when(true, function (Request $request) {}); +CODE_SAMPLE + , + [new AddClosureParamTypeFromObject('Request', 'when', 1, 0)] + ), + ]); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [MethodCall::class, StaticCall::class]; + } + + /** + * @param MethodCall|StaticCall $node + */ + public function refactor(Node $node): ?Node + { + foreach ($this->addClosureParamTypeFromObjects as $addClosureParamTypeFromObject) { + if ($node instanceof MethodCall) { + $caller = $node->var; + } elseif ($node instanceof StaticCall) { + $caller = $node->class; + } else { + continue; + } + + if (! $this->isCallMatch($caller, $addClosureParamTypeFromObject, $node)) { + continue; + } + + $type = $this->getType($caller); + if (! $type instanceof ObjectType) { + continue; + } + + return $this->processCallLike($node, $addClosureParamTypeFromObject, $type); + } + + return null; + } + + /** + * @param mixed[] $configuration + */ + public function configure(array $configuration): void + { + Assert::allIsAOf($configuration, AddClosureParamTypeFromObject::class); + + $this->addClosureParamTypeFromObjects = $configuration; + } + + private function processCallLike( + MethodCall|StaticCall $callLike, + AddClosureParamTypeFromObject $addClosureParamTypeFromArg, + ObjectType $objectType + ): MethodCall|StaticCall|null { + if ($callLike->isFirstClassCallable()) { + return null; + } + + $callLikeArg = $callLike->args[$addClosureParamTypeFromArg->getCallLikePosition()] ?? null; + if (! $callLikeArg instanceof Arg) { + return null; + } + + // int positions shouldn't have names + if ($callLikeArg->name instanceof Identifier) { + return null; + } + + $functionLike = $callLikeArg->value; + if (! $functionLike instanceof Closure && ! $functionLike instanceof ArrowFunction) { + return null; + } + + if (! isset($functionLike->params[$addClosureParamTypeFromArg->getFunctionLikePosition()])) { + return null; + } + + $callLikeArg = $callLike->getArgs()[self::DEFAULT_CLOSURE_ARG_POSITION] ?? null; + if (! $callLikeArg instanceof Arg) { + return null; + } + + $hasChanged = $this->refactorParameter( + $functionLike->params[$addClosureParamTypeFromArg->getFunctionLikePosition()], + $objectType, + ); + + if ($hasChanged) { + return $callLike; + } + + return null; + } + + private function refactorParameter(Param $param, ObjectType $objectType): bool + { + // already set → no change + if ($param->type instanceof Node) { + $currentParamType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type); + if ($this->typeComparator->areTypesEqual($currentParamType, $objectType)) { + return false; + } + } + + $paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($objectType, TypeKind::PARAM); + $param->type = $paramTypeNode; + + return true; + } + + private function isCallMatch( + Name|Expr $name, + AddClosureParamTypeFromObject $addClosureParamTypeFromArg, + StaticCall|MethodCall $call + ): bool { + if (! $this->isObjectType($name, $addClosureParamTypeFromArg->getObjectType())) { + return false; + } + + return $this->isName($call->name, $addClosureParamTypeFromArg->getMethodName()); + } +} diff --git a/rules/TypeDeclaration/ValueObject/AddClosureParamTypeFromObject.php b/rules/TypeDeclaration/ValueObject/AddClosureParamTypeFromObject.php new file mode 100644 index 00000000000..82cba439845 --- /dev/null +++ b/rules/TypeDeclaration/ValueObject/AddClosureParamTypeFromObject.php @@ -0,0 +1,50 @@ + $callLikePosition + * @param int<0, max> $functionLikePosition + */ + public function __construct( + private string $className, + private string $methodName, + private int $callLikePosition, + private int $functionLikePosition, + ) { + RectorAssert::className($className); + } + + public function getObjectType(): ObjectType + { + return new ObjectType($this->className); + } + + public function getMethodName(): string + { + return $this->methodName; + } + + /** + * @return int<0, max> + */ + public function getCallLikePosition(): int + { + return $this->callLikePosition; + } + + /** + * @return int<0, max> + */ + public function getFunctionLikePosition(): int + { + return $this->functionLikePosition; + } +}