Skip to content

Commit a1a0e41

Browse files
authored
[type-coverage] Improve AddArrowFunctionParamArrayWhereDimFetchRector to handle multiple functions (#7112)
1 parent ab5fdc5 commit a1a0e41

File tree

4 files changed

+172
-27
lines changed

4 files changed

+172
-27
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Rector\Tests\TypeDeclaration\Rector\FuncCall\AddArrowFunctionParamArrayWhereDimFetchRector\Fixture;
4+
5+
final class HandleUsort
6+
{
7+
public function run(array $items)
8+
{
9+
usort(
10+
$items,
11+
fn ($a, $b): int => $a['key'] <=> $b['key']
12+
);
13+
}
14+
}
15+
16+
?>
17+
-----
18+
<?php
19+
20+
namespace Rector\Tests\TypeDeclaration\Rector\FuncCall\AddArrowFunctionParamArrayWhereDimFetchRector\Fixture;
21+
22+
final class HandleUsort
23+
{
24+
public function run(array $items)
25+
{
26+
usort(
27+
$items,
28+
fn (array $a, array $b): int => $a['key'] <=> $b['key']
29+
);
30+
}
31+
}
32+
33+
?>

rules/TypeDeclaration/Rector/FuncCall/AddArrayFunctionClosureParamTypeRector.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,19 @@ public function getNodeTypes(): array
6565
*/
6666
public function refactor(Node $node): ?Node
6767
{
68+
if ($node->isFirstClassCallable()) {
69+
return null;
70+
}
71+
72+
if (count($node->getArgs()) !== 2) {
73+
return null;
74+
}
75+
6876
foreach (NativeFuncCallPositions::ARRAY_AND_CALLBACK_POSITIONS as $functionName => $positions) {
6977
if (! $this->isName($node, $functionName)) {
7078
continue;
7179
}
7280

73-
if ($node->isFirstClassCallable()) {
74-
continue;
75-
}
76-
7781
$arrayPosition = $positions['array'];
7882
$callbackPosition = $positions['callback'];
7983

rules/TypeDeclaration/Rector/FuncCall/AddArrowFunctionParamArrayWhereDimFetchRector.php

Lines changed: 130 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
use PhpParser\Node;
88
use PhpParser\Node\Expr\ArrayDimFetch;
99
use PhpParser\Node\Expr\ArrowFunction;
10+
use PhpParser\Node\Expr\Closure;
1011
use PhpParser\Node\Expr\FuncCall;
1112
use PhpParser\Node\Identifier;
13+
use Rector\PhpParser\Node\BetterNodeFinder;
1214
use Rector\Rector\AbstractRector;
15+
use Rector\TypeDeclaration\Enum\NativeFuncCallPositions;
1316
use Rector\ValueObject\PhpVersionFeature;
1417
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
1518
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
@@ -20,6 +23,11 @@
2023
*/
2124
final class AddArrowFunctionParamArrayWhereDimFetchRector extends AbstractRector implements MinPhpVersionInterface
2225
{
26+
public function __construct(
27+
private BetterNodeFinder $betterNodeFinder
28+
) {
29+
}
30+
2331
public function getRuleDefinition(): RuleDefinition
2432
{
2533
return new RuleDefinition('Add function/closure param array type, if dim fetch is inside', [
@@ -53,48 +61,148 @@ public function getNodeTypes(): array
5361
*/
5462
public function refactor(Node $node): ?Node
5563
{
56-
if (! $this->isName($node, 'array_map')) {
57-
return null;
58-
}
59-
6064
if ($node->isFirstClassCallable()) {
6165
return null;
6266
}
6367

64-
$firstArgExpr = $node->getArgs()[0]
65-
->value;
66-
if (! $firstArgExpr instanceof ArrowFunction) {
68+
if (count($node->getArgs()) !== 2) {
6769
return null;
6870
}
6971

70-
$arrowFunction = $firstArgExpr;
71-
$arrowFunctionParam = $arrowFunction->getParams()[0];
72+
$hasChanged = false;
7273

73-
// param is known already
74-
if ($arrowFunctionParam->type instanceof Node) {
75-
return null;
76-
}
74+
foreach (NativeFuncCallPositions::ARRAY_AND_CALLBACK_POSITIONS as $functionName => $positions) {
75+
if (! $this->isName($node, $functionName)) {
76+
continue;
77+
}
7778

78-
if (! $arrowFunction->expr instanceof ArrayDimFetch) {
79-
return null;
80-
}
79+
$callbackPosition = $positions['callback'];
80+
81+
$closureExpr = $node->getArgs()[$callbackPosition]->value;
82+
if (! $closureExpr instanceof ArrowFunction && ! $closureExpr instanceof Closure) {
83+
continue;
84+
}
85+
86+
$isArrayVariableNames = $this->resolveIsArrayVariables($closureExpr);
87+
$instanceofVariableNames = $this->resolveInstanceofVariables($closureExpr);
88+
$skippedVariableNames = array_merge($isArrayVariableNames, $instanceofVariableNames);
89+
90+
$dimFetchVariableNames = $this->resolveDimFetchVariableNames($closureExpr);
91+
92+
foreach ($closureExpr->getParams() as $closureParam) {
93+
if ($closureParam->type instanceof \PhpParser\Node) {
94+
// param is known already
95+
continue;
96+
}
8197

82-
$var = $arrowFunction->expr;
83-
while ($var instanceof ArrayDimFetch) {
84-
$var = $var->var;
98+
// skip is_array() checked variables
99+
if ($this->isNames($closureParam->var, $skippedVariableNames)) {
100+
continue;
101+
}
102+
103+
if (! $this->isNames($closureParam->var, $dimFetchVariableNames)) {
104+
continue;
105+
}
106+
107+
$hasChanged = true;
108+
$closureParam->type = new Identifier('array');
109+
}
85110
}
86111

87-
if (! $this->nodeComparator->areNodesEqual($var, $arrowFunctionParam->var)) {
112+
if ($hasChanged === false) {
88113
return null;
89114
}
90115

91-
$arrowFunctionParam->type = new Identifier('array');
92-
93116
return $node;
94117
}
95118

96119
public function provideMinPhpVersion(): int
97120
{
98121
return PhpVersionFeature::SCALAR_TYPES;
99122
}
123+
124+
/**
125+
* @return string[]
126+
*/
127+
private function resolveDimFetchVariableNames(Closure|ArrowFunction $closureExpr): array
128+
{
129+
if ($closureExpr instanceof ArrowFunction) {
130+
$closureNodes = [$closureExpr->expr];
131+
} else {
132+
$closureNodes = $closureExpr->stmts;
133+
}
134+
135+
/** @var ArrayDimFetch[] $arrayDimFetches */
136+
$arrayDimFetches = $this->betterNodeFinder->findInstancesOfScoped($closureNodes, ArrayDimFetch::class);
137+
138+
$usedDimFetchVariableNames = [];
139+
140+
foreach ($arrayDimFetches as $arrayDimFetch) {
141+
if ($arrayDimFetch->var instanceof Node\Expr\Variable) {
142+
$usedDimFetchVariableNames[] = (string) $this->getName($arrayDimFetch->var);
143+
}
144+
}
145+
146+
return $usedDimFetchVariableNames;
147+
}
148+
149+
/**
150+
* @return string[]
151+
*/
152+
private function resolveIsArrayVariables(Closure|ArrowFunction $closureExpr): array
153+
{
154+
if ($closureExpr instanceof ArrowFunction) {
155+
$closureNodes = [$closureExpr->expr];
156+
} else {
157+
$closureNodes = $closureExpr->stmts;
158+
}
159+
160+
/** @var FuncCall[] $funcCalls */
161+
$funcCalls = $this->betterNodeFinder->findInstancesOfScoped($closureNodes, FuncCall::class);
162+
163+
$variableNames = [];
164+
165+
foreach ($funcCalls as $funcCall) {
166+
if (! $this->isName($funcCall, 'is_array')) {
167+
continue;
168+
}
169+
170+
$firstArgExpr = $funcCall->getArgs()[0]
171+
->value;
172+
if (! $firstArgExpr instanceof Node\Expr\Variable) {
173+
continue;
174+
}
175+
176+
$variableNames[] = (string) $this->getName($firstArgExpr);
177+
}
178+
179+
return $variableNames;
180+
}
181+
182+
/**
183+
* @return string[]
184+
*/
185+
private function resolveInstanceofVariables(Closure|ArrowFunction $closureExpr): array
186+
{
187+
if ($closureExpr instanceof ArrowFunction) {
188+
$closureNodes = [$closureExpr->expr];
189+
} else {
190+
$closureNodes = $closureExpr->stmts;
191+
}
192+
193+
/** @var Node\Expr\Instanceof_[] $instanceOfs */
194+
$instanceOfs = $this->betterNodeFinder->findInstancesOfScoped($closureNodes, Node\Expr\Instanceof_::class);
195+
196+
$variableNames = [];
197+
198+
foreach ($instanceOfs as $instanceOf) {
199+
if (! $instanceOf->expr instanceof Node\Expr\Variable) {
200+
continue;
201+
}
202+
203+
$variableNames[] = (string) $this->getName($instanceOf->expr);
204+
}
205+
206+
return $variableNames;
207+
}
100208
}

rules/TypeDeclaration/Rector/FunctionLike/AddClosureParamTypeForArrayMapRector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44

55
namespace Rector\TypeDeclaration\Rector\FunctionLike;
66

7-
use PhpParser\Node\VariadicPlaceholder;
87
use PhpParser\Node;
98
use PhpParser\Node\Arg;
109
use PhpParser\Node\Expr\Closure;
1110
use PhpParser\Node\Expr\FuncCall;
1211
use PhpParser\Node\Param;
12+
use PhpParser\Node\VariadicPlaceholder;
1313
use PHPStan\Reflection\Native\NativeFunctionReflection;
1414
use PHPStan\Type\ArrayType;
1515
use PHPStan\Type\MixedType;

0 commit comments

Comments
 (0)