diff --git a/config/set/type-declaration-docblocks.php b/config/set/type-declaration-docblocks.php new file mode 100644 index 00000000000..ed14898e71d --- /dev/null +++ b/config/set/type-declaration-docblocks.php @@ -0,0 +1,17 @@ +rules([ + DocblockVarFromParamDocblockInConstructorRector::class, + DocblockGetterReturnArrayFromPropertyDocblockVarRector::class, + ]); +}; diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/DocblockGetterReturnArrayFromPropertyDocblockVarRector/DocblockGetterReturnArrayFromPropertyDocblockVarRectorTest.php b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/DocblockGetterReturnArrayFromPropertyDocblockVarRector/DocblockGetterReturnArrayFromPropertyDocblockVarRectorTest.php new file mode 100644 index 00000000000..84bc7fbeb43 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/DocblockGetterReturnArrayFromPropertyDocblockVarRector/DocblockGetterReturnArrayFromPropertyDocblockVarRectorTest.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/ClassMethod/DocblockGetterReturnArrayFromPropertyDocblockVarRector/Fixture/skip_missing_return_type.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/DocblockGetterReturnArrayFromPropertyDocblockVarRector/Fixture/skip_missing_return_type.php.inc new file mode 100644 index 00000000000..12328162b48 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/DocblockGetterReturnArrayFromPropertyDocblockVarRector/Fixture/skip_missing_return_type.php.inc @@ -0,0 +1,16 @@ +names; + } +} diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/DocblockGetterReturnArrayFromPropertyDocblockVarRector/Fixture/some_property_with_array_docblock.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/DocblockGetterReturnArrayFromPropertyDocblockVarRector/Fixture/some_property_with_array_docblock.php.inc new file mode 100644 index 00000000000..8fdccb7bd3a --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/DocblockGetterReturnArrayFromPropertyDocblockVarRector/Fixture/some_property_with_array_docblock.php.inc @@ -0,0 +1,40 @@ +names; + } +} + +?> +----- +names; + } +} + +?> diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/DocblockGetterReturnArrayFromPropertyDocblockVarRector/config/configured_rule.php b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/DocblockGetterReturnArrayFromPropertyDocblockVarRector/config/configured_rule.php new file mode 100644 index 00000000000..b852f0fdb07 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/ClassMethod/DocblockGetterReturnArrayFromPropertyDocblockVarRector/config/configured_rule.php @@ -0,0 +1,9 @@ +withRules([DocblockGetterReturnArrayFromPropertyDocblockVarRector::class]); diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarFromParamDocblockInConstructorRector/DocblockVarFromParamDocblockInConstructorRectorTest.php b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarFromParamDocblockInConstructorRector/DocblockVarFromParamDocblockInConstructorRectorTest.php new file mode 100644 index 00000000000..85f501cba87 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarFromParamDocblockInConstructorRector/DocblockVarFromParamDocblockInConstructorRectorTest.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_/DocblockVarFromParamDocblockInConstructorRector/Fixture/skip_if_array_type_missing.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarFromParamDocblockInConstructorRector/Fixture/skip_if_array_type_missing.php.inc new file mode 100644 index 00000000000..037e1a0f85e --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarFromParamDocblockInConstructorRector/Fixture/skip_if_array_type_missing.php.inc @@ -0,0 +1,16 @@ +names = $names; + } +} diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarFromParamDocblockInConstructorRector/Fixture/some_class.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarFromParamDocblockInConstructorRector/Fixture/some_class.php.inc new file mode 100644 index 00000000000..d83aae40a4d --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarFromParamDocblockInConstructorRector/Fixture/some_class.php.inc @@ -0,0 +1,40 @@ +names = $names; + } +} + +?> +----- +names = $names; + } +} + +?> diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarFromParamDocblockInConstructorRector/config/configured_rule.php b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarFromParamDocblockInConstructorRector/config/configured_rule.php new file mode 100644 index 00000000000..65f0f0af931 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Class_/DocblockVarFromParamDocblockInConstructorRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(DocblockVarFromParamDocblockInConstructorRector::class); +}; diff --git a/rules/TypeDeclarationDocblocks/NodeAnalyzer/ConstructorAssignedTypeResolver.php b/rules/TypeDeclarationDocblocks/NodeAnalyzer/ConstructorAssignedTypeResolver.php new file mode 100644 index 00000000000..913af1ba7a7 --- /dev/null +++ b/rules/TypeDeclarationDocblocks/NodeAnalyzer/ConstructorAssignedTypeResolver.php @@ -0,0 +1,57 @@ +getMethod(MethodName::CONSTRUCT); + if (! $constructorClassMethod instanceof ClassMethod) { + return null; + } + + if ($constructorClassMethod->stmts === null) { + return null; + } + + $assigns = $this->betterNodeFinder->findInstancesOfScoped($constructorClassMethod->stmts, Assign::class); + foreach ($assigns as $assign) { + if (! $assign->var instanceof PropertyFetch) { + continue; + } + + $propertyFetch = $assign->var; + if (! $this->nodeNameResolver-> isName($propertyFetch->var, 'this')) { + continue; + } + + if (! $this->nodeNameResolver-> isName($propertyFetch->name, $propertyName)) { + continue; + } + + return $this->nodeTypeResolver->getType($assign->expr); + } + + return null; + } +} diff --git a/rules/TypeDeclarationDocblocks/Rector/ClassMethod/DocblockGetterReturnArrayFromPropertyDocblockVarRector.php b/rules/TypeDeclarationDocblocks/Rector/ClassMethod/DocblockGetterReturnArrayFromPropertyDocblockVarRector.php new file mode 100644 index 00000000000..b8400fd6b56 --- /dev/null +++ b/rules/TypeDeclarationDocblocks/Rector/ClassMethod/DocblockGetterReturnArrayFromPropertyDocblockVarRector.php @@ -0,0 +1,135 @@ +items; + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +class SomeClass +{ + /** + * @var int[] + */ + private array $items; + + /** + * @return int[] + */ + public function getItems(): array + { + return $this->items; + } +} +CODE_SAMPLE + ), + ]); + } + + /** + * @param ClassMethod $node + */ + public function refactor(Node $node): ?Node + { + if (! $node->returnType instanceof Node) { + return null; + } + + if (! $this->isName($node->returnType, 'array')) { + return null; + } + + $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); + + // return tag is already given + if ($phpDocInfo->getReturnTagValue() instanceof ReturnTagValueNode) { + return null; + } + + $propertyFetch = $this->matchReturnLocalPropertyFetch($node); + if (! $propertyFetch instanceof PropertyFetch) { + return null; + } + + $propertyFetchType = $this->getType($propertyFetch); + $propertyFetchDocTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($propertyFetchType); + + $returnTagValueNode = new ReturnTagValueNode($propertyFetchDocTypeNode, ''); + $phpDocInfo->addTagValueNode($returnTagValueNode); + + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); + + return $node; + } + + private function matchReturnLocalPropertyFetch(ClassMethod $classMethod): ?PropertyFetch + { + // we need exactly one statement of return + if ($classMethod->stmts === null || count($classMethod->stmts) !== 1) { + return null; + } + + $onlyStmt = $classMethod->stmts[0]; + if (! $onlyStmt instanceof Return_) { + return null; + } + + if (! $onlyStmt->expr instanceof PropertyFetch) { + return null; + } + + $propertyFetch = $onlyStmt->expr; + if (! $this->isName($propertyFetch->var, 'this')) { + return null; + } + + return $propertyFetch; + } +} diff --git a/rules/TypeDeclarationDocblocks/Rector/Class_/DocblockVarFromParamDocblockInConstructorRector.php b/rules/TypeDeclarationDocblocks/Rector/Class_/DocblockVarFromParamDocblockInConstructorRector.php new file mode 100644 index 00000000000..ddf0ce7300c --- /dev/null +++ b/rules/TypeDeclarationDocblocks/Rector/Class_/DocblockVarFromParamDocblockInConstructorRector.php @@ -0,0 +1,137 @@ +items = $items; + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +class SomeClass +{ + /** + * @var string[] + */ + private array $items; + + /** + * @param string[] $items + */ + public function __construct(array $items) + { + $this->items = $items; + } +} +CODE_SAMPLE + ), + + ]); + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + $constructorClassMethod = $node->getMethod(MethodName::CONSTRUCT); + if (! $constructorClassMethod instanceof ClassMethod) { + return null; + } + + $hasChanged = false; + + foreach ($node->getProperties() as $property) { + if (! $this->isArrayTypedProperty($property)) { + continue; + } + + $propertyPhpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($property); + + // @var tag already given + if ($propertyPhpDocInfo->getVarTagValueNode() instanceof VarTagValueNode) { + continue; + } + + $propertyName = $this->getName($property); + + $assignedType = $this->constructorAssignedTypeResolver->resolve($node, $propertyName); + if (! $assignedType instanceof ArrayType) { + continue; + } + + $arrayDocTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPHPStanPhpDocTypeNode($assignedType); + + $returnTagValueNode = new VarTagValueNode($arrayDocTypeNode, '', ''); + $propertyPhpDocInfo->addTagValueNode($returnTagValueNode); + + $hasChanged = true; + + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($property); + } + + if (! $hasChanged) { + return null; + } + + return $node; + } + + private function isArrayTypedProperty(Property $property): bool + { + if (! $property->type instanceof Node) { + return false; + } + + return $this->isName($property->type, 'array'); + } +} diff --git a/src/Set/ValueObject/SetList.php b/src/Set/ValueObject/SetList.php index b6015410557..d7e17163802 100644 --- a/src/Set/ValueObject/SetList.php +++ b/src/Set/ValueObject/SetList.php @@ -142,6 +142,11 @@ final class SetList */ public const TYPE_DECLARATION = __DIR__ . '/../../../config/set/type-declaration.php'; + /** + * @var string + */ + public const TYPE_DECLARATION_DOCBLOCKS = __DIR__ . '/../../../config/set/type-declaration-docblocks.php'; + /** * @var string */