diff --git a/bench/object_type_specifier_narrow_bench.php b/bench/object_type_specifier_narrow_bench.php new file mode 100644 index 00000000000..a5abcb2e38d --- /dev/null +++ b/bench/object_type_specifier_narrow_bench.php @@ -0,0 +1,76 @@ +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); diff --git a/rules/TypeDeclaration/PHPStan/ObjectTypeSpecifier.php b/rules/TypeDeclaration/PHPStan/ObjectTypeSpecifier.php index f96422e6dd0..abc137059a1 100644 --- a/rules/TypeDeclaration/PHPStan/ObjectTypeSpecifier.php +++ b/rules/TypeDeclaration/PHPStan/ObjectTypeSpecifier.php @@ -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); } @@ -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() - ); } } @@ -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); } @@ -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 ); @@ -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 @@ -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); }