diff --git a/src/Internal/ComposerHelper.php b/src/Internal/ComposerHelper.php index 6a5f518197..f380d892ff 100644 --- a/src/Internal/ComposerHelper.php +++ b/src/Internal/ComposerHelper.php @@ -23,6 +23,8 @@ final class ComposerHelper private static ?string $betterReflectionVersion = null; + private static ?string $phpstormStubsVersion = null; + private static ?string $phpDocParserVersion = null; /** @var array */ @@ -124,6 +126,21 @@ public static function getBetterReflectionVersion(): string return self::$betterReflectionVersion = self::processPackageVersion($rootPackage); } + public static function getPhpStormStubsVersion(): string + { + if (self::$phpstormStubsVersion !== null) { + return self::$phpstormStubsVersion; + } + + $installed = self::getInstalled(); + $rootPackage = $installed['versions']['jetbrains/phpstorm-stubs'] ?? null; + if ($rootPackage === null) { + return self::$phpstormStubsVersion = self::UNKNOWN_VERSION; + } + + return self::$phpstormStubsVersion = self::processPackageVersion($rootPackage); + } + public static function getPhpDocParserVersion(): string { if (self::$phpDocParserVersion !== null) { diff --git a/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php b/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php index ddffaffc89..f2c367d9a5 100644 --- a/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php +++ b/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php @@ -13,12 +13,15 @@ use PHPStan\BetterReflection\SourceLocator\Type\MemoizingSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator; use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator; +use PHPStan\Cache\Cache; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Internal\ComposerHelper; use PHPStan\Php\PhpVersion; use PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadFunctionsSourceLocator; use PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator; use PHPStan\Reflection\BetterReflection\SourceLocator\ComposerJsonAndInstalledJsonSourceLocatorMaker; +use PHPStan\Reflection\BetterReflection\SourceLocator\FileCachedSourceLocator; use PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher; use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedDirectorySourceLocatorRepository; use PHPStan\Reflection\BetterReflection\SourceLocator\OptimizedPsrAutoloaderLocatorFactory; @@ -34,6 +37,7 @@ use function extension_loaded; use function is_dir; use function is_file; +use function sprintf; use const PHP_VERSION_ID; #[AutowiredService] @@ -74,6 +78,7 @@ public function __construct( private bool $playgroundMode, // makes all PHPStan classes in the PHAR discoverable with PSR-4 #[AutowiredParameter] private ?string $singleReflectionFile, + private Cache $cache, ) { } @@ -161,8 +166,17 @@ public function create(): SourceLocator } } + $phpstormStubsVersion = ComposerHelper::getPhpStormStubsVersion(); + $locators[] = new RewriteClassAliasSourceLocator(new AggregateSourceLocator($fileLocators)); - $locators[] = new SkipClassAliasSourceLocator(new PhpInternalSourceLocator($astPhp8Locator, $this->phpstormStubsSourceStubber)); + $locators[] = new SkipClassAliasSourceLocator( + new FileCachedSourceLocator( + new PhpInternalSourceLocator($astPhp8Locator, $this->phpstormStubsSourceStubber), + $this->cache, + $this->phpVersion, + sprintf('phpstorm-stubs-php8-%s', $phpstormStubsVersion), + ), + ); $locators[] = new AutoloadSourceLocator($this->fileNodesFetcher, true); $locators[] = new PhpVersionBlacklistSourceLocator(new PhpInternalSourceLocator($astLocator, $this->reflectionSourceStubber), $this->phpstormStubsSourceStubber); diff --git a/src/Reflection/BetterReflection/SourceLocator/FileCachedSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/FileCachedSourceLocator.php new file mode 100644 index 0000000000..ad7a9dbe93 --- /dev/null +++ b/src/Reflection/BetterReflection/SourceLocator/FileCachedSourceLocator.php @@ -0,0 +1,184 @@ +, functions: array, constants: array} */ + private array $cachedSymbols = ['classes' => [], 'functions' => [], 'constants' => []]; + + /** + * @param non-empty-string $cacheKey + */ + public function __construct( + private SourceLocator $locator, + private Cache $cache, + private PhpVersion $phpVersion, + private string $cacheKey, + ) + { + } + + #[Override] + public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection + { + if ($identifier->isClass()) { + $className = strtolower($identifier->getName()); + + if (!array_key_exists($className, $this->cachedSymbols['classes'])) { + $result = $this->loadCache($identifier, $reflector); + if ($result === self::NOT_FOUND) { + $result = $this->locator->locateIdentifier($reflector, $identifier); + $this->storeCache($identifier, $result); + } elseif ($result === self::NULL_CACHED) { + $result = null; + } + $this->cachedSymbols['classes'][$className] = $result; + } + return $this->cachedSymbols['classes'][$className]; + } + + if ($identifier->isFunction()) { + $className = strtolower($identifier->getName()); + + if (!array_key_exists($className, $this->cachedSymbols['functions'])) { + $result = $this->loadCache($identifier, $reflector); + if ($result === self::NOT_FOUND) { + $result = $this->locator->locateIdentifier($reflector, $identifier); + $this->storeCache($identifier, $result); + } elseif ($result === self::NULL_CACHED) { + $result = null; + } + $this->cachedSymbols['functions'][$className] = $result; + } + return $this->cachedSymbols['functions'][$className]; + } + + if ($identifier->isConstant()) { + $constantName = ConstantNameHelper::normalize($identifier->getName()); + + if (!array_key_exists($constantName, $this->cachedSymbols['constants'])) { + $result = $this->loadCache($identifier, $reflector); + if ($result === self::NOT_FOUND) { + $result = $this->locator->locateIdentifier($reflector, $identifier); + $this->storeCache($identifier, $result); + } elseif ($result === self::NULL_CACHED) { + $result = null; + } + $this->cachedSymbols['constants'][$constantName] = $result; + } + return $this->cachedSymbols['constants'][$constantName]; + } + + return null; + } + + #[Override] + public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array + { + return $this->locator->locateIdentifiersByType($reflector, $identifierType); + } + + private function loadCache( + Identifier $identifier, + Reflector $reflector, + ): ReflectionClass|ReflectionFunction|ReflectionConstant|true|null + { + [$cacheKey, $variableCacheKey] = $this->getCacheKeys($identifier); + $cachedReflection = $this->cache->load( + $cacheKey, + $variableCacheKey, + ); + + if ($cachedReflection === self::NOT_FOUND) { + return self::NOT_FOUND; + } + if ($cachedReflection === self::NULL_CACHED) { + return self::NULL_CACHED; + } + + if ($identifier->isClass()) { + if (array_key_exists('backingType', $cachedReflection)) { + return ReflectionEnum::importFromCache($reflector, $cachedReflection); + } + + return ReflectionClass::importFromCache($reflector, $cachedReflection); + } + + if ($identifier->isFunction()) { + return ReflectionFunction::importFromCache($reflector, $cachedReflection); + } + + return ReflectionConstant::importFromCache($reflector, $cachedReflection); + } + + private function storeCache( + Identifier $identifier, + Reflection|null $reflection, + ): void + { + [$cacheKey, $variableCacheKey] = $this->getCacheKeys($identifier); + + $exported = self::NULL_CACHED; + if ($reflection !== null) { + if ( + !$reflection instanceof ReflectionClass + && !$reflection instanceof ReflectionFunction + && !$reflection instanceof ReflectionConstant + ) { + throw new ShouldNotHappenException(); + } + + $exported = $reflection->exportToCache(); + } + + $this->cache->save( + $cacheKey, + $variableCacheKey, + $exported, + ); + } + + /** @return array{non-empty-string, non-empty-string} */ + private function getCacheKeys(Identifier $identifier): array + { + $suffix = $this->getCacheKeySuffix($identifier); + + return [ + sprintf('%s-%s', $this->cacheKey, $suffix), + sprintf('v3-%s-%s-%s', ComposerHelper::getBetterReflectionVersion(), $this->phpVersion->getVersionString(), $suffix), + ]; + } + + /** @return non-empty-string */ + private function getCacheKeySuffix(Identifier $identifier): string + { + $identifierHash = hash('sha256', $identifier->getName()); + return sprintf('%s-%s', $identifier->getType()->getName(), $identifierHash); + } + +}