diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector/AddReturnDocblockForScalarArrayFromAssignsRectorTest.php b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector/AddReturnDocblockForScalarArrayFromAssignsRectorTest.php new file mode 100644 index 00000000000..09544e4d0b7 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector/AddReturnDocblockForScalarArrayFromAssignsRectorTest.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/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector/Fixture/simple_array_assigns.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector/Fixture/simple_array_assigns.php.inc new file mode 100644 index 00000000000..2e1609081e2 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector/Fixture/simple_array_assigns.php.inc @@ -0,0 +1,139 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector/Fixture/skip_various_cases.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector/Fixture/skip_various_cases.php.inc new file mode 100644 index 00000000000..da9f8a9344c --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector/Fixture/skip_various_cases.php.inc @@ -0,0 +1,85 @@ +withRules([AddReturnDocblockForScalarArrayFromAssignsRector::class]); diff --git a/rules/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector.php b/rules/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector.php new file mode 100644 index 00000000000..35eb4803c8a --- /dev/null +++ b/rules/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector.php @@ -0,0 +1,263 @@ +> + */ + public function getNodeTypes(): array + { + return [ClassMethod::class, Function_::class]; + } + + /** + * @param ClassMethod|Function_ $node + */ + public function refactor(Node $node): ?Node + { + $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node); + $returnType = $phpDocInfo->getReturnType(); + + if (! $returnType instanceof MixedType || $returnType->isExplicitMixed()) { + return null; + } + + if ($node->returnType instanceof Node && ! $this->isName($node->returnType, 'array')) { + return null; + } + + $returnsScoped = $this->betterNodeFinder->findReturnsScoped($node); + + + if (! $this->returnAnalyzer->hasOnlyReturnWithExpr($node, $returnsScoped)) { + return null; + } + + $returnedVariableNames = $this->extractReturnedVariableNames($returnsScoped); + if ($returnedVariableNames === []) { + return null; + } + + $scalarArrayTypes = []; + foreach ($returnedVariableNames as $variableName) { + $scalarType = $this->resolveScalarArrayTypeForVariable($node, $variableName); + if ($scalarType instanceof Type) { + $scalarArrayTypes[] = $scalarType; + } else { + return null; + } + } + + $firstScalarType = $scalarArrayTypes[0]; + foreach ($scalarArrayTypes as $scalarArrayType) { + if (! $firstScalarType->equals($scalarArrayType)) { + return null; + } + } + + $arrayType = new ArrayType(new MixedType(), $firstScalarType); + + $hasChanged = $this->phpDocTypeChanger->changeReturnType($node, $phpDocInfo, $arrayType); + if ($hasChanged) { + return $node; + } + + return null; + } + + /** + * @param Return_[] $returnsScoped + * @return string[] + */ + private function extractReturnedVariableNames(array $returnsScoped): array + { + $variableNames = []; + + foreach ($returnsScoped as $returnScoped) { + if (! $returnScoped->expr instanceof Variable) { + continue; + } + + $variableName = $this->getName($returnScoped->expr); + if ($variableName !== null) { + $variableNames[] = $variableName; + } + } + + return array_unique($variableNames); + } + + private function resolveScalarArrayTypeForVariable(ClassMethod|Function_ $node, string $variableName): ?Type + { + $assigns = $this->betterNodeFinder->findInstancesOfScoped([$node], Assign::class); + + $scalarTypes = []; + $arrayHasInitialized = false; + $arrayHasDimAssigns = false; + + foreach ($assigns as $assign) { + if ($assign->var instanceof Variable && $this->isName($assign->var, $variableName)) { + if ($assign->expr instanceof Array_ && $assign->expr->items === []) { + $arrayHasInitialized = true; + continue; + } + } + + if (! $assign->var instanceof ArrayDimFetch) { + continue; + } + + /** @var ArrayDimFetch $arrayDimFetch */ + $arrayDimFetch = $assign->var; + if (! $arrayDimFetch->var instanceof Variable) { + continue; + } + + if (! $this->isName($arrayDimFetch->var, $variableName)) { + continue; + } + + if ($arrayDimFetch->dim !== null) { + continue; + } + + $arrayHasDimAssigns = true; + + $scalarType = $this->resolveScalarType($assign->expr); + if ($scalarType instanceof Type) { + $scalarTypes[] = $scalarType; + } else { + return null; + } + } + + if (! $arrayHasInitialized || ! $arrayHasDimAssigns) { + return null; + } + + if ($scalarTypes === []) { + return null; + } + + $firstType = $scalarTypes[0]; + foreach ($scalarTypes as $scalarType) { + if (! $firstType->equals($scalarType)) { + return null; + } + } + + return $firstType; + } + + private function resolveScalarType(Expr $expr): ?Type + { + if ($expr instanceof String_) { + return new StringType(); + } + + if ($expr instanceof Int_) { + return new IntegerType(); + } + + if ($expr instanceof DNumber) { + return new FloatType(); + } + + return null; + } +}