Skip to content

Commit

Permalink
Result cache - invalidate when project extensions are edited
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Mar 18, 2021
1 parent 775c3f3 commit 1e53ab6
Showing 1 changed file with 151 additions and 4 deletions.
155 changes: 151 additions & 4 deletions src/Analyser/ResultCache/ResultCacheManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Analyser\ResultCache;

use Nette\DI\Definitions\Statement;
use Nette\Neon\Neon;
use PHPStan\Analyser\AnalyserResult;
use PHPStan\Analyser\Error;
Expand All @@ -11,18 +12,21 @@
use PHPStan\File\FileFinder;
use PHPStan\File\FileReader;
use PHPStan\File\FileWriter;
use PHPStan\Reflection\ReflectionProvider;
use function array_fill_keys;
use function array_key_exists;

class ResultCacheManager
{

private const CACHE_VERSION = 'v8-executed-hash';
private const CACHE_VERSION = 'v9-project-extensions';

private ExportedNodeFetcher $exportedNodeFetcher;

private FileFinder $scanFileFinder;

private ReflectionProvider $reflectionProvider;

private string $cacheFilePath;

private string $tempResultCachePath;
Expand Down Expand Up @@ -55,9 +59,13 @@ class ResultCacheManager
/** @var array<string, string> */
private array $fileReplacements = [];

/** @var array<string, true> */
private array $alreadyProcessed = [];

/**
* @param ExportedNodeFetcher $exportedNodeFetcher
* @param FileFinder $scanFileFinder
* @param ReflectionProvider $reflectionProvider
* @param string $cacheFilePath
* @param string $tempResultCachePath
* @param string[] $analysedPaths
Expand All @@ -73,6 +81,7 @@ class ResultCacheManager
public function __construct(
ExportedNodeFetcher $exportedNodeFetcher,
FileFinder $scanFileFinder,
ReflectionProvider $reflectionProvider,
string $cacheFilePath,
string $tempResultCachePath,
array $analysedPaths,
Expand All @@ -88,6 +97,7 @@ public function __construct(
{
$this->exportedNodeFetcher = $exportedNodeFetcher;
$this->scanFileFinder = $scanFileFinder;
$this->reflectionProvider = $reflectionProvider;
$this->cacheFilePath = $cacheFilePath;
$this->tempResultCachePath = $tempResultCachePath;
$this->analysedPaths = $analysedPaths;
Expand Down Expand Up @@ -159,7 +169,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
}

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

foreach ($data['projectExtensionFiles'] as $extensionFile => $fileHash) {
if (!is_file($extensionFile)) {
if ($output->isDebug()) {
$output->writeLineFormatted(sprintf('Result cache not used because extension file %s was not found.', $extensionFile));
}
return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], []);
}

if ($this->getFileHash($extensionFile) === $fileHash) {
continue;
}

if ($output->isDebug()) {
$output->writeLineFormatted(sprintf('Result cache not used because extension file %s hash does not match.', $extensionFile));
}

return new ResultCache($allAnalysedFiles, true, time(), $meta, [], [], []);
}

$invertedDependencies = $data['dependencies'];
$deletedFiles = array_fill_keys(array_keys($invertedDependencies), true);
$filesToAnalyse = [];
Expand Down Expand Up @@ -254,6 +283,21 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
return new ResultCache(array_unique($filesToAnalyse), false, $data['lastFullAnalysisTime'], $meta, $filteredErrors, $invertedDependenciesToReturn, $filteredExportedNodes);
}

/**
* @param mixed[] $cachedMeta
* @param mixed[] $currentMeta
* @return bool
*/
private function isMetaDifferent(array $cachedMeta, array $currentMeta): bool
{
$projectConfig = $currentMeta['projectConfig'];
if ($projectConfig !== null) {
$currentMeta['projectConfig'] = Neon::encode($currentMeta['projectConfig']);
}

return $cachedMeta !== $currentMeta;
}

/**
* @param string $analysedFile
* @param array<int, ExportedNode> $cachedFileExportedNodes
Expand Down Expand Up @@ -520,6 +564,7 @@ private function save(
return [
'lastFullAnalysisTime' => %s,
'meta' => %s,
'projectExtensionFiles' => %s,
'errorsCallback' => static function (): array { return %s; },
'dependencies' => %s,
'exportedNodesCallback' => static function (): array { return %s; },
Expand All @@ -533,19 +578,123 @@ private function save(
$file = $this->tempResultCachePath . '/' . $resultCacheName . '.php';
}

$projectConfigArray = $meta['projectConfig'];
if ($projectConfigArray !== null) {
$meta['projectConfig'] = Neon::encode($projectConfigArray);
}

FileWriter::write(
$file,
sprintf(
$template,
var_export($lastFullAnalysisTime, true),
var_export($meta, true),
var_export($this->getProjectExtensionFiles($projectConfigArray, $dependencies), true),
var_export($errors, true),
var_export($invertedDependencies, true),
var_export($exportedNodes, true)
)
);
}

/**
* @param mixed[]|null $projectConfig
* @param array<string, mixed> $dependencies
* @return array<string, string>
*/
private function getProjectExtensionFiles(?array $projectConfig, array $dependencies): array
{
$this->alreadyProcessed = [];
$projectExtensionFiles = [];
if ($projectConfig !== null) {
$services = $projectConfig['services'] ?? [];
foreach ($services as $service) {
$classes = $this->getClassesFromConfigDefinition($service);
if (is_array($service)) {
foreach (['class', 'factory', 'implement'] as $key) {
if (!isset($service[$key])) {
continue;
}

$classes = array_merge($classes, $this->getClassesFromConfigDefinition($service[$key]));
}
}

foreach (array_unique($classes) as $class) {
if (!$this->reflectionProvider->hasClass($class)) {
continue;
}

$classReflection = $this->reflectionProvider->getClass($class);
$fileName = $classReflection->getFileName();
if ($fileName === false) {
continue;
}

$allServiceFiles = $this->getAllDependencies($fileName, $dependencies);
foreach ($allServiceFiles as $serviceFile) {
if (array_key_exists($serviceFile, $projectExtensionFiles)) {
continue;
}

$projectExtensionFiles[$serviceFile] = $this->getFileHash($serviceFile);
}
}
}
}

return $projectExtensionFiles;
}

/**
* @param mixed $definition
* @return string[]
*/
private function getClassesFromConfigDefinition($definition): array
{
if (is_string($definition)) {
return [$definition];
}

if ($definition instanceof Statement) {
$entity = $definition->entity;
if (is_string($entity)) {
return [$entity];
} elseif (is_array($entity) && isset($entity[0]) && is_string($entity[0])) {
return [$entity[0]];
}
}

return [];
}

/**
* @param string $fileName
* @param array<string, array<int, string>> $dependencies
* @return array<int, string>
*/
private function getAllDependencies(string $fileName, array $dependencies): array
{
if (!array_key_exists($fileName, $dependencies)) {
return [];
}

if (array_key_exists($fileName, $this->alreadyProcessed)) {
return [];
}

$this->alreadyProcessed[$fileName] = true;

$files = [$fileName];
foreach ($dependencies[$fileName] as $fileDep) {
foreach ($this->getAllDependencies($fileDep, $dependencies) as $fileDep2) {
$files[] = $fileDep2;
}
}

return $files;
}

/**
* @param string[] $allAnalysedFiles
* @param mixed[]|null $projectConfigArray
Expand All @@ -567,8 +716,6 @@ private function getMeta(array $allAnalysedFiles, ?array $projectConfigArray): a
unset($projectConfigArray['parameters']['reportUnmatchedIgnoredErrors']);
unset($projectConfigArray['parameters']['memoryLimitFile']);
unset($projectConfigArray['parametersSchema']);

$projectConfigArray = Neon::encode($projectConfigArray);
}

return [
Expand Down

0 comments on commit 1e53ab6

Please sign in to comment.