From 1b9bb479cc5ac2d590587613e36609d39f2a08a5 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Fri, 6 Nov 2026 19:16:53 +0000 Subject: [PATCH] Remember annotation method and property lookups that find nothing The lookup recurses into the parent class and every interface, and getInterfaces() already includes the inherited ones, so the same ancestors are reached over and over. Only hits were cached, so a method without a @method tag anywhere in a wide, deep hierarchy was searched along every path again. Amp-Thread-ID: https://ampcode.com/threads/T-01a1ad6c-dec2-72ff-bbe8-5b3b62746c94 --- ...tationsMethodsClassReflectionExtension.php | 37 +++++++++++++------ ...ionsPropertiesClassReflectionExtension.php | 37 +++++++++++++------ 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php index efbf6423255..91fdbe77156 100644 --- a/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php @@ -7,12 +7,14 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedMethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Generic\TemplateTypeFactory; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeScope; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Type; +use function array_key_exists; use function array_map; use function count; @@ -21,25 +23,36 @@ final class AnnotationsMethodsClassReflectionExtension implements MethodsClassReflectionExtension { - /** @var ExtendedMethodReflection[][] */ + /** @var array> */ private array $methods = []; public function hasMethod(ClassReflection $classReflection, string $methodName): bool { - if (!isset($this->methods[$classReflection->getCacheKey()][$methodName])) { - $method = $this->findClassReflectionWithMethod($classReflection, $classReflection, $methodName); - if ($method === null) { - return false; - } - $this->methods[$classReflection->getCacheKey()][$methodName] = $method; + return $this->findMethod($classReflection, $methodName) !== null; + } + + public function getMethod(ClassReflection $classReflection, string $methodName): ExtendedMethodReflection + { + $method = $this->findMethod($classReflection, $methodName); + if ($method === null) { + throw new ShouldNotHappenException(); } - return true; + return $method; } - public function getMethod(ClassReflection $classReflection, string $methodName): ExtendedMethodReflection + /** + * Remembers misses too: the lookup recurses into the parent and every + * interface, which reach the same ancestors many times over. + */ + private function findMethod(ClassReflection $classReflection, string $methodName): ?ExtendedMethodReflection { - return $this->methods[$classReflection->getCacheKey()][$methodName]; + $cacheKey = $classReflection->getCacheKey(); + if (!array_key_exists($methodName, $this->methods[$cacheKey] ?? [])) { + $this->methods[$cacheKey][$methodName] = $this->findClassReflectionWithMethod($classReflection, $classReflection, $methodName); + } + + return $this->methods[$cacheKey][$methodName]; } private function findClassReflectionWithMethod( @@ -102,14 +115,14 @@ private function findClassReflectionWithMethod( $parentClass = $classReflection->getParentClass(); if ($parentClass !== null) { - $methodWithDeclaringClass = $this->findClassReflectionWithMethod($parentClass, $parentClass, $methodName); + $methodWithDeclaringClass = $this->findMethod($parentClass, $methodName); if ($methodWithDeclaringClass !== null) { return $methodWithDeclaringClass; } } foreach ($classReflection->getInterfaces() as $interfaceClass) { - $methodWithDeclaringClass = $this->findClassReflectionWithMethod($interfaceClass, $interfaceClass, $methodName); + $methodWithDeclaringClass = $this->findMethod($interfaceClass, $methodName); if ($methodWithDeclaringClass === null) { continue; } diff --git a/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php b/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php index f51577a281d..5b9183bfff4 100644 --- a/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php +++ b/src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php @@ -6,34 +6,47 @@ use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ExtendedPropertyReflection; use PHPStan\Reflection\PropertiesClassReflectionExtension; +use PHPStan\ShouldNotHappenException; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\NeverType; +use function array_key_exists; // autoTag: false - wired explicitly in ClassReflectionExtensionRegistry, must not be tagged #[AutowiredService(autoTag: false)] final class AnnotationsPropertiesClassReflectionExtension implements PropertiesClassReflectionExtension { - /** @var ExtendedPropertyReflection[][] */ + /** @var array> */ private array $properties = []; public function hasProperty(ClassReflection $classReflection, string $propertyName): bool { - if (!isset($this->properties[$classReflection->getCacheKey()][$propertyName])) { - $property = $this->findClassReflectionWithProperty($classReflection, $classReflection, $propertyName); - if ($property === null) { - return false; - } - $this->properties[$classReflection->getCacheKey()][$propertyName] = $property; + return $this->findProperty($classReflection, $propertyName) !== null; + } + + public function getProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection + { + $property = $this->findProperty($classReflection, $propertyName); + if ($property === null) { + throw new ShouldNotHappenException(); } - return true; + return $property; } - public function getProperty(ClassReflection $classReflection, string $propertyName): ExtendedPropertyReflection + /** + * Remembers misses too: the lookup recurses into the parent and every + * interface, which reach the same ancestors many times over. + */ + private function findProperty(ClassReflection $classReflection, string $propertyName): ?ExtendedPropertyReflection { - return $this->properties[$classReflection->getCacheKey()][$propertyName]; + $cacheKey = $classReflection->getCacheKey(); + if (!array_key_exists($propertyName, $this->properties[$cacheKey] ?? [])) { + $this->properties[$cacheKey][$propertyName] = $this->findClassReflectionWithProperty($classReflection, $classReflection, $propertyName); + } + + return $this->properties[$cacheKey][$propertyName]; } private function findClassReflectionWithProperty( @@ -87,14 +100,14 @@ private function findClassReflectionWithProperty( $parentClass = $classReflection->getParentClass(); if ($parentClass !== null) { - $methodWithDeclaringClass = $this->findClassReflectionWithProperty($parentClass, $parentClass, $propertyName); + $methodWithDeclaringClass = $this->findProperty($parentClass, $propertyName); if ($methodWithDeclaringClass !== null) { return $methodWithDeclaringClass; } } foreach ($classReflection->getInterfaces() as $interfaceClass) { - $methodWithDeclaringClass = $this->findClassReflectionWithProperty($interfaceClass, $interfaceClass, $propertyName); + $methodWithDeclaringClass = $this->findProperty($interfaceClass, $propertyName); if ($methodWithDeclaringClass === null) { continue; }