Skip to content

Commit 45735fb

Browse files
authored
Simplify narrowToFullyQualifiedOrAliasedObjectType in ObjectTypeSpecifier (#8835)
* simplify narrowToFullyQualifiedOrAliasedObjectType in ObjectTypeSpecifier * add bench for narrowToFullyQualifiedOrAliasedObjectType * simplify helper methods in ObjectTypeSpecifier
1 parent bc7b3a9 commit 45735fb

2 files changed

Lines changed: 90 additions & 33 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Benchmark for ObjectTypeSpecifier::narrowToFullyQualifiedOrAliasedObjectType().
7+
*
8+
* The method is hit once per object-typed phpdoc/name node during type resolution.
9+
* The simplification keeps behavior identical; this bench confirms it does not
10+
* regress CPU. Three representative inputs are looped, each hitting a different
11+
* exit path:
12+
*
13+
* - preslash: leading "\\" short-circuit (top of method)
14+
* - existing: resolve() uses + reflectionProvider->hasClass() hit -> FullyQualified
15+
* - non-existing: full fall-through to resolveNamespacedNonExistingObjectType()
16+
*
17+
* Run on both branches to compare:
18+
* php bench/object_type_specifier_narrow_bench.php
19+
*
20+
* Reports CPU time (user+sys); wall is noise under concurrent load.
21+
*/
22+
23+
use PhpParser\Node\Identifier;
24+
use PHPStan\Type\ObjectType;
25+
use Rector\DependencyInjection\LazyContainerFactory;
26+
use Rector\TypeDeclaration\PHPStan\ObjectTypeSpecifier;
27+
28+
require __DIR__ . '/../vendor/autoload.php';
29+
30+
const NODES = 2000; // narrow() calls per pass
31+
const REPEAT = 40; // outer repeats
32+
33+
$rectorConfig = (new LazyContainerFactory())->create();
34+
$rectorConfig->boot();
35+
36+
/** @var ObjectTypeSpecifier $objectTypeSpecifier */
37+
$objectTypeSpecifier = $rectorConfig->make(ObjectTypeSpecifier::class);
38+
39+
// node only used in the scope/template branch; scope is null here, so a bare dummy is fine
40+
$node = new Identifier('x');
41+
42+
$inputs = [
43+
new ObjectType('\\' . ObjectType::class), // preslash short-circuit
44+
new ObjectType(ObjectType::class), // existing class -> hasClass hit
45+
new ObjectType('No\\Such\\ClassName'), // non-existing -> full fall-through
46+
];
47+
48+
$cpuSeconds = static function (): float {
49+
$usage = getrusage();
50+
return $usage['ru_utime.tv_sec'] + $usage['ru_utime.tv_usec'] / 1_000_000
51+
+ $usage['ru_stime.tv_sec'] + $usage['ru_stime.tv_usec'] / 1_000_000;
52+
};
53+
54+
$run = static function () use ($objectTypeSpecifier, $node, $inputs): void {
55+
for ($i = 0; $i < NODES; ++$i) {
56+
foreach ($inputs as $objectType) {
57+
$objectTypeSpecifier->narrowToFullyQualifiedOrAliasedObjectType($node, $objectType, null);
58+
}
59+
}
60+
};
61+
62+
// warm up autoloading once, excluded from timing
63+
$run();
64+
65+
$cpuStart = $cpuSeconds();
66+
for ($repeat = 0; $repeat < REPEAT; ++$repeat) {
67+
$run();
68+
}
69+
$cpu = $cpuSeconds() - $cpuStart;
70+
71+
$totalCalls = NODES * count($inputs) * REPEAT;
72+
73+
printf('ObjectTypeSpecifier::narrowToFullyQualifiedOrAliasedObjectType benchmark%s', PHP_EOL);
74+
printf(' nodes=%d inputs=%d repeat=%d (%d calls)%s', NODES, count($inputs), REPEAT, $totalCalls, PHP_EOL);
75+
printf('%s', PHP_EOL);
76+
printf(' cpu=%.4fs per-call=%.4fus%s', $cpu, $cpu / $totalCalls * 1_000_000, PHP_EOL);

‎rules/TypeDeclaration/PHPStan/ObjectTypeSpecifier.php‎

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ public function narrowToFullyQualifiedOrAliasedObjectType(
4444
Scope|null $scope,
4545
bool $withPreslash = false
4646
): TypeWithClassName|NonExistingObjectType|UnionType|MixedType|TemplateType {
47-
$className = ltrim($objectType->getClassName(), '\\');
48-
if (str_starts_with($objectType->getClassName(), '\\')) {
47+
$rawClassName = $objectType->getClassName();
48+
$className = ltrim($rawClassName, '\\');
49+
if (str_starts_with($rawClassName, '\\')) {
4950
return new FullyQualifiedObjectType($className);
5051
}
5152

@@ -87,27 +88,18 @@ public function narrowToFullyQualifiedOrAliasedObjectType(
8788

8889
$classReflection = $scope->getClassReflection();
8990
if ($classReflection instanceof ClassReflection) {
90-
$templateTags = $classReflection->getTemplateTags();
9191
$nameScope = $this->nameScopeFactory->createNameScopeFromNodeWithoutTemplateTypes($node);
9292
$templateTypeScope = $nameScope->getTemplateTypeScope();
93-
94-
if (! $templateTypeScope instanceof TemplateTypeScope) {
95-
// invalid type
96-
return $this->resolveNamespacedNonExistingObjectType($namespaceName, $className, $withPreslash);
97-
}
98-
99-
$currentTemplateTag = $templateTags[$className] ?? null;
100-
if ($currentTemplateTag === null) {
101-
// invalid type
102-
return $this->resolveNamespacedNonExistingObjectType($namespaceName, $className, $withPreslash);
93+
$currentTemplateTag = $classReflection->getTemplateTags()[$className] ?? null;
94+
95+
if ($templateTypeScope instanceof TemplateTypeScope && $currentTemplateTag !== null) {
96+
return TemplateTypeFactory::create(
97+
$templateTypeScope,
98+
$currentTemplateTag->getName(),
99+
$currentTemplateTag->getBound(),
100+
$currentTemplateTag->getVariance()
101+
);
103102
}
104-
105-
return TemplateTypeFactory::create(
106-
$templateTypeScope,
107-
$currentTemplateTag->getName(),
108-
$currentTemplateTag->getBound(),
109-
$currentTemplateTag->getVariance()
110-
);
111103
}
112104
}
113105

@@ -120,15 +112,7 @@ private function resolveNamespacedNonExistingObjectType(
120112
string $className,
121113
bool $withPreslash
122114
): NonExistingObjectType {
123-
if ($namespacedName === null) {
124-
return new NonExistingObjectType($className);
125-
}
126-
127-
if ($withPreslash) {
128-
return new NonExistingObjectType($className);
129-
}
130-
131-
if (str_contains($className, '\\')) {
115+
if ($namespacedName === null || $withPreslash || str_contains($className, '\\')) {
132116
return new NonExistingObjectType($className);
133117
}
134118

@@ -153,14 +137,12 @@ private function matchAliasedObjectType(ObjectType $objectType, array $uses): ?A
153137
continue;
154138
}
155139

156-
$useName = $prefix . $useUse->name->toString();
157140
$alias = $useUse->alias->toString();
158141
$fullyQualifiedName = $prefix . $useUse->name->toString();
159142

160143
$processAliasedObject = $this->processAliasedObject(
161144
$alias,
162145
$className,
163-
$useName,
164146
$fullyQualifiedName
165147
);
166148

@@ -176,7 +158,6 @@ private function matchAliasedObjectType(ObjectType $objectType, array $uses): ?A
176158
private function processAliasedObject(
177159
string $alias,
178160
string $className,
179-
string $useName,
180161
string $fullyQualifiedName
181162
): ?AliasedObjectType {
182163
// A. is alias in use statement matching this class alias
@@ -185,7 +166,7 @@ private function processAliasedObject(
185166
}
186167

187168
// B. is aliased classes matching the class name
188-
if ($useName === $className) {
169+
if ($fullyQualifiedName === $className) {
189170
return new AliasedObjectType($className, $fullyQualifiedName);
190171
}
191172

0 commit comments

Comments
 (0)