Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Analyser/Analyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public function analyse(
$dependencies = [];
$usedTraitDependencies = [];
$exportedNodes = [];
$externalFileDependencies = [];
foreach ($files as $file) {
if ($preFileCallback !== null) {
$preFileCallback($file);
Expand All @@ -99,6 +100,10 @@ public function analyse(
$collectedData = array_merge($collectedData, $fileAnalyserResult->getCollectedData());
$dependencies[$file] = $fileAnalyserResult->getDependencies();
$usedTraitDependencies[$file] = $fileAnalyserResult->getUsedTraitDependencies();
$fileExternalDeps = $fileAnalyserResult->getExternalFileDependencies();
if (count($fileExternalDeps) > 0) {
$externalFileDependencies[$file] = $fileExternalDeps;
}

$fileExportedNodes = $fileAnalyserResult->getExportedNodes();
if (count($fileExportedNodes) > 0) {
Expand Down Expand Up @@ -143,6 +148,7 @@ public function analyse(
exportedNodes: $exportedNodes,
reachedInternalErrorsCountLimit: $reachedInternalErrorsCountLimit,
peakMemoryUsageBytes: memory_get_peak_usage(true),
externalFileDependencies: $internalErrorsCount === 0 ? $externalFileDependencies : null,
);
}

Expand Down
10 changes: 10 additions & 0 deletions src/Analyser/AnalyserResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ final class AnalyserResult
* @param array<string, array<string>>|null $dependencies
* @param array<string, array<string>>|null $usedTraitDependencies
* @param array<string, array<RootExportedNode>> $exportedNodes
* @param array<string, list<string>>|null $externalFileDependencies
*/
public function __construct(
private array $unorderedErrors,
Expand All @@ -43,6 +44,7 @@ public function __construct(
private array $exportedNodes,
private bool $reachedInternalErrorsCountLimit,
private int $peakMemoryUsageBytes,
private ?array $externalFileDependencies = null,
)
{
}
Expand Down Expand Up @@ -159,6 +161,14 @@ public function getExportedNodes(): array
return $this->exportedNodes;
}

/**
* @return array<string, list<string>>|null
*/
public function getExternalFileDependencies(): ?array
{
return $this->externalFileDependencies;
}

public function hasReachedInternalErrorsCountLimit(): bool
{
return $this->reachedInternalErrorsCountLimit;
Expand Down
3 changes: 3 additions & 0 deletions src/Analyser/AnalyserResultFinalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $
exportedNodes: $analyserResult->getExportedNodes(),
reachedInternalErrorsCountLimit: $analyserResult->hasReachedInternalErrorsCountLimit(),
peakMemoryUsageBytes: $analyserResult->getPeakMemoryUsageBytes(),
externalFileDependencies: $analyserResult->getExternalFileDependencies(),
), $collectorErrors, $locallyIgnoredCollectorErrors);
}

Expand All @@ -167,6 +168,7 @@ private function mergeFilteredPhpErrors(AnalyserResult $analyserResult): Analyse
exportedNodes: $analyserResult->getExportedNodes(),
reachedInternalErrorsCountLimit: $analyserResult->hasReachedInternalErrorsCountLimit(),
peakMemoryUsageBytes: $analyserResult->getPeakMemoryUsageBytes(),
externalFileDependencies: $analyserResult->getExternalFileDependencies(),
);
}

Expand Down Expand Up @@ -231,6 +233,7 @@ private function addUnmatchedIgnoredErrors(
exportedNodes: $analyserResult->getExportedNodes(),
reachedInternalErrorsCountLimit: $analyserResult->hasReachedInternalErrorsCountLimit(),
peakMemoryUsageBytes: $analyserResult->getPeakMemoryUsageBytes(),
externalFileDependencies: $analyserResult->getExternalFileDependencies(),
),
$collectorErrors,
$locallyIgnoredCollectorErrors,
Expand Down
46 changes: 46 additions & 0 deletions src/Analyser/ExternalFileDependencyRegistrar.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PHPStan\DependencyInjection\AutowiredService;
use function array_unique;
use function array_values;

/**
* Allows extensions to declare that the currently analyzed file depends on
* an external (non-analyzed) file. When that external file changes, only
* the dependent analyzed files are re-analyzed instead of the entire project.
*
* This is an alternative to ResultCacheMetaExtension for cases where
* external data changes should not cause full cache invalidation.
*
* @api
*/
#[AutowiredService]
final class ExternalFileDependencyRegistrar
{

/** @var list<string> */
private array $currentFileDependencies = [];

/**
* Register a dependency on an external file for the currently analyzed file.
*/
public function add(string $externalFilePath): void
{
$this->currentFileDependencies[] = $externalFilePath;
}

/**
* @return list<string>
* @internal Used by FileAnalyser after each file analysis
*/
public function getAndReset(): array
{
$deps = array_values(array_unique($this->currentFileDependencies));
$this->currentFileDependencies = [];

return $deps;
}

}
2 changes: 2 additions & 0 deletions src/Analyser/FileAnalyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public function __construct(
private IgnoreErrorExtensionProvider $ignoreErrorExtensionProvider,
private RuleErrorTransformer $ruleErrorTransformer,
private LocalIgnoresProcessor $localIgnoresProcessor,
private ExternalFileDependencyRegistrar $externalFileDependencyRegistrar,
#[AutowiredParameter]
private bool $reportIgnoresWithoutComments,
)
Expand Down Expand Up @@ -247,6 +248,7 @@ public function analyseFile(
$linesToIgnore,
$unmatchedLineIgnores,
$processedFiles,
$this->externalFileDependencyRegistrar->getAndReset(),
);
}

Expand Down
10 changes: 10 additions & 0 deletions src/Analyser/FileAnalyserResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ final class FileAnalyserResult
* @param LinesToIgnore $linesToIgnore
* @param LinesToIgnore $unmatchedLineIgnores
* @param list<string> $processedFiles
* @param list<string> $externalFileDependencies
*/
public function __construct(
private array $errors,
Expand All @@ -38,6 +39,7 @@ public function __construct(
private array $linesToIgnore,
private array $unmatchedLineIgnores,
private array $processedFiles,
private array $externalFileDependencies = [],
)
{
}
Expand Down Expand Up @@ -130,4 +132,12 @@ public function getProcessedFiles(): array
return $this->processedFiles;
}

/**
* @return list<string>
*/
public function getExternalFileDependencies(): array
{
return $this->externalFileDependencies;
}

}
12 changes: 12 additions & 0 deletions src/Analyser/ResultCache/ResultCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ final class ResultCache
* @param array<string, array<RootExportedNode>> $exportedNodes
* @param array<string, array{string, bool, string}> $projectExtensionFiles
* @param array<string, string> $currentFileHashes
* @param array<string, array<string>> $externalFileDependencies
*/
public function __construct(
private array $filesToAnalyse,
Expand All @@ -43,6 +44,7 @@ public function __construct(
private array $exportedNodes,
private array $projectExtensionFiles,
private array $currentFileHashes,
private array $externalFileDependencies = [],
)
{
}
Expand Down Expand Up @@ -153,4 +155,14 @@ public function getCurrentFileHashes(): array
return $this->currentFileHashes;
}

/**
* Inverted external file dependencies: external file => dependent analyzed files.
*
* @return array<string, array<string>>
*/
public function getExternalFileDependencies(): array
{
return $this->externalFileDependencies;
}

}
103 changes: 98 additions & 5 deletions src/Analyser/ResultCache/ResultCacheManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,28 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
$filesToAnalyse = [];
$invertedDependenciesToReturn = [];
$invertedUsedTraitDependenciesToReturn = [];

// Check external file dependencies for incremental re-analysis
/** @var array<string, array{fileHash: string, dependentFiles: list<string>}> $cachedExternalDependencies */
$cachedExternalDependencies = $data['externalDependencies'] ?? [];
$externalDependenciesToReturn = [];
foreach ($cachedExternalDependencies as $externalFile => $externalData) {
$externalDependenciesToReturn[$externalFile] = $externalData['dependentFiles'];
if (is_file($externalFile) && $this->getFileHash($externalFile) === $externalData['fileHash']) {
continue;
}

if ($output->isVeryVerbose()) {
$output->writeLineFormatted(sprintf('External file %s changed, re-analysing dependent files.', $externalFile));
}
foreach ($externalData['dependentFiles'] as $dependentFile) {
if (!is_file($dependentFile)) {
continue;
}

$filesToAnalyse[] = $dependentFile;
}
}
$errors = $data['errorsCallback']();
$locallyIgnoredErrors = $data['locallyIgnoredErrorsCallback']();
$linesToIgnore = $data['linesToIgnore'];
Expand Down Expand Up @@ -515,6 +537,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
exportedNodes: $filteredExportedNodes,
projectExtensionFiles: $data['projectExtensionFiles'],
currentFileHashes: $currentFileHashes,
externalFileDependencies: $externalDependenciesToReturn,
);
}

Expand Down Expand Up @@ -624,7 +647,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache
if ($projectConfigArray !== null) {
$meta['projectConfig'] = Neon::encode($projectConfigArray);
}
$doSave = function (array $errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, ?array $dependencies, ?array $usedTraitDependencies, array $exportedNodes, array $projectExtensionFiles) use ($internalErrors, $resultCache, $output, $onlyFiles, $meta): bool {
$doSave = function (array $errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, ?array $dependencies, ?array $usedTraitDependencies, array $exportedNodes, array $projectExtensionFiles, array $externalFileDependencies = []) use ($internalErrors, $resultCache, $output, $onlyFiles, $meta): bool {
if ($onlyFiles) {
if ($output->isVeryVerbose()) {
$output->writeLineFormatted('Result cache was not saved because only files were passed as analysed paths.');
Expand Down Expand Up @@ -672,7 +695,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache
}
}

$this->save($resultCache->getLastFullAnalysisTime(), $errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, $dependencies, $usedTraitDependencies, $exportedNodes, $projectExtensionFiles, $resultCache->getCurrentFileHashes(), $meta);
$this->save($resultCache->getLastFullAnalysisTime(), $errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, $dependencies, $usedTraitDependencies, $exportedNodes, $projectExtensionFiles, $resultCache->getCurrentFileHashes(), $meta, $externalFileDependencies);

if ($output->isVeryVerbose()) {
$output->writeLineFormatted('Result cache is saved.');
Expand All @@ -688,7 +711,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache
if ($analyserResult->getDependencies() !== null) {
$projectExtensionFiles = $this->getProjectExtensionFiles($projectConfigArray, $analyserResult->getDependencies());
}
$saved = $doSave($freshErrorsByFile, $freshLocallyIgnoredErrorsByFile, $analyserResult->getLinesToIgnore(), $analyserResult->getUnmatchedLineIgnores(), $freshCollectedDataByFile, $analyserResult->getDependencies(), $analyserResult->getUsedTraitDependencies(), $analyserResult->getExportedNodes(), $projectExtensionFiles);
$saved = $doSave($freshErrorsByFile, $freshLocallyIgnoredErrorsByFile, $analyserResult->getLinesToIgnore(), $analyserResult->getUnmatchedLineIgnores(), $freshCollectedDataByFile, $analyserResult->getDependencies(), $analyserResult->getUsedTraitDependencies(), $analyserResult->getExportedNodes(), $projectExtensionFiles, $analyserResult->getExternalFileDependencies() ?? []);
} else {
if ($output->isVeryVerbose()) {
$output->writeLineFormatted('Result cache was not saved because it was not requested.');
Expand All @@ -706,6 +729,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache
$exportedNodes = $this->mergeExportedNodes($resultCache, $analyserResult->getExportedNodes());
$linesToIgnore = $this->mergeLinesToIgnore($resultCache, $analyserResult->getLinesToIgnore());
$unmatchedLineIgnores = $this->mergeUnmatchedLineIgnores($resultCache, $analyserResult->getUnmatchedLineIgnores());
$externalFileDependencies = $this->mergeExternalFileDependencies($resultCache->getExternalFileDependencies(), $resultCache->getFilesToAnalyse(), $analyserResult->getExternalFileDependencies());

$saved = false;
if ($save !== false) {
Expand All @@ -729,7 +753,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache
$projectExtensionFiles[$file] = [$hash, true, $className];
}
}
$saved = $doSave($errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, $dependencies, $usedTraitDependencies, $exportedNodes, $projectExtensionFiles);
$saved = $doSave($errorsByFile, $locallyIgnoredErrorsByFile, $linesToIgnore, $unmatchedLineIgnores, $collectedDataByFile, $dependencies, $usedTraitDependencies, $exportedNodes, $projectExtensionFiles, $externalFileDependencies);
}

$flatErrors = [];
Expand Down Expand Up @@ -760,6 +784,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache
exportedNodes: $exportedNodes,
reachedInternalErrorsCountLimit: $analyserResult->hasReachedInternalErrorsCountLimit(),
peakMemoryUsageBytes: $analyserResult->getPeakMemoryUsageBytes(),
externalFileDependencies: $externalFileDependencies !== [] ? $externalFileDependencies : null,
), $saved);
}

Expand Down Expand Up @@ -944,6 +969,46 @@ private function mergeUnmatchedLineIgnores(ResultCache $resultCache, array $fres
return $newUnmatchedLineIgnores;
}

/**
* Merges cached inverted external dependencies with fresh analysis results.
*
* @param array<string, array<string>> $cachedExternalDependencies Inverted: external file => dependent analyzed files
* @param string[] $filesToAnalyse Files that were re-analyzed
* @param array<string, list<string>>|null $freshExternalDependencies Non-inverted: analyzed file => external files
* @return array<string, list<string>> Non-inverted: analyzed file => external files
*/
private function mergeExternalFileDependencies(
array $cachedExternalDependencies,
array $filesToAnalyse,
?array $freshExternalDependencies,
): array
{
if ($freshExternalDependencies === null) {
return [];
}

// Un-invert cached external dependencies: external file => [dependents] → dependent => [external files]
$cachedPerFile = [];
foreach ($cachedExternalDependencies as $externalFile => $dependentFiles) {
foreach ($dependentFiles as $dependentFile) {
$cachedPerFile[$dependentFile][] = $externalFile;
}
}

// Replace re-analyzed files with fresh data
$merged = $cachedPerFile;
foreach ($filesToAnalyse as $file) {
unset($merged[$file]);
if (!array_key_exists($file, $freshExternalDependencies)) {
continue;
}

$merged[$file] = $freshExternalDependencies[$file];
}

return $merged;
}

/**
* @param array<string, list<Error>> $errors
* @param array<string, list<Error>> $locallyIgnoredErrors
Expand All @@ -956,6 +1021,7 @@ private function mergeUnmatchedLineIgnores(ResultCache $resultCache, array $fres
* @param array<string, array{string, bool, string}> $projectExtensionFiles
* @param array<string, string> $currentFileHashes
* @param mixed[] $meta
* @param array<string, list<string>> $externalFileDependencies
*/
private function save(
int $lastFullAnalysisTime,
Expand All @@ -970,6 +1036,7 @@ private function save(
array $projectExtensionFiles,
array $currentFileHashes,
array $meta,
array $externalFileDependencies = [],
): void
{
$invertedDependencies = [];
Expand Down Expand Up @@ -1043,6 +1110,31 @@ private function save(

ksort($exportedNodes);

// Build inverted external dependencies: external file => {hash, dependentFiles}
$invertedExternalDependencies = [];
foreach ($externalFileDependencies as $analysedFile => $externalFiles) {
foreach ($externalFiles as $externalFile) {
if (!array_key_exists($externalFile, $invertedExternalDependencies)) {
if (!is_file($externalFile)) {
continue;
}
$invertedExternalDependencies[$externalFile] = [
'fileHash' => $this->getFileHash($externalFile),
'dependentFiles' => [],
];
}
$invertedExternalDependencies[$externalFile]['dependentFiles'][] = $analysedFile;
}
}

foreach ($invertedExternalDependencies as $externalFile => $externalData) {
$dependentFiles = array_values(array_unique($externalData['dependentFiles']));
sort($dependentFiles);
$invertedExternalDependencies[$externalFile]['dependentFiles'] = $dependentFiles;
}

ksort($invertedExternalDependencies);

$file = $this->cacheFilePath;

FileWriter::write(
Expand All @@ -1059,7 +1151,8 @@ private function save(
'unmatchedLineIgnores' => " . var_export($unmatchedLineIgnores, true) . ",
'collectedDataCallback' => static function (): array { return " . var_export($collectedData, true) . "; },
'dependencies' => " . var_export($invertedDependencies, true) . ",
'exportedNodesCallback' => static function (): array { return " . var_export($exportedNodes, true) . '; },
'exportedNodesCallback' => static function (): array { return " . var_export($exportedNodes, true) . "; },
'externalDependencies' => " . var_export($invertedExternalDependencies, true) . ',
];
',
);
Expand Down
Loading
Loading