Skip to content

Commit 8156563

Browse files
committed
Store PhpStorm stub paths of enum methods in the reflection cache relative to the stubs root
1 parent 6ebd2d7 commit 8156563

2 files changed

Lines changed: 200 additions & 19 deletions

File tree

‎src/Reflection/BetterReflection/SourceLocator/CachedPhpInternalSourceLocator.php‎

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier):
9393

9494
$cached = $this->cache->load($cacheKey, $variableCacheKey);
9595
if (is_array($cached)) {
96-
$cached = $this->resolveStubFilename($cached);
96+
$cached = $this->mapStubFilenames($cached, fn (string $filename): ?string => $this->resolveStubFilename($filename));
9797
}
9898
if (is_array($cached)) {
9999
if ($identifier->isConstant()) {
@@ -117,7 +117,7 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier):
117117
|| $reflection instanceof ReflectionFunction
118118
|| $reflection instanceof ReflectionConstant
119119
) {
120-
$exported = $this->relativizeStubFilename($reflection->exportToCache());
120+
$exported = $this->mapStubFilenames($reflection->exportToCache(), fn (string $filename): ?string => $this->relativizeStubFilename($filename));
121121
if ($exported !== null) {
122122
$this->cache->save($cacheKey, $variableCacheKey, $exported);
123123
}
@@ -127,34 +127,71 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier):
127127
}
128128

129129
/**
130-
* @param array<string, mixed> $exported
130+
* Maps the stub path of every located source in the exported blob: the
131+
* reflection's own, and the ones that the methods an enum gets from the
132+
* engine (cases(), from(), tryFrom()) carry next to it.
133+
*
134+
* @param array<string, mixed> $data
135+
* @param callable(string): ?string $mapFilename
136+
* @return array<string, mixed>|null null when any of the paths cannot be mapped
137+
*/
138+
private function mapStubFilenames(array $data, callable $mapFilename): ?array
139+
{
140+
$locatedSource = $this->mapStubFilename($data['locatedSource'] ?? null, $mapFilename);
141+
if ($locatedSource === null) {
142+
return null;
143+
}
144+
$data['locatedSource'] = $locatedSource;
145+
146+
foreach ($data['immediateMethods'] ?? [] as $i => $method) {
147+
if (!isset($method['locatedSource'])) {
148+
continue;
149+
}
150+
151+
$locatedSource = $this->mapStubFilename($method['locatedSource'], $mapFilename);
152+
if ($locatedSource === null) {
153+
return null;
154+
}
155+
$data['immediateMethods'][$i]['locatedSource'] = $locatedSource;
156+
}
157+
158+
return $data;
159+
}
160+
161+
/**
162+
* @param callable(string): ?string $mapFilename
131163
* @return array<string, mixed>|null
132164
*/
133-
private function relativizeStubFilename(array $exported): ?array
165+
private function mapStubFilename(mixed $locatedSource, callable $mapFilename): ?array
134166
{
135-
$filename = $exported['locatedSource']['data']['filename'] ?? null;
167+
$filename = $locatedSource['data']['filename'] ?? null;
136168
if (!is_string($filename)) {
137169
return null;
138170
}
139171

172+
$filename = $mapFilename($filename);
173+
if ($filename === null) {
174+
return null;
175+
}
176+
177+
$locatedSource['data']['filename'] = $filename;
178+
179+
return $locatedSource;
180+
}
181+
182+
private function relativizeStubFilename(string $filename): ?string
183+
{
140184
$markerPosition = strpos($filename, self::STUBS_DIR_MARKER);
141185
if ($markerPosition === false) {
142186
return null;
143187
}
144188

145-
$exported['locatedSource']['data']['filename'] = self::RELATIVE_FILENAME_PREFIX . substr($filename, $markerPosition + strlen(self::STUBS_DIR_MARKER));
146-
147-
return $exported;
189+
return self::RELATIVE_FILENAME_PREFIX . substr($filename, $markerPosition + strlen(self::STUBS_DIR_MARKER));
148190
}
149191

150-
/**
151-
* @param array<string, mixed> $cached
152-
* @return array<string, mixed>|null
153-
*/
154-
private function resolveStubFilename(array $cached): ?array
192+
private function resolveStubFilename(string $filename): ?string
155193
{
156-
$filename = $cached['locatedSource']['data']['filename'] ?? null;
157-
if (!is_string($filename) || !str_starts_with($filename, self::RELATIVE_FILENAME_PREFIX)) {
194+
if (!str_starts_with($filename, self::RELATIVE_FILENAME_PREFIX)) {
158195
return null;
159196
}
160197

@@ -163,9 +200,7 @@ private function resolveStubFilename(array $cached): ?array
163200
return null;
164201
}
165202

166-
$cached['locatedSource']['data']['filename'] = $resolved;
167-
168-
return $cached;
203+
return $resolved;
169204
}
170205

171206
/**
@@ -180,7 +215,7 @@ public function locateIdentifiersByType(Reflector $reflector, IdentifierType $id
180215
private function getVariableCacheKey(): string
181216
{
182217
return $this->variableCacheKey ??= sprintf(
183-
'v3-%s-%s-%s-%s',
218+
'v4-%s-%s-%s-%s',
184219
ComposerHelper::getBetterReflectionVersion(),
185220
ComposerHelper::getPhpStormStubsVersion(),
186221
$this->phpVersion->getVersionString(),
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Reflection\BetterReflection\SourceLocator;
4+
5+
use Override;
6+
use PHPStan\BetterReflection\Identifier\Identifier;
7+
use PHPStan\BetterReflection\Identifier\IdentifierType;
8+
use PHPStan\BetterReflection\Reflection\Reflection;
9+
use PHPStan\BetterReflection\Reflection\ReflectionEnum;
10+
use PHPStan\BetterReflection\Reflector\DefaultReflector;
11+
use PHPStan\BetterReflection\Reflector\Reflector;
12+
use PHPStan\BetterReflection\SourceLocator\Ast\Locator;
13+
use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber;
14+
use PHPStan\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator;
15+
use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator;
16+
use PHPStan\Cache\Cache;
17+
use PHPStan\Cache\CacheStorage;
18+
use PHPStan\Cache\MemoryCacheStorage;
19+
use PHPStan\Php\PhpVersion;
20+
use PHPStan\Reflection\BetterReflection\SourceStubber\ExtensionVersionProvider;
21+
use PHPStan\Testing\PHPStanTestCase;
22+
use function array_walk_recursive;
23+
use function is_string;
24+
use function strpos;
25+
use function strtolower;
26+
use function substr;
27+
28+
class CachedPhpInternalSourceLocatorTest extends PHPStanTestCase
29+
{
30+
31+
public static function getAdditionalConfigFiles(): array
32+
{
33+
return [__DIR__ . '/data/php-8.4.neon'];
34+
}
35+
36+
/**
37+
* The methods an enum gets from the engine (cases(), from(), tryFrom())
38+
* carry their own located source in the cache entry, next to the enum's.
39+
*/
40+
public function testEnumFromCacheFilledByAnotherInstallation(): void
41+
{
42+
$storage = new MemoryCacheStorage();
43+
$this->createReflector($this->createStorageOfAnotherInstallation($storage))->reflectClass('RoundingMode');
44+
45+
$enum = $this->createReflector($storage, 'RoundingMode')->reflectClass('RoundingMode');
46+
$this->assertInstanceOf(ReflectionEnum::class, $enum);
47+
48+
$method = $enum->getMethod('cases');
49+
$this->assertNotNull($method);
50+
51+
$fileName = $method->getFileName();
52+
$this->assertNotNull($fileName);
53+
$this->assertFileExists($fileName);
54+
}
55+
56+
/**
57+
* @param string|null $cachedOnlyClassName a class that the stubs locator does not find, so it has to come from the cache
58+
*/
59+
private function createReflector(CacheStorage $storage, ?string $cachedOnlyClassName = null): Reflector
60+
{
61+
$container = self::getContainer();
62+
$stubber = $container->getByType(PhpStormStubsSourceStubber::class);
63+
64+
$stubsLocator = new PhpInternalSourceLocator(new Locator($container->getService('php8PhpParser')), $stubber);
65+
if ($cachedOnlyClassName !== null) {
66+
$stubsLocator = $this->createLocatorWithoutClass($stubsLocator, $cachedOnlyClassName);
67+
}
68+
69+
return new DefaultReflector(new CachedPhpInternalSourceLocator(
70+
$stubsLocator,
71+
new Cache($storage),
72+
$container->getByType(PhpVersion::class),
73+
$stubber,
74+
$container->getByType(ExtensionVersionProvider::class),
75+
));
76+
}
77+
78+
private function createLocatorWithoutClass(SourceLocator $locator, string $className): SourceLocator
79+
{
80+
return new class ($locator, $className) implements SourceLocator {
81+
82+
public function __construct(private SourceLocator $locator, private string $className)
83+
{
84+
}
85+
86+
#[Override]
87+
public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?Reflection
88+
{
89+
if ($identifier->isClass() && strtolower($identifier->getName()) === strtolower($this->className)) {
90+
return null;
91+
}
92+
93+
return $this->locator->locateIdentifier($reflector, $identifier);
94+
}
95+
96+
#[Override]
97+
public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array
98+
{
99+
return $this->locator->locateIdentifiersByType($reflector, $identifierType);
100+
}
101+
102+
};
103+
}
104+
105+
/**
106+
* Saves the entries with the stub paths of an installation that has since
107+
* been moved or deleted, like a cache directory shared between CI jobs
108+
* that check out the project into different directories.
109+
*/
110+
private function createStorageOfAnotherInstallation(CacheStorage $storage): CacheStorage
111+
{
112+
return new class ($storage) implements CacheStorage {
113+
114+
public function __construct(private CacheStorage $storage)
115+
{
116+
}
117+
118+
#[Override]
119+
public function load(string $key, string $variableKey)
120+
{
121+
return $this->storage->load($key, $variableKey);
122+
}
123+
124+
#[Override]
125+
public function save(string $key, string $variableKey, $data): void
126+
{
127+
array_walk_recursive($data, static function (&$value): void {
128+
if (!is_string($value)) {
129+
return;
130+
}
131+
132+
$markerPosition = strpos($value, '/jetbrains/phpstorm-stubs/');
133+
if ($markerPosition === false) {
134+
return;
135+
}
136+
137+
$value = '/moved-away/vendor' . substr($value, $markerPosition);
138+
});
139+
140+
$this->storage->save($key, $variableKey, $data);
141+
}
142+
143+
};
144+
}
145+
146+
}

0 commit comments

Comments
 (0)