Skip to content

Commit 5c745dd

Browse files
committed
Updated Rector to commit 23c63d7f5fe081f6e9a8c4805f8995102aa4b6df
rectorphp/rector-src@23c63d7 [coding-style] Decouple ArrowFunctionToFirstClassCallableRector to allow step-by-step upgrade (#7737)
1 parent 5a40963 commit 5c745dd

File tree

9 files changed

+397
-230
lines changed

9 files changed

+397
-230
lines changed

config/set/php81.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
declare (strict_types=1);
44
namespace RectorPrefix202512;
55

6+
use Rector\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector;
7+
use Rector\CodingStyle\Rector\Closure\ClosureDelegatingCallToFirstClassCallableRector;
68
use Rector\CodingStyle\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector;
79
use Rector\CodingStyle\Rector\FuncCall\FunctionFirstClassCallableRector;
8-
use Rector\CodingStyle\Rector\FunctionLike\FunctionLikeToFirstClassCallableRector;
910
use Rector\Config\RectorConfig;
1011
use Rector\Php81\Rector\Array_\ArrayToFirstClassCallableRector;
1112
use Rector\Php81\Rector\Class_\MyCLabsClassToEnumRector;
@@ -30,10 +31,10 @@
3031
SpatieEnumMethodCallToEnumConstRector::class,
3132
NullToStrictStringFuncCallArgRector::class,
3233
NullToStrictIntPregSlitFuncCallLimitArgRector::class,
33-
// array of local method call
3434
ArrayToFirstClassCallableRector::class,
3535
// closure/arrow function
36-
FunctionLikeToFirstClassCallableRector::class,
36+
ArrowFunctionDelegatingCallToFirstClassCallableRector::class,
37+
ClosureDelegatingCallToFirstClassCallableRector::class,
3738
ClosureFromCallableToFirstClassCallableRector::class,
3839
FunctionFirstClassCallableRector::class,
3940
RemoveReflectionSetAccessibleCallsRector::class,
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
<?php
2+
3+
declare (strict_types=1);
4+
namespace Rector\CodingStyle\Guard;
5+
6+
use PhpParser\Node;
7+
use PhpParser\Node\Arg;
8+
use PhpParser\Node\Expr\ArrowFunction;
9+
use PhpParser\Node\Expr\CallLike;
10+
use PhpParser\Node\Expr\Closure;
11+
use PhpParser\Node\Expr\FuncCall;
12+
use PhpParser\Node\Expr\MethodCall;
13+
use PhpParser\Node\Expr\StaticCall;
14+
use PhpParser\Node\Expr\Variable;
15+
use PhpParser\Node\FunctionLike;
16+
use PhpParser\Node\Identifier;
17+
use PhpParser\Node\Param;
18+
use PhpParser\NodeVisitor;
19+
use PHPStan\Analyser\Scope;
20+
use PHPStan\Reflection\Annotations\AnnotationMethodReflection;
21+
use Rector\NodeNameResolver\NodeNameResolver;
22+
use Rector\NodeTypeResolver\Node\AttributeKey;
23+
use Rector\PhpDocParser\NodeTraverser\SimpleCallableNodeTraverser;
24+
use Rector\PhpParser\AstResolver;
25+
use Rector\PhpParser\Comparing\NodeComparator;
26+
use Rector\Reflection\ReflectionResolver;
27+
final class ArrowFunctionAndClosureFirstClassCallableGuard
28+
{
29+
/**
30+
* @readonly
31+
*/
32+
private ReflectionResolver $reflectionResolver;
33+
/**
34+
* @readonly
35+
*/
36+
private AstResolver $astResolver;
37+
/**
38+
* @readonly
39+
*/
40+
private NodeComparator $nodeComparator;
41+
/**
42+
* @readonly
43+
*/
44+
private NodeNameResolver $nodeNameResolver;
45+
public function __construct(ReflectionResolver $reflectionResolver, AstResolver $astResolver, NodeComparator $nodeComparator, NodeNameResolver $nodeNameResolver)
46+
{
47+
$this->reflectionResolver = $reflectionResolver;
48+
$this->astResolver = $astResolver;
49+
$this->nodeComparator = $nodeComparator;
50+
$this->nodeNameResolver = $nodeNameResolver;
51+
}
52+
/**
53+
* @param \PhpParser\Node\Expr\ArrowFunction|\PhpParser\Node\Expr\Closure $arrowFunctionOrClosure
54+
* @param \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $callLike
55+
*/
56+
public function shouldSkip($arrowFunctionOrClosure, $callLike, Scope $scope): bool
57+
{
58+
if ($callLike->isFirstClassCallable()) {
59+
return \true;
60+
}
61+
// use cheap checks first
62+
if ($arrowFunctionOrClosure->getAttribute(AttributeKey::HAS_CLOSURE_WITH_VARIADIC_ARGS) === \true) {
63+
return \true;
64+
}
65+
if ($arrowFunctionOrClosure->getAttribute(AttributeKey::IS_ASSIGNED_TO) === \true || $arrowFunctionOrClosure->getAttribute(AttributeKey::IS_BEING_ASSIGNED)) {
66+
return \true;
67+
}
68+
$params = $arrowFunctionOrClosure->getParams();
69+
if (count($params) !== count($callLike->getArgs())) {
70+
return \true;
71+
}
72+
$args = $callLike->getArgs();
73+
if ($this->isChainedCall($callLike)) {
74+
return \true;
75+
}
76+
if ($this->isUsingNamedArgs($args)) {
77+
return \true;
78+
}
79+
if ($this->isUsingByRef($params)) {
80+
return \true;
81+
}
82+
if ($this->isNotUsingSameParamsForArgs($params, $args)) {
83+
return \true;
84+
}
85+
if ($this->isDependantMethod($callLike, $params)) {
86+
return \true;
87+
}
88+
if ($this->isUsingThisInNonObjectContext($callLike, $scope)) {
89+
return \true;
90+
}
91+
$reflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($callLike);
92+
// does not exists, probably by magic method
93+
if ($reflection === null) {
94+
return \true;
95+
}
96+
// exists, but by @method annotation
97+
if ($reflection instanceof AnnotationMethodReflection && !$reflection->getDeclaringClass()->hasNativeMethod($reflection->getName())) {
98+
return \true;
99+
}
100+
$functionLike = $this->astResolver->resolveClassMethodOrFunctionFromCall($callLike);
101+
if (!$functionLike instanceof FunctionLike) {
102+
return \false;
103+
}
104+
return count($functionLike->getParams()) > 1;
105+
}
106+
/**
107+
* @param Param[] $params
108+
* @param \PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\FuncCall $expr
109+
*/
110+
private function isDependantMethod($expr, array $params): bool
111+
{
112+
if ($expr instanceof FuncCall) {
113+
return \false;
114+
}
115+
$found = \false;
116+
$parentNode = $expr instanceof MethodCall ? $expr->var : $expr->class;
117+
foreach ($params as $param) {
118+
SimpleCallableNodeTraverser::traverse($parentNode, function (Node $node) use ($param, &$found): ?int {
119+
if ($this->nodeComparator->areNodesEqual($node, $param->var)) {
120+
$found = \true;
121+
return NodeVisitor::STOP_TRAVERSAL;
122+
}
123+
return null;
124+
});
125+
if ($found) {
126+
return \true;
127+
}
128+
}
129+
return \false;
130+
}
131+
/**
132+
* @param \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $callLike
133+
*/
134+
private function isUsingThisInNonObjectContext($callLike, Scope $scope): bool
135+
{
136+
if (!$callLike instanceof MethodCall) {
137+
return \false;
138+
}
139+
if (in_array('this', $scope->getDefinedVariables(), \true)) {
140+
return \false;
141+
}
142+
$found = \false;
143+
SimpleCallableNodeTraverser::traverse($callLike, function (Node $node) use (&$found): ?int {
144+
if ($this->nodeNameResolver->isName($node, 'this')) {
145+
$found = \true;
146+
return NodeVisitor::STOP_TRAVERSAL;
147+
}
148+
return null;
149+
});
150+
return $found;
151+
}
152+
/**
153+
* @param Param[] $params
154+
*/
155+
private function isUsingByRef(array $params): bool
156+
{
157+
foreach ($params as $param) {
158+
if ($param->byRef) {
159+
return \true;
160+
}
161+
}
162+
return \false;
163+
}
164+
/**
165+
* @param Arg[] $args
166+
*/
167+
private function isUsingNamedArgs(array $args): bool
168+
{
169+
foreach ($args as $arg) {
170+
if ($arg->name instanceof Identifier) {
171+
return \true;
172+
}
173+
}
174+
return \false;
175+
}
176+
/**
177+
* @param \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $callLike
178+
*/
179+
private function isChainedCall($callLike): bool
180+
{
181+
if (!$callLike instanceof MethodCall) {
182+
return \false;
183+
}
184+
return $callLike->var instanceof CallLike;
185+
}
186+
/**
187+
* @param Param[] $params
188+
* @param Arg[] $args
189+
*/
190+
private function isNotUsingSameParamsForArgs(array $params, array $args): bool
191+
{
192+
if (count($args) > count($params)) {
193+
return \true;
194+
}
195+
if (count($args) === 1 && $args[0]->unpack) {
196+
return !$params[0]->variadic;
197+
}
198+
foreach ($args as $key => $arg) {
199+
if (!$this->nodeComparator->areNodesEqual($arg->value, $params[$key]->var)) {
200+
return \true;
201+
}
202+
if (!$arg->value instanceof Variable) {
203+
continue;
204+
}
205+
$variableName = (string) $this->nodeNameResolver->getName($arg->value);
206+
foreach ($params as $param) {
207+
if ($param->var instanceof Variable && $this->nodeNameResolver->isName($param->var, $variableName) && $param->variadic && !$arg->unpack) {
208+
return \true;
209+
}
210+
}
211+
}
212+
return \false;
213+
}
214+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
declare (strict_types=1);
4+
namespace Rector\CodingStyle\Rector\ArrowFunction;
5+
6+
use PhpParser\Node;
7+
use PhpParser\Node\Expr;
8+
use PhpParser\Node\Expr\ArrowFunction;
9+
use PhpParser\Node\Expr\CallLike;
10+
use PhpParser\Node\Expr\FuncCall;
11+
use PhpParser\Node\Expr\MethodCall;
12+
use PhpParser\Node\Expr\StaticCall;
13+
use PhpParser\Node\VariadicPlaceholder;
14+
use Rector\CodingStyle\Guard\ArrowFunctionAndClosureFirstClassCallableGuard;
15+
use Rector\PHPStan\ScopeFetcher;
16+
use Rector\Rector\AbstractRector;
17+
use Rector\ValueObject\PhpVersionFeature;
18+
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
19+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
20+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
21+
/**
22+
* @see \Rector\Tests\CodingStyle\Rector\ArrowFunction\ArrowFunctionDelegatingCallToFirstClassCallableRector\ArrowFunctionDelegatingCallToFirstClassCallableRectorTest
23+
*/
24+
final class ArrowFunctionDelegatingCallToFirstClassCallableRector extends AbstractRector implements MinPhpVersionInterface
25+
{
26+
/**
27+
* @readonly
28+
*/
29+
private ArrowFunctionAndClosureFirstClassCallableGuard $arrowFunctionAndCLosureFirstClassCallableGuard;
30+
public function __construct(ArrowFunctionAndClosureFirstClassCallableGuard $arrowFunctionAndCLosureFirstClassCallableGuard)
31+
{
32+
$this->arrowFunctionAndCLosureFirstClassCallableGuard = $arrowFunctionAndCLosureFirstClassCallableGuard;
33+
}
34+
public function getRuleDefinition(): RuleDefinition
35+
{
36+
return new RuleDefinition('Convert nested arrow function call to first class callable', [new CodeSample(<<<'CODE_SAMPLE'
37+
fn ($parameter) => Call::to($parameter);
38+
CODE_SAMPLE
39+
, <<<'CODE_SAMPLE'
40+
Call::to(...);
41+
CODE_SAMPLE
42+
)]);
43+
}
44+
public function getNodeTypes(): array
45+
{
46+
return [ArrowFunction::class];
47+
}
48+
/**
49+
* @param ArrowFunction $node
50+
*/
51+
public function refactor(Node $node): ?\PhpParser\Node\Expr\CallLike
52+
{
53+
if (!$node->expr instanceof FuncCall && !$node->expr instanceof MethodCall && !$node->expr instanceof StaticCall) {
54+
return null;
55+
}
56+
$callLike = $node->expr;
57+
// dynamic name? skip
58+
if ($callLike->name instanceof Expr) {
59+
return null;
60+
}
61+
if ($this->arrowFunctionAndCLosureFirstClassCallableGuard->shouldSkip($node, $callLike, ScopeFetcher::fetch($node))) {
62+
return null;
63+
}
64+
// turn into first class callable
65+
$callLike->args = [new VariadicPlaceholder()];
66+
return $callLike;
67+
}
68+
public function provideMinPhpVersion(): int
69+
{
70+
return PhpVersionFeature::FIRST_CLASS_CALLABLE_SYNTAX;
71+
}
72+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare (strict_types=1);
4+
namespace Rector\CodingStyle\Rector\Closure;
5+
6+
use PhpParser\Node;
7+
use PhpParser\Node\Expr;
8+
use PhpParser\Node\Expr\Closure;
9+
use PhpParser\Node\Expr\FuncCall;
10+
use PhpParser\Node\Expr\MethodCall;
11+
use PhpParser\Node\Expr\StaticCall;
12+
use PhpParser\Node\Stmt\Return_;
13+
use PhpParser\Node\VariadicPlaceholder;
14+
use Rector\CodingStyle\Guard\ArrowFunctionAndClosureFirstClassCallableGuard;
15+
use Rector\PHPStan\ScopeFetcher;
16+
use Rector\Rector\AbstractRector;
17+
use Rector\ValueObject\PhpVersionFeature;
18+
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
19+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
20+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
21+
/**
22+
* @see \Rector\Tests\CodingStyle\Rector\Closure\ClosureDelegatingCallToFirstClassCallableRector\ClosureDelegatingCallToFirstClassCallableRectorTest
23+
*/
24+
final class ClosureDelegatingCallToFirstClassCallableRector extends AbstractRector implements MinPhpVersionInterface
25+
{
26+
/**
27+
* @readonly
28+
*/
29+
private ArrowFunctionAndClosureFirstClassCallableGuard $arrowFunctionAndClosureFirstClassCallableGuard;
30+
public function __construct(ArrowFunctionAndClosureFirstClassCallableGuard $arrowFunctionAndClosureFirstClassCallableGuard)
31+
{
32+
$this->arrowFunctionAndClosureFirstClassCallableGuard = $arrowFunctionAndClosureFirstClassCallableGuard;
33+
}
34+
public function getRuleDefinition(): RuleDefinition
35+
{
36+
return new RuleDefinition('Convert closure with sole nested call to first class callable', [new CodeSample(<<<'CODE_SAMPLE'
37+
function ($parameter) {
38+
return AnotherClass::someMethod($parameter);
39+
}
40+
CODE_SAMPLE
41+
, <<<'CODE_SAMPLE'
42+
AnotherClass::someMethod(...);
43+
CODE_SAMPLE
44+
)]);
45+
}
46+
public function getNodeTypes(): array
47+
{
48+
return [Closure::class];
49+
}
50+
/**
51+
* @param Closure $node
52+
* @return null|\PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall
53+
*/
54+
public function refactor(Node $node)
55+
{
56+
// must have exactly 1 stmt with Return_
57+
if (count($node->stmts) !== 1 || !$node->stmts[0] instanceof Return_) {
58+
return null;
59+
}
60+
$callLike = $node->stmts[0]->expr;
61+
if (!$callLike instanceof FuncCall && !$callLike instanceof MethodCall && !$callLike instanceof StaticCall) {
62+
return null;
63+
}
64+
// dynamic name? skip
65+
if ($callLike->name instanceof Expr) {
66+
return null;
67+
}
68+
if ($this->arrowFunctionAndClosureFirstClassCallableGuard->shouldSkip($node, $callLike, ScopeFetcher::fetch($node))) {
69+
return null;
70+
}
71+
$callLike->args = [new VariadicPlaceholder()];
72+
return $callLike;
73+
}
74+
public function provideMinPhpVersion(): int
75+
{
76+
return PhpVersionFeature::FIRST_CLASS_CALLABLE_SYNTAX;
77+
}
78+
}

0 commit comments

Comments
 (0)