Skip to content

Commit 3b02457

Browse files
authored
[perf] Read caller class name off node in ExportToReflectionFunctionRector (#8724)
1 parent ee09064 commit 3b02457

3 files changed

Lines changed: 131 additions & 1 deletion

File tree

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Benchmark for ExportToReflectionFunctionRector.
7+
*
8+
* The rule subscribes to StaticCall and, once the call is X::export('foo'), decides whether X is
9+
* ReflectionFunction (or an ancestor) before rewriting to new ReflectionFunction('foo'). It read the
10+
* caller type with getType($node->class), which runs the type resolver over the class name node even
11+
* though the name is already on the node.
12+
*
13+
* The change reads a concrete class name straight off the node (new ObjectType($name)) and only falls
14+
* back to getType() for self/static/parent, which depend on the enclosing class. The isSuperTypeOf()
15+
* check that follows is unchanged, so the decision is identical.
16+
*
17+
* This bench parses a file of many \ReflectionFunction::export('foo') calls, attaches scope (as the
18+
* real run does), then drives refactor() over every StaticCall node.
19+
*
20+
* Run on both branches to compare:
21+
* php bench/export_to_reflection_function_name_type_bench.php
22+
*
23+
* Reports CPU time (user+sys) and wall time; CPU is the reliable metric, wall is noisy under
24+
* concurrent load.
25+
*/
26+
27+
use PhpParser\Node\Expr\StaticCall;
28+
use Rector\DependencyInjection\LazyContainerFactory;
29+
use Rector\Php74\Rector\StaticCall\ExportToReflectionFunctionRector;
30+
use Rector\PhpParser\Node\BetterNodeFinder;
31+
use Rector\Testing\TestingParser\TestingParser;
32+
33+
require __DIR__ . '/../vendor/autoload.php';
34+
35+
const CALLS = 150; // ReflectionFunction::export() static calls
36+
const REPEAT = 60; // outer repeats over the whole call set
37+
38+
$rectorConfig = (new LazyContainerFactory())->create();
39+
$rectorConfig->boot();
40+
41+
/** @var TestingParser $testingParser */
42+
$testingParser = $rectorConfig->make(TestingParser::class);
43+
44+
/** @var BetterNodeFinder $betterNodeFinder */
45+
$betterNodeFinder = $rectorConfig->make(BetterNodeFinder::class);
46+
47+
$rector = $rectorConfig->make(ExportToReflectionFunctionRector::class);
48+
49+
$source = "<?php\n\nnamespace Bench;\n\n";
50+
for ($i = 0; $i < CALLS; ++$i) {
51+
$source .= "\$reflectionFunction{$i} = \\ReflectionFunction::export('foo');\n";
52+
}
53+
54+
$sourceFilePath = tempnam(sys_get_temp_dir(), 'bench_') . '.php';
55+
file_put_contents($sourceFilePath, $source);
56+
57+
$stmts = $testingParser->parseFileToDecoratedNodes($sourceFilePath);
58+
59+
/** @var StaticCall[] $staticCalls */
60+
$staticCalls = $betterNodeFinder->findInstanceOf($stmts, StaticCall::class);
61+
62+
$cpuSeconds = static function (): float {
63+
$usage = getrusage();
64+
return $usage['ru_utime.tv_sec'] + $usage['ru_utime.tv_usec'] / 1_000_000
65+
+ $usage['ru_stime.tv_sec'] + $usage['ru_stime.tv_usec'] / 1_000_000;
66+
};
67+
68+
$run = static function () use ($rector, $staticCalls): void {
69+
foreach ($staticCalls as $staticCall) {
70+
$rector->refactor($staticCall);
71+
}
72+
};
73+
74+
// warm up autoloading/reflection caches once, excluded from timing
75+
$run();
76+
77+
$wallStart = microtime(true);
78+
$cpuStart = $cpuSeconds();
79+
80+
for ($repeat = 0; $repeat < REPEAT; ++$repeat) {
81+
$run();
82+
}
83+
84+
$cpu = $cpuSeconds() - $cpuStart;
85+
$wall = microtime(true) - $wallStart;
86+
87+
@unlink($sourceFilePath);
88+
89+
$totalCalls = REPEAT * count($staticCalls);
90+
91+
printf('static calls: %d, repeats: %d, refactor() calls: %d%s', count($staticCalls), REPEAT, $totalCalls, PHP_EOL);
92+
printf('CPU: %.4f s (%.3f us/call)%s', $cpu, $cpu / $totalCalls * 1_000_000, PHP_EOL);
93+
printf('wall: %.4f s%s', $wall, PHP_EOL);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php74\Rector\StaticCall\ExportToReflectionFunctionRector\Fixture;
4+
5+
class SomeExporter
6+
{
7+
public static function export($name)
8+
{
9+
}
10+
}
11+
12+
class SkipOtherClass
13+
{
14+
public function run()
15+
{
16+
$value = SomeExporter::export('foo');
17+
}
18+
}

‎rules/Php74/Rector/StaticCall/ExportToReflectionFunctionRector.php‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
use PhpParser\Node\Expr\StaticCall;
1212
use PhpParser\Node\Name;
1313
use PHPStan\Type\ObjectType;
14+
use PHPStan\Type\Type;
15+
use Rector\Enum\ObjectReference;
1416
use Rector\PhpParser\Node\Value\ValueResolver;
1517
use Rector\Rector\AbstractRector;
1618
use Rector\RuleDoc\CodeSample\CodeSample;
@@ -83,7 +85,7 @@ public function refactor(Node $node): ?Node
8385
return null;
8486
}
8587

86-
$callerType = $this->getType($node->class);
88+
$callerType = $this->resolveCallerType($node->class);
8789
if (! $callerType->isSuperTypeOf(new ObjectType('ReflectionFunction'))->yes()) {
8890
return null;
8991
}
@@ -101,4 +103,21 @@ public function refactor(Node $node): ?Node
101103

102104
return $new;
103105
}
106+
107+
private function resolveCallerType(Name $name): Type
108+
{
109+
// a concrete class name is resolvable straight from the node, no type inference needed;
110+
// self/static/parent depend on the enclosing class, so fall back to scope-based type
111+
$className = $this->getName($name);
112+
if ($className === null) {
113+
return $this->getType($name);
114+
}
115+
116+
$loweredClassName = strtolower($className);
117+
if (in_array($loweredClassName, [ObjectReference::SELF, ObjectReference::STATIC, ObjectReference::PARENT], true)) {
118+
return $this->getType($name);
119+
}
120+
121+
return new ObjectType($className);
122+
}
104123
}

0 commit comments

Comments
 (0)