diff --git a/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector/AddClosureParamTypeFromVariableCallRectorTest.php b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector/AddClosureParamTypeFromVariableCallRectorTest.php new file mode 100644 index 00000000000..1115f0803f1 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector/AddClosureParamTypeFromVariableCallRectorTest.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/AddClosureParamTypeFromVariableCallRector/Fixture/object_param.php.inc b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector/Fixture/object_param.php.inc new file mode 100644 index 00000000000..9bddd73fae7 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector/Fixture/object_param.php.inc @@ -0,0 +1,39 @@ +name; + }; + + $printItem(new Item()); + } +} + +?> +----- +name; + }; + + $printItem(new Item()); + } +} + +?> diff --git a/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector/Fixture/recursive_form_view.php.inc b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector/Fixture/recursive_form_view.php.inc new file mode 100644 index 00000000000..a23f4240260 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector/Fixture/recursive_form_view.php.inc @@ -0,0 +1,41 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector/Fixture/skip_already_typed.php.inc b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector/Fixture/skip_already_typed.php.inc new file mode 100644 index 00000000000..b283e0b1485 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector/Fixture/skip_already_typed.php.inc @@ -0,0 +1,17 @@ +name; + }; + + $printItem(new Item()); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector/Fixture/skip_conflicting_types.php.inc b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector/Fixture/skip_conflicting_types.php.inc new file mode 100644 index 00000000000..8e45e1e9da5 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector/Fixture/skip_conflicting_types.php.inc @@ -0,0 +1,19 @@ + + */ +final class FormView implements \ArrayAccess +{ + public function offsetExists($offset): bool + { + return true; + } + + public function offsetGet($offset): self + { + return $this; + } + + public function offsetSet($offset, $value): void + { + } + + public function offsetUnset($offset): void + { + } +} diff --git a/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector/Source/Item.php b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector/Source/Item.php new file mode 100644 index 00000000000..4124b40429c --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector/Source/Item.php @@ -0,0 +1,10 @@ +withRules([AddClosureParamTypeFromVariableCallRector::class]); diff --git a/rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector.php b/rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector.php new file mode 100644 index 00000000000..27e882b5632 --- /dev/null +++ b/rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeFromVariableCallRector.php @@ -0,0 +1,221 @@ +name; +}; + +$printItem(new Item()); +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +$printItem = function (Item $item) { + echo $item->name; +}; + +$printItem(new Item()); +CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [ClassMethod::class, Function_::class, Closure::class]; + } + + /** + * @param ClassMethod|Function_|Closure $node + */ + public function refactor(Node $node): ?Node + { + if ($node->stmts === null) { + return null; + } + + $closuresByVariableName = $this->collectAssignedClosures($node->stmts); + if ($closuresByVariableName === []) { + return null; + } + + $hasChanged = false; + + foreach ($closuresByVariableName as $variableName => $closure) { + $argumentTypesByPosition = $this->collectCallArgumentTypes($node->stmts, $variableName); + + foreach ($closure->getParams() as $position => $param) { + if (! isset($argumentTypesByPosition[$position])) { + continue; + } + + if ($this->refactorClosureParam($param, $argumentTypesByPosition[$position])) { + $hasChanged = true; + } + } + } + + if ($hasChanged) { + return $node; + } + + return null; + } + + /** + * @param Node\Stmt[] $stmts + * @return array + */ + private function collectAssignedClosures(array $stmts): array + { + $closuresByVariableName = []; + + $this->traverseNodesWithCallable($stmts, function (Node $node) use (&$closuresByVariableName): int|null { + // do not descend into nested scopes + if ($node instanceof Class_ || $node instanceof FunctionLike) { + return NodeVisitor::DONT_TRAVERSE_CHILDREN; + } + + if (! $node instanceof Assign) { + return null; + } + + if (! $node->var instanceof Variable) { + return null; + } + + if (! $node->expr instanceof Closure) { + return null; + } + + $variableName = $this->getName($node->var); + if ($variableName === null) { + return null; + } + + $closuresByVariableName[$variableName] = $node->expr; + return null; + }); + + return $closuresByVariableName; + } + + /** + * @param Node\Stmt[] $stmts + * @return array + */ + private function collectCallArgumentTypes(array $stmts, string $variableName): array + { + $objectTypesByPosition = []; + $blockedPositions = []; + + $this->traverseNodesWithCallable($stmts, function (Node $node) use ( + $variableName, + &$objectTypesByPosition, + &$blockedPositions + ): int|null { + // do not descend into nested scopes, e.g. recursive self-calls + if ($node instanceof Class_ || $node instanceof FunctionLike) { + return NodeVisitor::DONT_TRAVERSE_CHILDREN; + } + + if (! $node instanceof FuncCall || $node->isFirstClassCallable()) { + return null; + } + + if (! $node->name instanceof Variable || ! $this->isName($node->name, $variableName)) { + return null; + } + + foreach ($node->getArgs() as $position => $arg) { + if ($arg->unpack) { + $blockedPositions[$position] = true; + continue; + } + + $argType = $this->getType($arg->value); + if (! $argType instanceof ObjectType) { + $blockedPositions[$position] = true; + continue; + } + + // conflicting object types across calls → skip this position + if (isset($objectTypesByPosition[$position]) + && $objectTypesByPosition[$position]->getClassName() !== $argType->getClassName()) { + $blockedPositions[$position] = true; + continue; + } + + $objectTypesByPosition[$position] = $argType; + } + + return null; + }); + + foreach (array_keys($blockedPositions) as $position) { + unset($objectTypesByPosition[$position]); + } + + return $objectTypesByPosition; + } + + private function refactorClosureParam(Param $param, ObjectType $objectType): bool + { + if ($param->type instanceof Node) { + return false; + } + + if ($param->variadic) { + return false; + } + + $paramTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($objectType, TypeKind::PARAM); + if (! $paramTypeNode instanceof Node) { + return false; + } + + $param->type = $paramTypeNode; + return true; + } +} diff --git a/src/Config/Level/TypeDeclarationLevel.php b/src/Config/Level/TypeDeclarationLevel.php index 9698fa73c33..a4772b1557e 100644 --- a/src/Config/Level/TypeDeclarationLevel.php +++ b/src/Config/Level/TypeDeclarationLevel.php @@ -70,6 +70,7 @@ use Rector\TypeDeclaration\Rector\FunctionLike\AddClosureParamTypeForArrayMapRector; use Rector\TypeDeclaration\Rector\FunctionLike\AddClosureParamTypeForArrayReduceRector; use Rector\TypeDeclaration\Rector\FunctionLike\AddClosureParamTypeFromIterableMethodCallRector; +use Rector\TypeDeclaration\Rector\FunctionLike\AddClosureParamTypeFromVariableCallRector; use Rector\TypeDeclaration\Rector\FunctionLike\AddParamTypeSplFixedArrayRector; use Rector\TypeDeclaration\Rector\FunctionLike\AddReturnTypeDeclarationFromYieldsRector; use Rector\TypeDeclaration\Rector\Property\TypedPropertyFromAssignsRector; @@ -173,6 +174,7 @@ final class TypeDeclarationLevel ScalarTypedPropertyFromJMSSerializerAttributeTypeRector::class, // array parameter from dim fetch assign inside + AddClosureParamTypeFromVariableCallRector::class, StrictArrayParamDimFetchRector::class, AddParamFromDimFetchKeyUseRector::class, AddParamStringTypeFromSprintfUseRector::class,