diff --git a/src/Parser/CachedParser.php b/src/Parser/CachedParser.php index 82193bd4846..818c6a399b1 100644 --- a/src/Parser/CachedParser.php +++ b/src/Parser/CachedParser.php @@ -59,12 +59,12 @@ final class CachedParser implements Parser public function __construct( private Parser $originalParser, private int $cachedNodesByStringCountMax, - private int $cachedSourceBytesMax = self::CACHED_SOURCE_BYTES_DEFAULT_LIMIT, + int $cachedSourceBytesMax = self::CACHED_SOURCE_BYTES_DEFAULT_LIMIT, ) { $this->cachedNodesByString = new LruCache( $this->cachedNodesByStringCountMax, - $this->cachedSourceBytesMax, + $cachedSourceBytesMax, self::SIZE_EVICTION_FLOOR_LIMIT, ); $this->cachedSourceByFile = new LruCache(maxWeight: self::MEMOIZED_SOURCE_BYTES_LIMIT); diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 59bbf690301..3c78aa8624e 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -16,6 +16,7 @@ use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Internal\LruCache; use PHPStan\Parser\Parser; use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; @@ -62,7 +63,6 @@ use PHPStan\Type\TypehintHelper; use PHPStan\Type\UnionType; use function array_key_exists; -use function array_key_first; use function array_keys; use function array_map; use function array_slice; @@ -77,8 +77,8 @@ final class PhpClassReflectionExtension { - /** @var array shared LRU over the member cache keys below; first entry = least recently used */ - private array $memberCacheOrder = []; + /** @var LruCache shared LRU over the member cache keys below */ + private LruCache $memberCacheOrder; /** @var PhpPropertyReflection[][] */ private array $propertiesIncludingAnnotations = []; @@ -118,9 +118,10 @@ public function __construct( private bool $inferPrivatePropertyTypeFromConstructor, private PhpVersion $phpVersion, #[AutowiredParameter(ref: '%cache.memberCacheKeysMax%')] - private int $memberCacheKeysMax, + int $memberCacheKeysMax, ) { + $this->memberCacheOrder = new LruCache($memberCacheKeysMax); } /** @@ -135,25 +136,18 @@ public function __construct( */ private function touchMemberCacheKey(string $cacheKey): void { - if (isset($this->memberCacheOrder[$cacheKey])) { - unset($this->memberCacheOrder[$cacheKey]); - $this->memberCacheOrder[$cacheKey] = true; + if ($this->memberCacheOrder->get($cacheKey) !== null) { return; } - $this->memberCacheOrder[$cacheKey] = true; - if ($this->memberCacheKeysMax === 0 || count($this->memberCacheOrder) <= $this->memberCacheKeysMax) { - return; + foreach ($this->memberCacheOrder->set($cacheKey, true, 0) as $evictKey) { + unset( + $this->methodsIncludingAnnotations[$evictKey], + $this->nativeMethods[$evictKey], + $this->propertiesIncludingAnnotations[$evictKey], + $this->nativeProperties[$evictKey], + ); } - - $evictKey = array_key_first($this->memberCacheOrder); - unset( - $this->memberCacheOrder[$evictKey], - $this->methodsIncludingAnnotations[$evictKey], - $this->nativeMethods[$evictKey], - $this->propertiesIncludingAnnotations[$evictKey], - $this->nativeProperties[$evictKey], - ); } public function hasProperty(ClassReflection $classReflection, string $propertyName): bool diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index e57f329f53c..3ccd03e06c0 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -15,6 +15,7 @@ use PHPStan\File\FileContentHasher; use PHPStan\File\FileHelper; use PHPStan\Internal\ComposerHelper; +use PHPStan\Internal\LruCache; use PHPStan\Parser\Parser; use PHPStan\PhpDoc\NameScopeAlreadyBeingCreatedException; use PHPStan\PhpDoc\PhpDocNodeResolver; @@ -58,10 +59,8 @@ final class FileTypeMapper private const SKIP_NODE = 1; private const POP_TYPE_MAP_STACK = 2; - /** @var array}> */ - private array $memoryCache = []; - - private int $memoryCacheCount = 0; + /** @var LruCache}> */ + private LruCache $memoryCache; /** @var array */ private array $inProcess = []; @@ -87,9 +86,13 @@ public function __construct( #[AutowiredParameter(ref: '%cache.resolvedPhpDocBlockCacheCountMax%')] private int $resolvedPhpDocBlockCacheCountMax, #[AutowiredParameter(ref: '%cache.nameScopeMapMemoryCacheCountMax%')] - private int $nameScopeMapMemoryCacheCountMax, + int $nameScopeMapMemoryCacheCountMax, ) { + // 0 kept one entry here rather than meaning "no limit" as it does for the other bounded + // caches: the eviction loop ran before the insertion, emptying the cache and then putting + // a single entry back. Preserved rather than normalised - see the PR description. + $this->memoryCache = new LruCache($nameScopeMapMemoryCacheCountMax === 0 ? 1 : $nameScopeMapMemoryCacheCountMax); } /** @api */ @@ -336,13 +339,8 @@ public function getNameScope( */ private function getNameScopeMap(string $fileName): array { - if (isset($this->memoryCache[$fileName])) { - // LRU: move the freshly-accessed entry to the end so eviction drops - // genuinely cold files, not hot dependencies inserted early on. - $cachedEntry = $this->memoryCache[$fileName]; - unset($this->memoryCache[$fileName]); - $this->memoryCache[$fileName] = $cachedEntry; - + $cachedEntry = $this->memoryCache->get($fileName); + if ($cachedEntry !== null) { return $cachedEntry; } @@ -364,19 +362,10 @@ private function getNameScopeMap(string $fileName): array } else { [$nameScopeMap] = $cached; } - while ($this->memoryCacheCount >= $this->nameScopeMapMemoryCacheCountMax) { - $oldestKey = array_key_first($this->memoryCache); - if ($oldestKey === null) { - break; - } - unset($this->memoryCache[$oldestKey]); - $this->memoryCacheCount--; - } - - $this->memoryCache[$fileName] = [$nameScopeMap]; - $this->memoryCacheCount++; + $entry = [$nameScopeMap]; + $this->memoryCache->set($fileName, $entry, 0); - return $this->memoryCache[$fileName]; + return $entry; } /** diff --git a/src/Type/UsefulTypeAliasResolver.php b/src/Type/UsefulTypeAliasResolver.php index ed745d63606..d35864b3a1a 100644 --- a/src/Type/UsefulTypeAliasResolver.php +++ b/src/Type/UsefulTypeAliasResolver.php @@ -5,13 +5,12 @@ use PHPStan\Analyser\NameScope; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Internal\LruCache; use PHPStan\PhpDoc\TypeNodeResolver; use PHPStan\PhpDoc\TypeStringResolver; use PHPStan\Reflection\ReflectionProvider; use PHPStan\ShouldNotHappenException; use function array_key_exists; -use function array_key_first; -use function count; use function sprintf; #[AutowiredService(as: TypeAliasResolver::class)] @@ -21,8 +20,8 @@ final class UsefulTypeAliasResolver implements TypeAliasResolver /** @var array */ private array $resolvedGlobalTypeAliases = []; - /** @var array LRU; first entry = least recently used */ - private array $resolvedLocalTypeAliases = []; + /** @var LruCache */ + private LruCache $resolvedLocalTypeAliases; /** @var array */ private array $resolvingClassTypeAliases = []; @@ -40,9 +39,10 @@ public function __construct( private TypeNodeResolver $typeNodeResolver, private ReflectionProvider $reflectionProvider, #[AutowiredParameter(ref: '%cache.resolvedLocalTypeAliasesCountMax%')] - private int $resolvedLocalTypeAliasesCountMax, + int $resolvedLocalTypeAliasesCountMax, ) { + $this->resolvedLocalTypeAliases = new LruCache($resolvedLocalTypeAliasesCountMax); } public function hasTypeAlias(string $aliasName, ?string $classNameScope): bool @@ -84,12 +84,9 @@ private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope): $aliasNameInClassScope = $className . '::' . $aliasName; - if (array_key_exists($aliasNameInClassScope, $this->resolvedLocalTypeAliases)) { - // LRU: move to the most-recently-used position - $resolvedAliasType = $this->resolvedLocalTypeAliases[$aliasNameInClassScope]; - unset($this->resolvedLocalTypeAliases[$aliasNameInClassScope]); - - return $this->resolvedLocalTypeAliases[$aliasNameInClassScope] = $resolvedAliasType; + $resolvedAliasType = $this->resolvedLocalTypeAliases->get($aliasNameInClassScope); + if ($resolvedAliasType !== null) { + return $resolvedAliasType; } // prevent infinite recursion @@ -127,12 +124,9 @@ private function resolveLocalTypeAlias(string $aliasName, NameScope $nameScope): $resolvedAliasType = new CircularTypeAliasErrorType(); } - $this->resolvedLocalTypeAliases[$aliasNameInClassScope] = $resolvedAliasType; - if ($this->resolvedLocalTypeAliasesCountMax !== 0 && count($this->resolvedLocalTypeAliases) > $this->resolvedLocalTypeAliasesCountMax) { - // resolved alias types transitively pin ClassReflections and their whole - // reflection trees — evict the least recently used - unset($this->resolvedLocalTypeAliases[array_key_first($this->resolvedLocalTypeAliases)]); - } + // resolved alias types transitively pin ClassReflections and their whole + // reflection trees, so the cache is bounded and evicts the least recently used + $this->resolvedLocalTypeAliases->set($aliasNameInClassScope, $resolvedAliasType, 0); unset($this->inProcess[$aliasNameInClassScope]); return $resolvedAliasType;