Skip to content

Commit eb07bff

Browse files
authored
Add ClosureFromCallableToFirstClassCallableRector rule (#7071)
1 parent 410616a commit eb07bff

File tree

8 files changed

+277
-0
lines changed

8 files changed

+277
-0
lines changed

rules-tests/CodingStyle/Rector/FuncCall/CallUserFuncToMethodCallRector/config/configured_rule.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use Rector\CodingStyle\Rector\FuncCall\CallUserFuncToMethodCallRector;
66
use Rector\Config\RectorConfig;
7+
use Rector\ValueObject\PhpVersion;
78

89
return RectorConfig::configure()
10+
->withPhpVersion(PhpVersion::PHP_81)
911
->withRules([CallUserFuncToMethodCallRector::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\CodingStyle\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class ClosureFromCallableToFirstClassCallableRectorTest 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,29 @@
1+
<?php
2+
3+
namespace Rector\Tests\CodingStyle\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture;
4+
5+
use SomeNamespace\Foo;
6+
use Closure;
7+
8+
Closure::fromCallable(['SomeClass', 'staticMethod']);
9+
Closure::fromCallable(['Foo\SomeClass', 'staticMethod']);
10+
11+
Closure::fromCallable([SomeClass::class, 'staticMethod']);
12+
Closure::fromCallable([Foo\SomeClass::class, 'staticMethod']);
13+
14+
?>
15+
-----
16+
<?php
17+
18+
namespace Rector\Tests\CodingStyle\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture;
19+
20+
use SomeNamespace\Foo;
21+
use Closure;
22+
23+
\SomeClass::staticMethod(...);
24+
\Foo\SomeClass::staticMethod(...);
25+
26+
\Rector\Tests\CodingStyle\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture\SomeClass::staticMethod(...);
27+
\SomeNamespace\Foo\SomeClass::staticMethod(...);
28+
29+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace Rector\Tests\CodingStyle\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture {
4+
function trim()
5+
{}
6+
7+
\Closure::fromCallable('Rector\Tests\CodingStyle\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture\trim');
8+
\Closure::fromCallable('trim');
9+
}
10+
11+
namespace {
12+
\Closure::fromCallable('trim');
13+
\Closure::fromCallable('\trim');
14+
\Closure::fromCallable('Psl\Str\trim');
15+
\Closure::fromCallable('\Psl\Str\trim');
16+
}
17+
18+
?>
19+
-----
20+
<?php
21+
22+
namespace Rector\Tests\CodingStyle\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture {
23+
function trim()
24+
{}
25+
26+
\Rector\Tests\CodingStyle\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture\trim(...);
27+
\trim(...);
28+
}
29+
30+
namespace {
31+
\trim(...);
32+
\trim(...);
33+
\Psl\Str\trim(...);
34+
\Psl\Str\trim(...);
35+
}
36+
37+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Rector\Tests\CodingStyle\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture;
4+
5+
use Closure;
6+
7+
Closure::fromCallable([$obj, 'method']);
8+
9+
?>
10+
-----
11+
<?php
12+
13+
namespace Rector\Tests\CodingStyle\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\Fixture;
14+
15+
use Closure;
16+
17+
$obj->method(...);
18+
19+
?>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\CodingStyle\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector;
6+
use Rector\Config\RectorConfig;
7+
8+
return RectorConfig::configure()
9+
->withRules([ClosureFromCallableToFirstClassCallableRector::class]);
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\CodingStyle\Rector\FuncCall;
6+
7+
use PhpParser\Node;
8+
use Rector\Rector\AbstractRector;
9+
use Rector\ValueObject\PhpVersionFeature;
10+
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
11+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
12+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
13+
14+
/**
15+
* @see \Rector\Tests\CodingStyle\Rector\FuncCall\ClosureFromCallableToFirstClassCallableRector\ClosureFromCallableToFirstClassCallableRectorTest
16+
*/
17+
final class ClosureFromCallableToFirstClassCallableRector extends AbstractRector implements MinPhpVersionInterface
18+
{
19+
public function __construct()
20+
{
21+
}
22+
23+
public function getRuleDefinition(): RuleDefinition
24+
{
25+
return new RuleDefinition(
26+
'Change `Closure::fromCallable()` to first class callable syntax',
27+
[
28+
new CodeSample('Closure::fromCallable([$obj, \'method\']);', '$obj->method(...);'),
29+
new CodeSample('Closure::fromCallable(\'trim\');', 'trim(...);'),
30+
new CodeSample(
31+
'Closure::fromCallable([\'SomeClass\', \'staticMethod\']);',
32+
'SomeClass::staticMethod(...);'
33+
),
34+
]
35+
);
36+
37+
}
38+
39+
/**
40+
* @return array<class-string<Node>>
41+
*/
42+
public function getNodeTypes(): array
43+
{
44+
return [Node\Expr\StaticCall::class];
45+
}
46+
47+
/**
48+
* @param Node\Expr\StaticCall $node
49+
*/
50+
public function refactor(Node $node): ?Node
51+
{
52+
if ($this->shouldSkip($node)) {
53+
return null;
54+
}
55+
56+
$arg = $node->args[0];
57+
if (! $arg instanceof Node\Arg) {
58+
return null;
59+
}
60+
61+
if ($arg->value instanceof Node\Scalar\String_) {
62+
return new Node\Expr\FuncCall(
63+
$this->toFullyQualified($arg->value->value),
64+
[new Node\VariadicPlaceholder()],
65+
);
66+
}
67+
68+
if ($arg->value instanceof Node\Expr\Array_) {
69+
if (
70+
! array_key_exists(0, $arg->value->items)
71+
|| ! array_key_exists(1, $arg->value->items)
72+
|| ! $arg->value->items[1]->value instanceof Node\Scalar\String_
73+
) {
74+
return null;
75+
}
76+
77+
if ($arg->value->items[0]->value instanceof Node\Expr\Variable) {
78+
return new Node\Expr\MethodCall(
79+
$arg->value->items[0]->value,
80+
$arg->value->items[1]->value->value,
81+
[new Node\VariadicPlaceholder()],
82+
);
83+
}
84+
85+
if ($arg->value->items[0]->value instanceof Node\Scalar\String_) {
86+
$classNode = new Node\Name\FullyQualified($arg->value->items[0]->value->value);
87+
} elseif ($arg->value->items[0]->value instanceof Node\Expr\ClassConstFetch) {
88+
if ($arg->value->items[0]->value->class instanceof Node\Expr) {
89+
return null;
90+
}
91+
92+
$classNode = new Node\Name\FullyQualified($arg->value->items[0]->value->class->name);
93+
} elseif ($arg->value->items[0]->value instanceof Node\Name\FullyQualified) {
94+
$classNode = new Node\Name\FullyQualified($arg->value->items[0]->value->name);
95+
} else {
96+
return null;
97+
}
98+
99+
return new Node\Expr\StaticCall(
100+
$classNode,
101+
$arg->value->items[1]->value->value,
102+
[new Node\VariadicPlaceholder()],
103+
);
104+
}
105+
106+
return $node;
107+
}
108+
109+
public function provideMinPhpVersion(): int
110+
{
111+
return PhpVersionFeature::FIRST_CLASS_CALLABLE_SYNTAX;
112+
}
113+
114+
public function shouldSkip(Node\Expr\StaticCall $node): bool
115+
{
116+
if (! $node->class instanceof Node\Name) {
117+
return true;
118+
}
119+
120+
if (! $this->isName($node->class, 'Closure')) {
121+
return true;
122+
}
123+
124+
if (! $node->name instanceof Node\Identifier || $node->name->name !== 'fromCallable') {
125+
return true;
126+
}
127+
128+
if ($node->isFirstClassCallable()) {
129+
return true;
130+
}
131+
132+
$args = $node->getArgs();
133+
if (count($args) !== 1) {
134+
return true;
135+
}
136+
137+
return false;
138+
}
139+
140+
public function toFullyQualified(string $functionName): Node\Name\FullyQualified
141+
{
142+
// in case there's already a \ prefix, remove it
143+
$functionName = ltrim($functionName, '\\');
144+
145+
return new Node\Name\FullyQualified($functionName);
146+
}
147+
}

src/ValueObject/PhpVersionFeature.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,12 @@ final class PhpVersionFeature
617617
*/
618618
public const RETURN_TYPE_WILL_CHANGE_ATTRIBUTE = PhpVersion::PHP_81;
619619

620+
/**
621+
* @see https://wiki.php.net/rfc/first_class_callable_syntax
622+
* @var int
623+
*/
624+
public const FIRST_CLASS_CALLABLE_SYNTAX = PhpVersion::PHP_81;
625+
620626
/**
621627
* @see https://wiki.php.net/rfc/deprecate_dynamic_properties
622628
* @var int

0 commit comments

Comments
 (0)