Skip to content

Commit 1e53ab6

Browse files
committed
Result cache - invalidate when project extensions are edited
1 parent 775c3f3 commit 1e53ab6

File tree

1 file changed

+151
-4
lines changed

1 file changed

+151
-4
lines changed

src/Analyser/ResultCache/ResultCacheManager.php

+151-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Analyser\ResultCache;
44

5+
use Nette\DI\Definitions\Statement;
56
use Nette\Neon\Neon;
67
use PHPStan\Analyser\AnalyserResult;
78
use PHPStan\Analyser\Error;
@@ -11,18 +12,21 @@
1112
use PHPStan\File\FileFinder;
1213
use PHPStan\File\FileReader;
1314
use PHPStan\File\FileWriter;
15+
use PHPStan\Reflection\ReflectionProvider;
1416
use function array_fill_keys;
1517
use function array_key_exists;
1618

1719
class ResultCacheManager
1820
{
1921

20-
private const CACHE_VERSION = 'v8-executed-hash';
22+
private const CACHE_VERSION = 'v9-project-extensions';
2123

2224
private ExportedNodeFetcher $exportedNodeFetcher;
2325

2426
private FileFinder $scanFileFinder;
2527

28+
private ReflectionProvider $reflectionProvider;
29+
2630
private string $cacheFilePath;
2731

2832
private string $tempResultCachePath;
@@ -55,9 +59,13 @@ class ResultCacheManager
5559
/** @var array<string, string> */
5660
private array $fileReplacements = [];
5761

62+
/** @var array<string, true> */
63+
private array $alreadyProcessed = [];
64+
5865
/**
5966
* @param ExportedNodeFetcher $exportedNodeFetcher
6067
* @param FileFinder $scanFileFinder
68+
* @param ReflectionProvider $reflectionProvider
6169
* @param string $cacheFilePath
6270
* @param string $tempResultCachePath
6371
* @param string[] $analysedPaths
@@ -73,6 +81,7 @@ class ResultCacheManager
7381
public function __construct(
7482
ExportedNodeFetcher $exportedNodeFetcher,
7583
FileFinder $scanFileFinder,
84+
ReflectionProvider $reflectionProvider,
7685
string $cacheFilePath,
7786
string $tempResultCachePath,
7887
array $analysedPaths,
@@ -88,6 +97,7 @@ public function __construct(
8897
{
8998
$this->exportedNodeFetcher = $exportedNodeFetcher;
9099
$this->scanFileFinder = $scanFileFinder;
100+
$this->reflectionProvider = $reflectionProvider;
91101
$this->cacheFilePath = $cacheFilePath;
92102
$this->tempResultCachePath = $tempResultCachePath;
93103
$this->analysedPaths = $analysedPaths;
@@ -159,7 +169,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
159169
}
160170

161171
$meta = $this->getMeta($allAnalysedFiles, $projectConfigArray);
162-
if ($data['meta'] !== $meta) {
172+
if ($this->isMetaDifferent($data['meta'], $meta)) {
163173
if ($output->isDebug()) {
164174
$output->writeLineFormatted('Result cache not used because the metadata do not match.');
165175
}
@@ -174,6 +184,25 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
174184
return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], []);
175185
}
176186

187+
foreach ($data['projectExtensionFiles'] as $extensionFile => $fileHash) {
188+
if (!is_file($extensionFile)) {
189+
if ($output->isDebug()) {
190+
$output->writeLineFormatted(sprintf('Result cache not used because extension file %s was not found.', $extensionFile));
191+
}
192+
return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], []);
193+
}
194+
195+
if ($this->getFileHash($extensionFile) === $fileHash) {
196+
continue;
197+
}
198+
199+
if ($output->isDebug()) {
200+
$output->writeLineFormatted(sprintf('Result cache not used because extension file %s hash does not match.', $extensionFile));
201+
}
202+
203+
return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], []);
204+
}
205+
177206
$invertedDependencies = $data['dependencies'];
178207
$deletedFiles = array_fill_keys(array_keys($invertedDependencies), true);
179208
$filesToAnalyse = [];
@@ -254,6 +283,21 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
254283
return new ResultCache(array_unique($filesToAnalyse), false, $data['lastFullAnalysisTime'], $meta, $filteredErrors, $invertedDependenciesToReturn, $filteredExportedNodes);
255284
}
256285

286+
/**
287+
* @param mixed[] $cachedMeta
288+
* @param mixed[] $currentMeta
289+
* @return bool
290+
*/
291+
private function isMetaDifferent(array $cachedMeta, array $currentMeta): bool
292+
{
293+
$projectConfig = $currentMeta['projectConfig'];
294+
if ($projectConfig !== null) {
295+
$currentMeta['projectConfig'] = Neon::encode($currentMeta['projectConfig']);
296+
}
297+
298+
return $cachedMeta !== $currentMeta;
299+
}
300+
257301
/**
258302
* @param string $analysedFile
259303
* @param array<int, ExportedNode> $cachedFileExportedNodes
@@ -520,6 +564,7 @@ private function save(
520564
return [
521565
'lastFullAnalysisTime' => %s,
522566
'meta' => %s,
567+
'projectExtensionFiles' => %s,
523568
'errorsCallback' => static function (): array { return %s; },
524569
'dependencies' => %s,
525570
'exportedNodesCallback' => static function (): array { return %s; },
@@ -533,19 +578,123 @@ private function save(
533578
$file = $this->tempResultCachePath . '/' . $resultCacheName . '.php';
534579
}
535580

581+
$projectConfigArray = $meta['projectConfig'];
582+
if ($projectConfigArray !== null) {
583+
$meta['projectConfig'] = Neon::encode($projectConfigArray);
584+
}
585+
536586
FileWriter::write(
537587
$file,
538588
sprintf(
539589
$template,
540590
var_export($lastFullAnalysisTime, true),
541591
var_export($meta, true),
592+
var_export($this->getProjectExtensionFiles($projectConfigArray, $dependencies), true),
542593
var_export($errors, true),
543594
var_export($invertedDependencies, true),
544595
var_export($exportedNodes, true)
545596
)
546597
);
547598
}
548599

600+
/**
601+
* @param mixed[]|null $projectConfig
602+
* @param array<string, mixed> $dependencies
603+
* @return array<string, string>
604+
*/
605+
private function getProjectExtensionFiles(?array $projectConfig, array $dependencies): array
606+
{
607+
$this->alreadyProcessed = [];
608+
$projectExtensionFiles = [];
609+
if ($projectConfig !== null) {
610+
$services = $projectConfig['services'] ?? [];
611+
foreach ($services as $service) {
612+
$classes = $this->getClassesFromConfigDefinition($service);
613+
if (is_array($service)) {
614+
foreach (['class', 'factory', 'implement'] as $key) {
615+
if (!isset($service[$key])) {
616+
continue;
617+
}
618+
619+
$classes = array_merge($classes, $this->getClassesFromConfigDefinition($service[$key]));
620+
}
621+
}
622+
623+
foreach (array_unique($classes) as $class) {
624+
if (!$this->reflectionProvider->hasClass($class)) {
625+
continue;
626+
}
627+
628+
$classReflection = $this->reflectionProvider->getClass($class);
629+
$fileName = $classReflection->getFileName();
630+
if ($fileName === false) {
631+
continue;
632+
}
633+
634+
$allServiceFiles = $this->getAllDependencies($fileName, $dependencies);
635+
foreach ($allServiceFiles as $serviceFile) {
636+
if (array_key_exists($serviceFile, $projectExtensionFiles)) {
637+
continue;
638+
}
639+
640+
$projectExtensionFiles[$serviceFile] = $this->getFileHash($serviceFile);
641+
}
642+
}
643+
}
644+
}
645+
646+
return $projectExtensionFiles;
647+
}
648+
649+
/**
650+
* @param mixed $definition
651+
* @return string[]
652+
*/
653+
private function getClassesFromConfigDefinition($definition): array
654+
{
655+
if (is_string($definition)) {
656+
return [$definition];
657+
}
658+
659+
if ($definition instanceof Statement) {
660+
$entity = $definition->entity;
661+
if (is_string($entity)) {
662+
return [$entity];
663+
} elseif (is_array($entity) && isset($entity[0]) && is_string($entity[0])) {
664+
return [$entity[0]];
665+
}
666+
}
667+
668+
return [];
669+
}
670+
671+
/**
672+
* @param string $fileName
673+
* @param array<string, array<int, string>> $dependencies
674+
* @return array<int, string>
675+
*/
676+
private function getAllDependencies(string $fileName, array $dependencies): array
677+
{
678+
if (!array_key_exists($fileName, $dependencies)) {
679+
return [];
680+
}
681+
682+
if (array_key_exists($fileName, $this->alreadyProcessed)) {
683+
return [];
684+
}
685+
686+
$this->alreadyProcessed[$fileName] = true;
687+
688+
$files = [$fileName];
689+
foreach ($dependencies[$fileName] as $fileDep) {
690+
foreach ($this->getAllDependencies($fileDep, $dependencies) as $fileDep2) {
691+
$files[] = $fileDep2;
692+
}
693+
}
694+
695+
return $files;
696+
}
697+
549698
/**
550699
* @param string[] $allAnalysedFiles
551700
* @param mixed[]|null $projectConfigArray
@@ -567,8 +716,6 @@ private function getMeta(array $allAnalysedFiles, ?array $projectConfigArray): a
567716
unset($projectConfigArray['parameters']['reportUnmatchedIgnoredErrors']);
568717
unset($projectConfigArray['parameters']['memoryLimitFile']);
569718
unset($projectConfigArray['parametersSchema']);
570-
571-
$projectConfigArray = Neon::encode($projectConfigArray);
572719
}
573720

574721
return [

0 commit comments

Comments
 (0)