From 100c7ae0e157681ba780bdc5ef8c42252e612ee0 Mon Sep 17 00:00:00 2001 From: Jack Worman Date: Thu, 29 Oct 2026 15:21:23 -0500 Subject: [PATCH] Store PhpStorm stub paths of enum methods in the reflection cache relative to the stubs root --- .../CachedPhpInternalSourceLocator.php | 73 ++++++--- .../CachedPhpInternalSourceLocatorTest.php | 146 ++++++++++++++++++ 2 files changed, 200 insertions(+), 19 deletions(-) create mode 100644 tests/PHPStan/Reflection/BetterReflection/SourceLocator/CachedPhpInternalSourceLocatorTest.php diff --git a/src/Reflection/BetterReflection/SourceLocator/CachedPhpInternalSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/CachedPhpInternalSourceLocator.php index 974b68fdf09..9a8aa7e0d56 100644 --- a/src/Reflection/BetterReflection/SourceLocator/CachedPhpInternalSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/CachedPhpInternalSourceLocator.php @@ -93,7 +93,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): $cached = $this->cache->load($cacheKey, $variableCacheKey); if (is_array($cached)) { - $cached = $this->resolveStubFilename($cached); + $cached = $this->mapStubFilenames($cached, fn (string $filename): ?string => $this->resolveStubFilename($filename)); } if (is_array($cached)) { if ($identifier->isConstant()) { @@ -117,7 +117,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): || $reflection instanceof ReflectionFunction || $reflection instanceof ReflectionConstant ) { - $exported = $this->relativizeStubFilename($reflection->exportToCache()); + $exported = $this->mapStubFilenames($reflection->exportToCache(), fn (string $filename): ?string => $this->relativizeStubFilename($filename)); if ($exported !== null) { $this->cache->save($cacheKey, $variableCacheKey, $exported); } @@ -127,34 +127,71 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): } /** - * @param array $exported + * Maps the stub path of every located source in the exported blob: the + * reflection's own, and the ones that the methods an enum gets from the + * engine (cases(), from(), tryFrom()) carry next to it. + * + * @param array $data + * @param callable(string): ?string $mapFilename + * @return array|null null when any of the paths cannot be mapped + */ + private function mapStubFilenames(array $data, callable $mapFilename): ?array + { + $locatedSource = $this->mapStubFilename($data['locatedSource'] ?? null, $mapFilename); + if ($locatedSource === null) { + return null; + } + $data['locatedSource'] = $locatedSource; + + foreach ($data['immediateMethods'] ?? [] as $i => $method) { + if (!isset($method['locatedSource'])) { + continue; + } + + $locatedSource = $this->mapStubFilename($method['locatedSource'], $mapFilename); + if ($locatedSource === null) { + return null; + } + $data['immediateMethods'][$i]['locatedSource'] = $locatedSource; + } + + return $data; + } + + /** + * @param callable(string): ?string $mapFilename * @return array|null */ - private function relativizeStubFilename(array $exported): ?array + private function mapStubFilename(mixed $locatedSource, callable $mapFilename): ?array { - $filename = $exported['locatedSource']['data']['filename'] ?? null; + $filename = $locatedSource['data']['filename'] ?? null; if (!is_string($filename)) { return null; } + $filename = $mapFilename($filename); + if ($filename === null) { + return null; + } + + $locatedSource['data']['filename'] = $filename; + + return $locatedSource; + } + + private function relativizeStubFilename(string $filename): ?string + { $markerPosition = strpos($filename, self::STUBS_DIR_MARKER); if ($markerPosition === false) { return null; } - $exported['locatedSource']['data']['filename'] = self::RELATIVE_FILENAME_PREFIX . substr($filename, $markerPosition + strlen(self::STUBS_DIR_MARKER)); - - return $exported; + return self::RELATIVE_FILENAME_PREFIX . substr($filename, $markerPosition + strlen(self::STUBS_DIR_MARKER)); } - /** - * @param array $cached - * @return array|null - */ - private function resolveStubFilename(array $cached): ?array + private function resolveStubFilename(string $filename): ?string { - $filename = $cached['locatedSource']['data']['filename'] ?? null; - if (!is_string($filename) || !str_starts_with($filename, self::RELATIVE_FILENAME_PREFIX)) { + if (!str_starts_with($filename, self::RELATIVE_FILENAME_PREFIX)) { return null; } @@ -163,9 +200,7 @@ private function resolveStubFilename(array $cached): ?array return null; } - $cached['locatedSource']['data']['filename'] = $resolved; - - return $cached; + return $resolved; } /** @@ -180,7 +215,7 @@ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $id private function getVariableCacheKey(): string { return $this->variableCacheKey ??= sprintf( - 'v3-%s-%s-%s-%s', + 'v4-%s-%s-%s-%s', ComposerHelper::getBetterReflectionVersion(), ComposerHelper::getPhpStormStubsVersion(), $this->phpVersion->getVersionString(), diff --git a/tests/PHPStan/Reflection/BetterReflection/SourceLocator/CachedPhpInternalSourceLocatorTest.php b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/CachedPhpInternalSourceLocatorTest.php new file mode 100644 index 00000000000..151bc027dd8 --- /dev/null +++ b/tests/PHPStan/Reflection/BetterReflection/SourceLocator/CachedPhpInternalSourceLocatorTest.php @@ -0,0 +1,146 @@ +createReflector($this->createStorageOfAnotherInstallation($storage))->reflectClass('RoundingMode'); + + $enum = $this->createReflector($storage, 'RoundingMode')->reflectClass('RoundingMode'); + $this->assertInstanceOf(ReflectionEnum::class, $enum); + + $method = $enum->getMethod('cases'); + $this->assertNotNull($method); + + $fileName = $method->getFileName(); + $this->assertNotNull($fileName); + $this->assertFileExists($fileName); + } + + /** + * @param string|null $cachedOnlyClassName a class that the stubs locator does not find, so it has to come from the cache + */ + private function createReflector(CacheStorage $storage, ?string $cachedOnlyClassName = null): Reflector + { + $container = self::getContainer(); + $stubber = $container->getByType(PhpStormStubsSourceStubber::class); + + $stubsLocator = new PhpInternalSourceLocator(new Locator($container->getService('php8PhpParser')), $stubber); + if ($cachedOnlyClassName !== null) { + $stubsLocator = $this->createLocatorWithoutClass($stubsLocator, $cachedOnlyClassName); + } + + return new DefaultReflector(new CachedPhpInternalSourceLocator( + $stubsLocator, + new Cache($storage), + $container->getByType(PhpVersion::class), + $stubber, + $container->getByType(ExtensionVersionProvider::class), + )); + } + + private function createLocatorWithoutClass(SourceLocator $locator, string $className): SourceLocator + { + return new class ($locator, $className) implements SourceLocator { + + public function __construct(private SourceLocator $locator, private string $className) + { + } + + #[Override] + public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection + { + if ($identifier->isClass() && strtolower($identifier->getName()) === strtolower($this->className)) { + return null; + } + + return $this->locator->locateIdentifier($reflector, $identifier); + } + + #[Override] + public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array + { + return $this->locator->locateIdentifiersByType($reflector, $identifierType); + } + + }; + } + + /** + * Saves the entries with the stub paths of an installation that has since + * been moved or deleted, like a cache directory shared between CI jobs + * that check out the project into different directories. + */ + private function createStorageOfAnotherInstallation(CacheStorage $storage): CacheStorage + { + return new class ($storage) implements CacheStorage { + + public function __construct(private CacheStorage $storage) + { + } + + #[Override] + public function load(string $key, string $variableKey) + { + return $this->storage->load($key, $variableKey); + } + + #[Override] + public function save(string $key, string $variableKey, $data): void + { + array_walk_recursive($data, static function (&$value): void { + if (!is_string($value)) { + return; + } + + $markerPosition = strpos($value, '/jetbrains/phpstorm-stubs/'); + if ($markerPosition === false) { + return; + } + + $value = '/moved-away/vendor' . substr($value, $markerPosition); + }); + + $this->storage->save($key, $variableKey, $data); + } + + }; + } + +}