From 3d29ac0e8c11d8b74a75f8915b9f1f224fb335a9 Mon Sep 17 00:00:00 2001 From: Liam Hammett Date: Sun, 17 Aug 2025 21:42:02 +0100 Subject: [PATCH 1/3] Add a more specific return docblock for scalar lists when we are certain of the types inside the array (eg. when they were initialised and returned within the same function) --- ...ockForScalarArrayFromAssignsRectorTest.php | 28 ++ .../Fixture/simple_array_assigns.php.inc | 139 +++++++++ .../Fixture/skip_various_cases.php.inc | 85 ++++++ .../config/configured_rule.php | 9 + ...ocblockForScalarArrayFromAssignsRector.php | 269 ++++++++++++++++++ 5 files changed, 530 insertions(+) create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector/AddReturnDocblockForScalarArrayFromAssignsRectorTest.php create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector/Fixture/simple_array_assigns.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector/Fixture/skip_various_cases.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector/config/configured_rule.php create mode 100644 rules/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector.php 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..f3c241669ff --- /dev/null +++ b/rules/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector.php @@ -0,0 +1,269 @@ +> + */ + 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 ($returnsScoped === []) { + return null; + } + + 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; + } + } + + if ($scalarArrayTypes === []) { + 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(Node $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; + } +} From d503e1e13c11163594fe28d21ea884808e67792c Mon Sep 17 00:00:00 2001 From: Liam Hammett Date: Mon, 18 Aug 2025 00:49:30 +0100 Subject: [PATCH 2/3] Fixes for PHPStan --- ...ocblockForScalarArrayFromAssignsRector.php | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/rules/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector.php b/rules/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector.php index f3c241669ff..6dbbebab13e 100644 --- a/rules/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector.php +++ b/rules/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector.php @@ -5,29 +5,30 @@ namespace Rector\TypeDeclaration\Rector\ClassMethod; use PhpParser\Node; +use PHPStan\Type\Type; +use PhpParser\Node\Expr; +use PHPStan\Type\ArrayType; +use PHPStan\Type\FloatType; +use PHPStan\Type\MixedType; +use PHPStan\Type\StringType; +use PHPStan\Type\IntegerType; use PhpParser\Node\Expr\Array_; -use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\Assign; +use PhpParser\Node\Scalar\Int_; +use PhpParser\Node\Stmt\Return_; use PhpParser\Node\Expr\Variable; +use Rector\Rector\AbstractRector; use PhpParser\Node\Scalar\DNumber; -use PhpParser\Node\Scalar\Int_; use PhpParser\Node\Scalar\String_; -use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Function_; -use PhpParser\Node\Stmt\Return_; -use PHPStan\Type\ArrayType; -use PHPStan\Type\FloatType; -use PHPStan\Type\IntegerType; -use PHPStan\Type\MixedType; -use PHPStan\Type\StringType; -use PHPStan\Type\Type; -use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; -use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger; +use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\Node\Expr\ArrayDimFetch; use Rector\PhpParser\Node\BetterNodeFinder; -use Rector\Rector\AbstractRector; use Rector\TypeDeclaration\NodeAnalyzer\ReturnAnalyzer; -use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; +use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; +use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; +use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger; /** * @see \Rector\Tests\TypeDeclaration\Rector\ClassMethod\AddReturnDocblockForScalarArrayFromAssignsRector\AddReturnDocblockForScalarArrayFromAssignsRectorTest @@ -145,10 +146,6 @@ public function refactor(Node $node): ?Node } } - if ($scalarArrayTypes === []) { - return null; - } - $firstScalarType = $scalarArrayTypes[0]; foreach ($scalarArrayTypes as $scalarArrayType) { if (! $firstScalarType->equals($scalarArrayType)) { @@ -250,7 +247,7 @@ private function resolveScalarArrayTypeForVariable(ClassMethod|Function_ $node, return $firstType; } - private function resolveScalarType(Node $expr): ?Type + private function resolveScalarType(Expr $expr): ?Type { if ($expr instanceof String_) { return new StringType(); From 0ba411d20f601f003dfb555358e9b1c3f3dda94d Mon Sep 17 00:00:00 2001 From: Liam Hammett Date: Mon, 18 Aug 2025 12:30:34 +0200 Subject: [PATCH 3/3] Update rules/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector.php Co-authored-by: Abdul Malik Ikhsan --- .../AddReturnDocblockForScalarArrayFromAssignsRector.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/rules/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector.php b/rules/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector.php index 6dbbebab13e..35eb4803c8a 100644 --- a/rules/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector.php +++ b/rules/TypeDeclaration/Rector/ClassMethod/AddReturnDocblockForScalarArrayFromAssignsRector.php @@ -123,9 +123,6 @@ public function refactor(Node $node): ?Node $returnsScoped = $this->betterNodeFinder->findReturnsScoped($node); - if ($returnsScoped === []) { - return null; - } if (! $this->returnAnalyzer->hasOnlyReturnWithExpr($node, $returnsScoped)) { return null;