Skip to content

Commit

Permalink
Result cache - invalidate when project extensions are edited
Browse files Browse the repository at this point in the history
ondrejmirtes committed Mar 18, 2021

Unverified

This user has not yet uploaded their public signing key.
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
@@ -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;
@@ -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;
@@ -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
@@ -73,6 +81,7 @@ class ResultCacheManager
public function __construct(
ExportedNodeFetcher $exportedNodeFetcher,
FileFinder $scanFileFinder,
ReflectionProvider $reflectionProvider,
string $cacheFilePath,
string $tempResultCachePath,
array $analysedPaths,
@@ -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;
@@ -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.');
}
@@ -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 = [];
@@ -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
@@ -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; },
@@ -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
@@ -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 [

0 comments on commit 1e53ab6

Please sign in to comment.