Skip to content

Commit 3a187d1

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

2 files changed

Lines changed: 159 additions & 18 deletions

File tree

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

Lines changed: 53 additions & 18 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
/**
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Reflection\BetterReflection\SourceLocator;
4+
5+
use Override;
6+
use PHPStan\BetterReflection\Reflection\ReflectionEnum;
7+
use PHPStan\BetterReflection\Reflector\DefaultReflector;
8+
use PHPStan\BetterReflection\Reflector\Reflector;
9+
use PHPStan\BetterReflection\SourceLocator\Ast\Locator;
10+
use PHPStan\BetterReflection\SourceLocator\SourceStubber\PhpStormStubsSourceStubber;
11+
use PHPStan\BetterReflection\SourceLocator\Type\PhpInternalSourceLocator;
12+
use PHPStan\Cache\Cache;
13+
use PHPStan\Cache\CacheStorage;
14+
use PHPStan\Cache\MemoryCacheStorage;
15+
use PHPStan\Php\PhpVersion;
16+
use PHPStan\Reflection\BetterReflection\SourceStubber\ExtensionVersionProvider;
17+
use PHPStan\Testing\PHPStanTestCase;
18+
use function array_walk_recursive;
19+
use function is_string;
20+
use function strpos;
21+
use function substr;
22+
23+
class CachedPhpInternalSourceLocatorTest extends PHPStanTestCase
24+
{
25+
26+
public static function getAdditionalConfigFiles(): array
27+
{
28+
return [__DIR__ . '/data/php-8.4.neon'];
29+
}
30+
31+
/**
32+
* The methods an enum gets from the engine (cases(), from(), tryFrom())
33+
* carry their own located source in the cache entry, next to the enum's.
34+
*/
35+
public function testEnumFromCacheFilledByAnotherInstallation(): void
36+
{
37+
$storage = new MemoryCacheStorage();
38+
$this->createReflector($this->createStorageOfAnotherInstallation($storage))->reflectClass('RoundingMode');
39+
40+
$enum = $this->createReflector($storage)->reflectClass('RoundingMode');
41+
$this->assertInstanceOf(ReflectionEnum::class, $enum);
42+
43+
$method = $enum->getMethod('cases');
44+
$this->assertNotNull($method);
45+
46+
$fileName = $method->getFileName();
47+
$this->assertNotNull($fileName);
48+
$this->assertFileExists($fileName);
49+
}
50+
51+
private function createReflector(CacheStorage $storage): Reflector
52+
{
53+
$container = self::getContainer();
54+
$stubber = $container->getByType(PhpStormStubsSourceStubber::class);
55+
56+
return new DefaultReflector(new CachedPhpInternalSourceLocator(
57+
new PhpInternalSourceLocator(new Locator($container->getService('php8PhpParser')), $stubber),
58+
new Cache($storage),
59+
$container->getByType(PhpVersion::class),
60+
$stubber,
61+
$container->getByType(ExtensionVersionProvider::class),
62+
));
63+
}
64+
65+
/**
66+
* Saves the entries with the stub paths of an installation that has since
67+
* been moved or deleted, like a cache directory shared between CI jobs
68+
* that check out the project into different directories.
69+
*/
70+
private function createStorageOfAnotherInstallation(CacheStorage $storage): CacheStorage
71+
{
72+
return new class ($storage) implements CacheStorage {
73+
74+
public function __construct(private CacheStorage $storage)
75+
{
76+
}
77+
78+
#[Override]
79+
public function load(string $key, string $variableKey)
80+
{
81+
return $this->storage->load($key, $variableKey);
82+
}
83+
84+
#[Override]
85+
public function save(string $key, string $variableKey, $data): void
86+
{
87+
array_walk_recursive($data, static function (&$value): void {
88+
if (!is_string($value)) {
89+
return;
90+
}
91+
92+
$markerPosition = strpos($value, '/jetbrains/phpstorm-stubs/');
93+
if ($markerPosition === false) {
94+
return;
95+
}
96+
97+
$value = '/moved-away/vendor' . substr($value, $markerPosition);
98+
});
99+
100+
$this->storage->save($key, $variableKey, $data);
101+
}
102+
103+
};
104+
}
105+
106+
}

0 commit comments

Comments
 (0)