Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions bench/object_type_specifier_narrow_bench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

/**
* Benchmark for ObjectTypeSpecifier::narrowToFullyQualifiedOrAliasedObjectType().
*
* The method is hit once per object-typed phpdoc/name node during type resolution.
* The simplification keeps behavior identical; this bench confirms it does not
* regress CPU. Three representative inputs are looped, each hitting a different
* exit path:
*
* - preslash: leading "\\" short-circuit (top of method)
* - existing: resolve() uses + reflectionProvider->hasClass() hit -> FullyQualified
* - non-existing: full fall-through to resolveNamespacedNonExistingObjectType()
*
* Run on both branches to compare:
* php bench/object_type_specifier_narrow_bench.php
*
* Reports CPU time (user+sys); wall is noise under concurrent load.
*/

use PhpParser\Node\Identifier;
use PHPStan\Type\ObjectType;
use Rector\DependencyInjection\LazyContainerFactory;
use Rector\TypeDeclaration\PHPStan\ObjectTypeSpecifier;

require __DIR__ . '/../vendor/autoload.php';

const NODES = 2000; // narrow() calls per pass
const REPEAT = 40; // outer repeats

$rectorConfig = (new LazyContainerFactory())->create();
$rectorConfig->boot();

/** @var ObjectTypeSpecifier $objectTypeSpecifier */
$objectTypeSpecifier = $rectorConfig->make(ObjectTypeSpecifier::class);

// node only used in the scope/template branch; scope is null here, so a bare dummy is fine
$node = new Identifier('x');

$inputs = [
new ObjectType('\\' . ObjectType::class), // preslash short-circuit
new ObjectType(ObjectType::class), // existing class -> hasClass hit
new ObjectType('No\\Such\\ClassName'), // non-existing -> full fall-through
];

$cpuSeconds = static function (): float {
$usage = getrusage();
return $usage['ru_utime.tv_sec'] + $usage['ru_utime.tv_usec'] / 1_000_000
+ $usage['ru_stime.tv_sec'] + $usage['ru_stime.tv_usec'] / 1_000_000;
};

$run = static function () use ($objectTypeSpecifier, $node, $inputs): void {
for ($i = 0; $i < NODES; ++$i) {
foreach ($inputs as $objectType) {
$objectTypeSpecifier->narrowToFullyQualifiedOrAliasedObjectType($node, $objectType, null);
}
}
};

// warm up autoloading once, excluded from timing
$run();

$cpuStart = $cpuSeconds();
for ($repeat = 0; $repeat < REPEAT; ++$repeat) {
$run();
}
$cpu = $cpuSeconds() - $cpuStart;

$totalCalls = NODES * count($inputs) * REPEAT;

printf('ObjectTypeSpecifier::narrowToFullyQualifiedOrAliasedObjectType benchmark%s', PHP_EOL);
printf(' nodes=%d inputs=%d repeat=%d (%d calls)%s', NODES, count($inputs), REPEAT, $totalCalls, PHP_EOL);
printf('%s', PHP_EOL);
printf(' cpu=%.4fs per-call=%.4fus%s', $cpu, $cpu / $totalCalls * 1_000_000, PHP_EOL);
47 changes: 14 additions & 33 deletions rules/TypeDeclaration/PHPStan/ObjectTypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ public function narrowToFullyQualifiedOrAliasedObjectType(
Scope|null $scope,
bool $withPreslash = false
): TypeWithClassName|NonExistingObjectType|UnionType|MixedType|TemplateType {
$className = ltrim($objectType->getClassName(), '\\');
if (str_starts_with($objectType->getClassName(), '\\')) {
$rawClassName = $objectType->getClassName();
$className = ltrim($rawClassName, '\\');
if (str_starts_with($rawClassName, '\\')) {
return new FullyQualifiedObjectType($className);
}

Expand Down Expand Up @@ -87,27 +88,18 @@ public function narrowToFullyQualifiedOrAliasedObjectType(

$classReflection = $scope->getClassReflection();
if ($classReflection instanceof ClassReflection) {
$templateTags = $classReflection->getTemplateTags();
$nameScope = $this->nameScopeFactory->createNameScopeFromNodeWithoutTemplateTypes($node);
$templateTypeScope = $nameScope->getTemplateTypeScope();

if (! $templateTypeScope instanceof TemplateTypeScope) {
// invalid type
return $this->resolveNamespacedNonExistingObjectType($namespaceName, $className, $withPreslash);
}

$currentTemplateTag = $templateTags[$className] ?? null;
if ($currentTemplateTag === null) {
// invalid type
return $this->resolveNamespacedNonExistingObjectType($namespaceName, $className, $withPreslash);
$currentTemplateTag = $classReflection->getTemplateTags()[$className] ?? null;

if ($templateTypeScope instanceof TemplateTypeScope && $currentTemplateTag !== null) {
return TemplateTypeFactory::create(
$templateTypeScope,
$currentTemplateTag->getName(),
$currentTemplateTag->getBound(),
$currentTemplateTag->getVariance()
);
}

return TemplateTypeFactory::create(
$templateTypeScope,
$currentTemplateTag->getName(),
$currentTemplateTag->getBound(),
$currentTemplateTag->getVariance()
);
}
}

Expand All @@ -120,15 +112,7 @@ private function resolveNamespacedNonExistingObjectType(
string $className,
bool $withPreslash
): NonExistingObjectType {
if ($namespacedName === null) {
return new NonExistingObjectType($className);
}

if ($withPreslash) {
return new NonExistingObjectType($className);
}

if (str_contains($className, '\\')) {
if ($namespacedName === null || $withPreslash || str_contains($className, '\\')) {
return new NonExistingObjectType($className);
}

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

$useName = $prefix . $useUse->name->toString();
$alias = $useUse->alias->toString();
$fullyQualifiedName = $prefix . $useUse->name->toString();

$processAliasedObject = $this->processAliasedObject(
$alias,
$className,
$useName,
$fullyQualifiedName
);

Expand All @@ -176,7 +158,6 @@ private function matchAliasedObjectType(ObjectType $objectType, array $uses): ?A
private function processAliasedObject(
string $alias,
string $className,
string $useName,
string $fullyQualifiedName
): ?AliasedObjectType {
// A. is alias in use statement matching this class alias
Expand All @@ -185,7 +166,7 @@ private function processAliasedObject(
}

// B. is aliased classes matching the class name
if ($useName === $className) {
if ($fullyQualifiedName === $className) {
return new AliasedObjectType($className, $fullyQualifiedName);
}

Expand Down
Loading