Skip to content

Commit a52f120

Browse files
authored
[tdd] Add AddReturnDocblockForDimFetchArrayFromAssignsRector (#7792)
1 parent 9116b3e commit a52f120

File tree

8 files changed

+394
-0
lines changed

8 files changed

+394
-0
lines changed

phpstan.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,3 +464,8 @@ parameters:
464464
-
465465
path: src/PhpParser/Node/CustomNode/FileWithoutNamespace.php
466466
identifier: symplify.forbiddenExtendOfNonAbstractClass
467+
468+
# false positive
469+
-
470+
identifier: phpstanApi.varTagAssumption
471+
path: rules/TypeDeclarationDocblocks/Rector/ClassMethod/AddReturnDocblockForDimFetchArrayFromAssignsRector.php
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForDimFetchArrayFromAssignsRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class AddReturnDocblockForDimFetchArrayFromAssignsRectorTest extends AbstractRectorTestCase
12+
{
13+
#[DataProvider('provideData')]
14+
public function test(string $filePath): void
15+
{
16+
$this->doTestFile($filePath);
17+
}
18+
19+
public static function provideData(): Iterator
20+
{
21+
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
22+
}
23+
24+
public function provideConfigFilePath(): string
25+
{
26+
return __DIR__ . '/config/configured_rule.php';
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace Rector\Tests\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForDimFetchArrayFromAssignsRector\Fixture;
4+
5+
final class ConditionalAssign
6+
{
7+
public function toArray(): array
8+
{
9+
$items = [];
10+
11+
if (mt_rand(0, 1)) {
12+
$items['key'] = 100;
13+
}
14+
15+
return $items;
16+
}
17+
}
18+
19+
?>
20+
-----
21+
<?php
22+
23+
namespace Rector\Tests\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForDimFetchArrayFromAssignsRector\Fixture;
24+
25+
final class ConditionalAssign
26+
{
27+
/**
28+
* @return array<string, int>
29+
*/
30+
public function toArray(): array
31+
{
32+
$items = [];
33+
34+
if (mt_rand(0, 1)) {
35+
$items['key'] = 100;
36+
}
37+
38+
return $items;
39+
}
40+
}
41+
42+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace Rector\Tests\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForDimFetchArrayFromAssignsRector\Fixture;
4+
5+
final class MultipleConditionalAssign
6+
{
7+
public function toArray(): array
8+
{
9+
$items = [];
10+
11+
if (mt_rand(0, 1)) {
12+
$items['key'] = 'some_string';
13+
}
14+
15+
if (mt_rand(0, 1)) {
16+
$items['another_key'] = 500;
17+
}
18+
19+
return $items;
20+
}
21+
}
22+
23+
?>
24+
-----
25+
<?php
26+
27+
namespace Rector\Tests\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForDimFetchArrayFromAssignsRector\Fixture;
28+
29+
final class MultipleConditionalAssign
30+
{
31+
/**
32+
* @return array<string, string|int>
33+
*/
34+
public function toArray(): array
35+
{
36+
$items = [];
37+
38+
if (mt_rand(0, 1)) {
39+
$items['key'] = 'some_string';
40+
}
41+
42+
if (mt_rand(0, 1)) {
43+
$items['another_key'] = 500;
44+
}
45+
46+
return $items;
47+
}
48+
}
49+
50+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Rector\Tests\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForDimFetchArrayFromAssignsRector\Fixture;
4+
5+
final class SomeItem
6+
{
7+
public function toArray(): array
8+
{
9+
$items = [];
10+
$items['key'] = 100;
11+
12+
return $items;
13+
}
14+
}
15+
16+
?>
17+
-----
18+
<?php
19+
20+
namespace Rector\Tests\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForDimFetchArrayFromAssignsRector\Fixture;
21+
22+
final class SomeItem
23+
{
24+
/**
25+
* @return array<string, int>
26+
*/
27+
public function toArray(): array
28+
{
29+
$items = [];
30+
$items['key'] = 100;
31+
32+
return $items;
33+
}
34+
}
35+
36+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForDimFetchArrayFromAssignsRector;
7+
8+
return RectorConfig::configure()
9+
->withRules([AddReturnDocblockForDimFetchArrayFromAssignsRector::class]);
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\TypeDeclarationDocblocks\Rector\ClassMethod;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\Array_;
9+
use PhpParser\Node\Expr\Assign;
10+
use PhpParser\Node\Expr\Variable;
11+
use PhpParser\Node\Stmt\ClassMethod;
12+
use PhpParser\Node\Stmt\Expression;
13+
use PhpParser\Node\Stmt\Return_;
14+
use PHPStan\Type\Constant\ConstantArrayType;
15+
use PHPStan\Type\Type;
16+
use PHPStan\Type\UnionType;
17+
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
18+
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger;
19+
use Rector\Rector\AbstractRector;
20+
use Rector\TypeDeclarationDocblocks\NodeFinder\ReturnNodeFinder;
21+
use Rector\TypeDeclarationDocblocks\TagNodeAnalyzer\UsefulArrayTagNodeAnalyzer;
22+
use Rector\TypeDeclarationDocblocks\TypeResolver\ConstantArrayTypeGeneralizer;
23+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
24+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
25+
26+
/**
27+
* @see \Rector\Tests\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForDimFetchArrayFromAssignsRector\AddReturnDocblockForDimFetchArrayFromAssignsRectorTest
28+
*/
29+
final class AddReturnDocblockForDimFetchArrayFromAssignsRector extends AbstractRector
30+
{
31+
public function __construct(
32+
private readonly PhpDocInfoFactory $phpDocInfoFactory,
33+
private readonly UsefulArrayTagNodeAnalyzer $usefulArrayTagNodeAnalyzer,
34+
private readonly ReturnNodeFinder $returnNodeFinder,
35+
private readonly ConstantArrayTypeGeneralizer $constantArrayTypeGeneralizer,
36+
private readonly PhpDocTypeChanger $phpDocTypeChanger,
37+
) {
38+
}
39+
40+
public function getRuleDefinition(): RuleDefinition
41+
{
42+
return new RuleDefinition(
43+
'Add @return docblock for methods returning array from dim fetch of assigned arrays',
44+
[
45+
new CodeSample(
46+
<<<'CODE_SAMPLE'
47+
final class SomeClass
48+
{
49+
public function toArray(): array
50+
{
51+
$items = [];
52+
53+
if (mt_rand(0, 1)) {
54+
$items['key'] = 'value';
55+
}
56+
57+
if (mt_rand(0, 1)) {
58+
$items['another_key'] = 'another_value';
59+
}
60+
61+
return $items;
62+
}
63+
}
64+
CODE_SAMPLE
65+
,
66+
<<<'CODE_SAMPLE'
67+
final class SomeClass
68+
{
69+
/**
70+
* @return array<string, string>
71+
*/
72+
public function toArray()
73+
{
74+
$items = [];
75+
76+
if (mt_rand(0, 1)) {
77+
$items['key'] = 'value';
78+
}
79+
80+
if (mt_rand(0, 1)) {
81+
$items['another_key'] = 'another_value';
82+
}
83+
84+
return $items;
85+
}
86+
}
87+
CODE_SAMPLE
88+
),
89+
90+
]
91+
);
92+
}
93+
94+
public function getNodeTypes(): array
95+
{
96+
return [ClassMethod::class];
97+
}
98+
99+
/**
100+
* @param ClassMethod $node
101+
*/
102+
public function refactor(Node $node): ?ClassMethod
103+
{
104+
if ($node->stmts === null) {
105+
return null;
106+
}
107+
108+
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node);
109+
110+
if ($this->usefulArrayTagNodeAnalyzer->isUsefulArrayTag($phpDocInfo->getReturnTagValue())) {
111+
return null;
112+
}
113+
114+
$soleReturn = $this->returnNodeFinder->findOnlyReturnWithExpr($node);
115+
if (! $soleReturn instanceof Return_) {
116+
return null;
117+
}
118+
119+
// only variable
120+
if (! $soleReturn->expr instanceof Variable) {
121+
return null;
122+
}
123+
124+
// @todo check type here
125+
$returnedExprType = $this->getType($soleReturn->expr);
126+
127+
if (! $this->isConstantArrayType($returnedExprType)) {
128+
return null;
129+
}
130+
131+
// find stmts with $item = [];
132+
$returnedVariableName = $this->getName($soleReturn->expr);
133+
if (! is_string($returnedVariableName)) {
134+
return null;
135+
}
136+
137+
if (! $this->isVariableInstantiated($node, $returnedVariableName)) {
138+
return null;
139+
}
140+
141+
if ($returnedExprType->getReferencedClasses() !== []) {
142+
// better handled by shared-interface/class rule, to avoid turning objects to mixed
143+
return null;
144+
}
145+
146+
// conditional assign
147+
$genericUnionedTypeNodes = [];
148+
149+
if ($returnedExprType instanceof UnionType) {
150+
foreach ($returnedExprType->getTypes() as $unionedType) {
151+
if ($unionedType instanceof ConstantArrayType) {
152+
// skip empty array
153+
if ($unionedType->getKeyTypes() === [] && $unionedType->getValueTypes() === []) {
154+
continue;
155+
}
156+
157+
$genericUnionedTypeNode = $this->constantArrayTypeGeneralizer->generalize($unionedType);
158+
$genericUnionedTypeNodes[] = $genericUnionedTypeNode;
159+
}
160+
}
161+
} else {
162+
/** @var ConstantArrayType $returnedExprType */
163+
$genericTypeNode = $this->constantArrayTypeGeneralizer->generalize($returnedExprType);
164+
$this->phpDocTypeChanger->changeReturnTypeNode($node, $phpDocInfo, $genericTypeNode);
165+
166+
return $node;
167+
}
168+
169+
// @todo handle multiple type nodes
170+
$this->phpDocTypeChanger->changeReturnTypeNode($node, $phpDocInfo, $genericUnionedTypeNodes[0]);
171+
172+
return $node;
173+
}
174+
175+
private function isVariableInstantiated(ClassMethod $classMethod, string $returnedVariableName): bool
176+
{
177+
foreach ((array) $classMethod->stmts as $stmt) {
178+
if (! $stmt instanceof Expression) {
179+
continue;
180+
}
181+
182+
if (! $stmt->expr instanceof Assign) {
183+
continue;
184+
}
185+
186+
$assign = $stmt->expr;
187+
if (! $assign->var instanceof Variable) {
188+
continue;
189+
}
190+
191+
if (! $this->isName($assign->var, $returnedVariableName)) {
192+
continue;
193+
}
194+
195+
// must be array assignment
196+
if (! $assign->expr instanceof Array_) {
197+
continue;
198+
}
199+
200+
return true;
201+
}
202+
203+
return false;
204+
}
205+
206+
private function isConstantArrayType(Type $returnedExprType): bool
207+
{
208+
if ($returnedExprType instanceof UnionType) {
209+
foreach ($returnedExprType->getTypes() as $unionedType) {
210+
if (! $unionedType instanceof ConstantArrayType) {
211+
return false;
212+
}
213+
}
214+
215+
return true;
216+
}
217+
218+
return $returnedExprType instanceof ConstantArrayType;
219+
}
220+
}

0 commit comments

Comments
 (0)