From bbfd6bcbaf4b41d7128a2ba7cce7b8d8bc4c8b37 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 12 Sep 2025 17:28:39 +0200 Subject: [PATCH 1/2] [type-declaration-docblocks] Add PrivateClassMethodArrayDocblockParamFromLocalCallsRector --- config/set/type-declaration-docblocks.php | 2 + ...yDocblockParamFromLocalCallsRectorTest.php | 28 ++++ .../handle_non_final_protected.php.inc | 38 +++++ ...le_non_final_protected_with_parent.php.inc | 42 ++++++ .../handle_protected_method_in_final.php.inc | 42 ++++++ .../Fixture/handle_public.php.inc | 44 ++++++ .../Fixture/multiple_calls.php.inc | 42 ++++++ .../Fixture/some_class.php.inc | 38 +++++ .../Source/SafeParentClass.php | 7 + .../config/configured_rule.php | 10 ++ ...ddMethodCallBasedStrictParamTypeRector.php | 21 +-- .../PrivateMethodFlagger.php | 28 ++++ ...ArrayDocblockParamFromLocalCallsRector.php | 132 ++++++++++++++++++ 13 files changed, 456 insertions(+), 18 deletions(-) create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/ClassMethodArrayDocblockParamFromLocalCallsRectorTest.php create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/handle_non_final_protected.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/handle_non_final_protected_with_parent.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/handle_protected_method_in_final.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/handle_public.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/multiple_calls.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/some_class.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Source/SafeParentClass.php create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/config/configured_rule.php create mode 100644 rules/TypeDeclarationDocblocks/PrivateMethodFlagger.php create mode 100644 rules/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector.php diff --git a/config/set/type-declaration-docblocks.php b/config/set/type-declaration-docblocks.php index 90b75fcc9ae..84901eca1c1 100644 --- a/config/set/type-declaration-docblocks.php +++ b/config/set/type-declaration-docblocks.php @@ -5,6 +5,7 @@ use Rector\Config\RectorConfig; use Rector\TypeDeclaration\Rector\ClassMethod\AddReturnArrayDocblockBasedOnArrayMapRector; use Rector\TypeDeclaration\Rector\ClassMethod\AddReturnDocblockForScalarArrayFromAssignsRector; +use Rector\TypeDeclarationDocblocks\Rector\Class_\ClassMethodArrayDocblockParamFromLocalCallsRector; use Rector\TypeDeclarationDocblocks\Rector\Class_\DocblockVarFromParamDocblockInConstructorRector; use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddParamArrayDocblockFromDataProviderRector; use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddParamArrayDocblockFromDimFetchAccessRector; @@ -24,5 +25,6 @@ AddReturnDocblockForCommonObjectDenominatorRector::class, AddParamArrayDocblockFromDimFetchAccessRector::class, AddParamArrayDocblockFromDataProviderRector::class, + ClassMethodArrayDocblockParamFromLocalCallsRector::class, ]); }; diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/ClassMethodArrayDocblockParamFromLocalCallsRectorTest.php b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/ClassMethodArrayDocblockParamFromLocalCallsRectorTest.php new file mode 100644 index 00000000000..ca978805c6b --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/ClassMethodArrayDocblockParamFromLocalCallsRectorTest.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/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/handle_non_final_protected.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/handle_non_final_protected.php.inc new file mode 100644 index 00000000000..97c433b0dea --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/handle_non_final_protected.php.inc @@ -0,0 +1,38 @@ +run(['item1', 'item2']); + } + + protected function run(array $items) + { + } +} + +?> +----- +run(['item1', 'item2']); + } + + /** + * @param string[] $items + */ + protected function run(array $items) + { + } +} + +?> diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/handle_non_final_protected_with_parent.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/handle_non_final_protected_with_parent.php.inc new file mode 100644 index 00000000000..8b93297caec --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/handle_non_final_protected_with_parent.php.inc @@ -0,0 +1,42 @@ +run(['item1', 'item2']); + } + + protected function run(array $items) + { + } +} + +?> +----- +run(['item1', 'item2']); + } + + /** + * @param string[] $items + */ + protected function run(array $items) + { + } +} + +?> diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/handle_protected_method_in_final.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/handle_protected_method_in_final.php.inc new file mode 100644 index 00000000000..8c3477ee311 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/handle_protected_method_in_final.php.inc @@ -0,0 +1,42 @@ +run([2512, 3423]); + + $this->run([324, 534]); + } + + protected function run(array $items) + { + } +} + +?> +----- +run([2512, 3423]); + + $this->run([324, 534]); + } + + /** + * @param int[] $items + */ + protected function run(array $items) + { + } +} + +?> diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/handle_public.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/handle_public.php.inc new file mode 100644 index 00000000000..9976d430394 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/handle_public.php.inc @@ -0,0 +1,44 @@ +run([2512, 3423]); + + $this->run([324, 534]); + } + + public function run(array $items) + { + } +} + +?> + +----- +run([2512, 3423]); + + $this->run([324, 534]); + } + + /** + * @param int[] $items + */ + public function run(array $items) + { + } +} + +?> + diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/multiple_calls.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/multiple_calls.php.inc new file mode 100644 index 00000000000..841d8799bf1 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/multiple_calls.php.inc @@ -0,0 +1,42 @@ +run(['item1', 'item2']); + + $this->run(['item1', 'item2']); + } + + private function run(array $items) + { + } +} + +?> +----- +run(['item1', 'item2']); + + $this->run(['item1', 'item2']); + } + + /** + * @param string[] $items + */ + private function run(array $items) + { + } +} + +?> diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/some_class.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/some_class.php.inc new file mode 100644 index 00000000000..41d2eeed4bc --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/some_class.php.inc @@ -0,0 +1,38 @@ +run(['item1', 'item2']); + } + + private function run(array $items) + { + } +} + +?> +----- +run(['item1', 'item2']); + } + + /** + * @param string[] $items + */ + private function run(array $items) + { + } +} + +?> diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Source/SafeParentClass.php b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Source/SafeParentClass.php new file mode 100644 index 00000000000..a0ca61c08fc --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Source/SafeParentClass.php @@ -0,0 +1,7 @@ +rule(ClassMethodArrayDocblockParamFromLocalCallsRector::class); +}; diff --git a/rules/TypeDeclaration/Rector/ClassMethod/AddMethodCallBasedStrictParamTypeRector.php b/rules/TypeDeclaration/Rector/ClassMethod/AddMethodCallBasedStrictParamTypeRector.php index b2c4af799ea..9844bf4c371 100644 --- a/rules/TypeDeclaration/Rector/ClassMethod/AddMethodCallBasedStrictParamTypeRector.php +++ b/rules/TypeDeclaration/Rector/ClassMethod/AddMethodCallBasedStrictParamTypeRector.php @@ -5,14 +5,13 @@ namespace Rector\TypeDeclaration\Rector\ClassMethod; use PhpParser\Node; -use PhpParser\Node\Name; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; -use Rector\Configuration\Parameter\FeatureFlags; use Rector\PhpParser\NodeFinder\LocalMethodCallFinder; use Rector\Rector\AbstractRector; use Rector\TypeDeclaration\NodeAnalyzer\CallTypesResolver; use Rector\TypeDeclaration\NodeAnalyzer\ClassMethodParamTypeCompleter; +use Rector\TypeDeclarationDocblocks\PrivateMethodFlagger; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -30,6 +29,7 @@ public function __construct( private readonly CallTypesResolver $callTypesResolver, private readonly ClassMethodParamTypeCompleter $classMethodParamTypeCompleter, private readonly LocalMethodCallFinder $localMethodCallFinder, + private readonly PrivateMethodFlagger $privateMethodFlagger ) { } @@ -92,7 +92,7 @@ public function refactor(Node $node): ?Node continue; } - if (! $this->isClassMethodPrivate($node, $classMethod)) { + if (! $this->privateMethodFlagger->isClassMethodPrivate($node, $classMethod)) { continue; } @@ -120,19 +120,4 @@ public function refactor(Node $node): ?Node return null; } - - private function isClassMethodPrivate(Class_ $class, ClassMethod $classMethod): bool - { - if ($classMethod->isPrivate()) { - return true; - } - - if ($classMethod->isFinal() && ! $class->extends instanceof Name && $class->implements === []) { - return true; - } - - $isClassFinal = $class->isFinal() || FeatureFlags::treatClassesAsFinal($class); - - return $isClassFinal && ! $class->extends instanceof Name && $class->implements === [] && $classMethod->isProtected(); - } } diff --git a/rules/TypeDeclarationDocblocks/PrivateMethodFlagger.php b/rules/TypeDeclarationDocblocks/PrivateMethodFlagger.php new file mode 100644 index 00000000000..403d8ff9bc9 --- /dev/null +++ b/rules/TypeDeclarationDocblocks/PrivateMethodFlagger.php @@ -0,0 +1,28 @@ +isPrivate()) { + return true; + } + + if ($classMethod->isFinal() && ! $class->extends instanceof Name && $class->implements === []) { + return true; + } + + $isClassFinal = $class->isFinal() || FeatureFlags::treatClassesAsFinal($class); + + return $isClassFinal && ! $class->extends instanceof Name && $class->implements === [] && $classMethod->isProtected(); + } +} diff --git a/rules/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector.php b/rules/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector.php new file mode 100644 index 00000000000..db8e1759916 --- /dev/null +++ b/rules/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector.php @@ -0,0 +1,132 @@ +run(['item1', 'item2']); + } + + private function run(array $items) + { + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +class SomeClass +{ + public function go() + { + $this->run(['item1', 'item2']); + } + + /** + * @param string[] $items + */ + private function run(array $items) + { + } +} +CODE_SAMPLE + ), + + ]); + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + $hasChanged = false; + + foreach ($node->getMethods() as $classMethod) { + if ($classMethod->getParams() === []) { + continue; + } + + $classMethodPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($classMethod); + + $methodCalls = $this->localMethodCallFinder->match($node, $classMethod); + $classMethodParameterTypes = $this->callTypesResolver->resolveStrictTypesFromCalls($methodCalls); + + foreach ($classMethod->getParams() as $parameterPosition => $param) { + $parameterName = $this->getName($param); + $parameterTagValueNode = $classMethodPhpDocInfo->getParamTagValueByName($parameterName); + + // already known, skip + if ($parameterTagValueNode instanceof ParamTagValueNode) { + continue; + } + + $resolvedParameterType = $classMethodParameterTypes[$parameterPosition] ?? null; + if (! $resolvedParameterType instanceof ArrayType) { + continue; + } + + $normalizedResolvedParameterType = $this->typeNormalizer->generalizeConstantBoolTypes( + $resolvedParameterType + ); + $arrayDocTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode( + $normalizedResolvedParameterType + ); + + $paramTagValueNode = new ParamTagValueNode($arrayDocTypeNode, false, '$' . $parameterName, '', false); + $classMethodPhpDocInfo->addTagValueNode($paramTagValueNode); + + $hasChanged = true; + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($classMethod); + } + } + + if (! $hasChanged) { + return null; + } + + return $node; + } +} From e2727e00b753b326b5f895a6d6ccc2adf551d751 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 12 Sep 2025 17:43:07 +0200 Subject: [PATCH 2/2] add various types --- .../multiple_calls_with_various_types.php.inc | 40 +++++++++++++++++++ .../Fixture/skip_non_array_param.php.inc | 15 +++++++ ...ArrayDocblockParamFromLocalCallsRector.php | 7 +++- 3 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/multiple_calls_with_various_types.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/skip_non_array_param.php.inc diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/multiple_calls_with_various_types.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/multiple_calls_with_various_types.php.inc new file mode 100644 index 00000000000..7515deb7f05 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/multiple_calls_with_various_types.php.inc @@ -0,0 +1,40 @@ +run(['item1', 'item2']); + $this->run([123, 456]); + } + + private function run(array $items) + { + } +} + +?> +----- +run(['item1', 'item2']); + $this->run([123, 456]); + } + + /** + * @param string[]|int[] $items + */ + private function run(array $items) + { + } +} + +?> diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/skip_non_array_param.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/skip_non_array_param.php.inc new file mode 100644 index 00000000000..faf245c5ccc --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector/Fixture/skip_non_array_param.php.inc @@ -0,0 +1,15 @@ +run(['item1', 'item2']); + } + + private function run($items) + { + } +} diff --git a/rules/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector.php b/rules/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector.php index db8e1759916..2aa5a921d28 100644 --- a/rules/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector.php +++ b/rules/TypeDeclarationDocblocks/Rector/Class_/ClassMethodArrayDocblockParamFromLocalCallsRector.php @@ -7,7 +7,6 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Class_; use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; -use PHPStan\Type\ArrayType; use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; use Rector\Comments\NodeDocBlock\DocBlockUpdater; use Rector\PhpParser\NodeFinder\LocalMethodCallFinder; @@ -95,6 +94,10 @@ public function refactor(Node $node): ?Node $classMethodParameterTypes = $this->callTypesResolver->resolveStrictTypesFromCalls($methodCalls); foreach ($classMethod->getParams() as $parameterPosition => $param) { + if ($param->type === null || ! $this->isName($param->type, 'array')) { + continue; + } + $parameterName = $this->getName($param); $parameterTagValueNode = $classMethodPhpDocInfo->getParamTagValueByName($parameterName); @@ -104,7 +107,7 @@ public function refactor(Node $node): ?Node } $resolvedParameterType = $classMethodParameterTypes[$parameterPosition] ?? null; - if (! $resolvedParameterType instanceof ArrayType) { + if (! $resolvedParameterType instanceof \PHPStan\Type\Type) { continue; }