Skip to content

Commit 89d41aa

Browse files
committed
[type-declaration-docblocks] Add AddReturnDocblockForJsonArrayRector
1 parent 5781d97 commit 89d41aa

10 files changed

Lines changed: 320 additions & 1 deletion

File tree

config/set/type-declaration-docblocks.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddParamArrayDocblockFromDimFetchAccessRector;
1515
use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForArrayDimAssignedObjectRector;
1616
use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForCommonObjectDenominatorRector;
17+
use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForJsonArrayRector;
1718
use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\DocblockGetterReturnArrayFromPropertyDocblockVarRector;
1819
use Rector\TypeDeclarationDocblocks\Rector\ClassMethod\DocblockReturnArrayFromDirectArrayInstanceRector;
1920

@@ -38,6 +39,7 @@
3839
AddReturnDocblockForScalarArrayFromAssignsRector::class,
3940
DocblockReturnArrayFromDirectArrayInstanceRector::class,
4041
AddReturnDocblockForArrayDimAssignedObjectRector::class,
42+
AddReturnDocblockForJsonArrayRector::class,
4143

4244
// tests
4345
AddParamArrayDocblockFromDataProviderRector::class,
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\AddReturnDocblockForJsonArrayRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class AddReturnDocblockForJsonArrayRectorTest 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+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForJsonArrayRector\Fixture;
6+
7+
use Nette\Utils\Json;
8+
9+
final class JsonUtils
10+
{
11+
public function provide(string $contents): array
12+
{
13+
return Json::decode($contents, true);
14+
}
15+
}
16+
17+
?>
18+
-----
19+
<?php
20+
21+
declare(strict_types=1);
22+
23+
namespace Rector\Tests\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForJsonArrayRector\Fixture;
24+
25+
use Nette\Utils\Json;
26+
27+
final class JsonUtils
28+
{
29+
/**
30+
* @return array<string, mixed>
31+
*/
32+
public function provide(string $contents): array
33+
{
34+
return Json::decode($contents, true);
35+
}
36+
}
37+
38+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForJsonArrayRector\Fixture;
6+
7+
final class SkipAlreadyFilled
8+
{
9+
/**
10+
* @return array<string, array<string, mixed>>
11+
*/
12+
public function provide(string $contents): array
13+
{
14+
return json_decode($contents, true);
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForJsonArrayRector\Fixture;
6+
7+
final class SkipMissingArg
8+
{
9+
public function provide(string $contents): array
10+
{
11+
return json_decode($contents, false);
12+
}
13+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForJsonArrayRector\Fixture;
6+
7+
final class SomeClass
8+
{
9+
public function provide(string $contents): array
10+
{
11+
return json_decode($contents, true);
12+
}
13+
}
14+
15+
?>
16+
-----
17+
<?php
18+
19+
declare(strict_types=1);
20+
21+
namespace Rector\Tests\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForJsonArrayRector\Fixture;
22+
23+
final class SomeClass
24+
{
25+
/**
26+
* @return array<string, mixed>
27+
*/
28+
public function provide(string $contents): array
29+
{
30+
return json_decode($contents, true);
31+
}
32+
}
33+
34+
?>
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\AddReturnDocblockForJsonArrayRector;
7+
8+
return RectorConfig::configure()
9+
->withRules([AddReturnDocblockForJsonArrayRector::class]);

rules/NetteUtils/Rector/StaticCall/UtilsJsonStaticCallNamedArgRector.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PhpParser\Node\Expr\StaticCall;
1010
use PhpParser\Node\Identifier;
1111
use Rector\Rector\AbstractRector;
12+
use Rector\TypeDeclarationDocblocks\Enum\NetteClassName;
1213
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
1314
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
1415

@@ -49,7 +50,7 @@ public function getNodeTypes(): array
4950
*/
5051
public function refactor(Node $node): ?Node
5152
{
52-
if (! $this->isName($node->class, 'Nette\Utils\Json')) {
53+
if (! $this->isName($node->class, NetteClassName::JSON)) {
5354
return null;
5455
}
5556

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\TypeDeclarationDocblocks\Enum;
6+
7+
final class NetteClassName
8+
{
9+
/**
10+
* @var string
11+
*/
12+
public const JSON = 'Nette\Utils\Json';
13+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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;
9+
use PhpParser\Node\Expr\FuncCall;
10+
use PhpParser\Node\Expr\StaticCall;
11+
use PhpParser\Node\Stmt\ClassMethod;
12+
use PhpParser\Node\Stmt\Function_;
13+
use PhpParser\Node\Stmt\Return_;
14+
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
15+
use PHPStan\Type\ArrayType;
16+
use PHPStan\Type\MixedType;
17+
use PHPStan\Type\StringType;
18+
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
19+
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger;
20+
use Rector\PhpParser\Node\Value\ValueResolver;
21+
use Rector\Rector\AbstractRector;
22+
use Rector\TypeDeclarationDocblocks\Enum\NetteClassName;
23+
use Rector\TypeDeclarationDocblocks\NodeFinder\ReturnNodeFinder;
24+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
25+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
26+
27+
/**
28+
* @see \Rector\Tests\TypeDeclarationDocblocks\Rector\ClassMethod\AddReturnDocblockForJsonArrayRector\AddReturnDocblockForJsonArrayRectorTest
29+
*/
30+
final class AddReturnDocblockForJsonArrayRector extends AbstractRector
31+
{
32+
public function __construct(
33+
private readonly PhpDocInfoFactory $phpDocInfoFactory,
34+
private readonly ReturnNodeFinder $returnNodeFinder,
35+
private readonly PhpDocTypeChanger $phpDocTypeChanger,
36+
private readonly ValueResolver $valueResolver
37+
) {
38+
}
39+
40+
public function getRuleDefinition(): RuleDefinition
41+
{
42+
return new RuleDefinition(
43+
'Add @return docblock for array based on return of json_decode() return array',
44+
[
45+
new CodeSample(
46+
<<<'CODE_SAMPLE'
47+
final class SomeClass
48+
{
49+
public function provide(string $contents): array
50+
{
51+
return json_decode($contents, true);
52+
}
53+
}
54+
CODE_SAMPLE
55+
,
56+
<<<'CODE_SAMPLE'
57+
final class SomeClass
58+
{
59+
/**
60+
* @return array<string, mixed>
61+
*/
62+
public function provide(string $contents): array
63+
{
64+
return json_decode($contents, true);
65+
}
66+
}
67+
CODE_SAMPLE
68+
),
69+
]
70+
);
71+
}
72+
73+
/**
74+
* @return array<class-string<Node>>
75+
*/
76+
public function getNodeTypes(): array
77+
{
78+
return [ClassMethod::class, Function_::class];
79+
}
80+
81+
/**
82+
* @param ClassMethod|Function_ $node
83+
*/
84+
public function refactor(Node $node): ?Node
85+
{
86+
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node);
87+
$returnType = $phpDocInfo->getReturnType();
88+
89+
if (! $returnType instanceof MixedType || $returnType->isExplicitMixed()) {
90+
return null;
91+
}
92+
93+
// definitely not an array return
94+
if ($node->returnType instanceof Node && ! $this->isName($node->returnType, 'array')) {
95+
return null;
96+
}
97+
98+
$onlyReturnWithExpr = $this->returnNodeFinder->findOnlyReturnWithExpr($node);
99+
if (! $onlyReturnWithExpr instanceof Return_) {
100+
return null;
101+
}
102+
103+
$returnedExpr = $onlyReturnWithExpr->expr;
104+
if (! $returnedExpr instanceof Expr) {
105+
return null;
106+
}
107+
108+
if (! $this->isJsonDecodeToArray($returnedExpr)) {
109+
return null;
110+
}
111+
112+
$classMethodDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node);
113+
// already filled
114+
if ($classMethodDocInfo->getReturnTagValue() instanceof ReturnTagValueNode) {
115+
return null;
116+
}
117+
118+
$hasChanged = $this->phpDocTypeChanger->changeReturnType(
119+
$node,
120+
$phpDocInfo,
121+
new ArrayType(new StringType(), new MixedType())
122+
);
123+
124+
if (! $hasChanged) {
125+
return null;
126+
}
127+
128+
return $node;
129+
}
130+
131+
private function isJsonDecodeToArray(Expr $expr): bool
132+
{
133+
if ($expr instanceof FuncCall) {
134+
if (! $this->isName($expr, 'json_decode')) {
135+
return false;
136+
}
137+
138+
if (count($expr->getArgs()) !== 2) {
139+
return false;
140+
}
141+
142+
$secondArg = $expr->getArgs()[1];
143+
return $this->valueResolver->isTrue($secondArg->value);
144+
}
145+
146+
if ($expr instanceof StaticCall) {
147+
if (! $this->isName($expr->class, NetteClassName::JSON)) {
148+
return false;
149+
}
150+
151+
if (! $this->isName($expr->name, 'decode')) {
152+
return false;
153+
}
154+
155+
if (count($expr->getArgs()) !== 2) {
156+
return false;
157+
}
158+
159+
$secondArg = $expr->getArgs()[1];
160+
return $this->valueResolver->isTrue($secondArg->value);
161+
}
162+
163+
return false;
164+
}
165+
}

0 commit comments

Comments
 (0)