diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 6753593ba47..208bb0e5069 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -189,7 +189,7 @@ jobs: patch -b src/Generated/Foo.php < patch.patch OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw") echo "$OUTPUT" - ../bashunit -a contains 'Result cache not used because the metadata do not match: projectConfig, scannedFiles' "$OUTPUT" + ../bashunit -a contains 'Result cache not used because the metadata do not match: scannedFiles' "$OUTPUT" ../bashunit -a contains 'Instantiated class ResultCacheE2EGenerated\Foo not found.' "$OUTPUT" - script: | cd e2e/result-cache-traits diff --git a/conf/services.neon b/conf/services.neon index 948baee6a76..df72e9f2b6a 100644 --- a/conf/services.neon +++ b/conf/services.neon @@ -56,6 +56,11 @@ services: - class: PHPStan\Reflection\BetterReflection\SourceLocator\CachingVisitor + - + class: PHPStan\Dependency\PackageDependencyResolver + arguments: + composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% + - class: PHPStan\Reflection\Php\PhpClassReflectionExtension arguments: diff --git a/src/Analyser/Analyser.php b/src/Analyser/Analyser.php index 475781e17b6..9b4e4a7460f 100644 --- a/src/Analyser/Analyser.php +++ b/src/Analyser/Analyser.php @@ -74,6 +74,7 @@ public function analyse( $reachedInternalErrorsCountLimit = false; $dependencies = []; $usedTraitDependencies = []; + $packageDependencies = []; $exportedNodes = []; $allProcessedFiles = []; foreach ($files as $file) { @@ -101,6 +102,7 @@ public function analyse( $collectedData = array_merge($collectedData, $fileAnalyserResult->getCollectedData()); $dependencies[$file] = $fileAnalyserResult->getDependencies(); $usedTraitDependencies[$file] = $fileAnalyserResult->getUsedTraitDependencies(); + $packageDependencies[$file] = $fileAnalyserResult->getPackageDependencies(); $fileExportedNodes = $fileAnalyserResult->getExportedNodes(); if (count($fileExportedNodes) > 0) { @@ -142,6 +144,7 @@ public function analyse( collectedData: $collectedData, dependencies: $internalErrorsCount === 0 ? $dependencies : null, usedTraitDependencies: $internalErrorsCount === 0 ? $usedTraitDependencies : null, + packageDependencies: $internalErrorsCount === 0 ? $packageDependencies : null, exportedNodes: $exportedNodes, reachedInternalErrorsCountLimit: $reachedInternalErrorsCountLimit, peakMemoryUsageBytes: memory_get_peak_usage(true), diff --git a/src/Analyser/AnalyserResult.php b/src/Analyser/AnalyserResult.php index 31b88e27300..f9cb1bdef66 100644 --- a/src/Analyser/AnalyserResult.php +++ b/src/Analyser/AnalyserResult.php @@ -27,6 +27,7 @@ final class AnalyserResult * @param list $internalErrors * @param array>|null $dependencies * @param array>|null $usedTraitDependencies + * @param array>|null $packageDependencies * @param array> $exportedNodes * @param list $processedFiles */ @@ -41,6 +42,7 @@ public function __construct( private array $collectedData, private ?array $dependencies, private ?array $usedTraitDependencies, + private ?array $packageDependencies, private array $exportedNodes, private bool $reachedInternalErrorsCountLimit, private int $peakMemoryUsageBytes, @@ -153,6 +155,14 @@ public function getUsedTraitDependencies(): ?array return $this->usedTraitDependencies; } + /** + * @return array>|null + */ + public function getPackageDependencies(): ?array + { + return $this->packageDependencies; + } + /** * @return array> */ diff --git a/src/Analyser/AnalyserResultFinalizer.php b/src/Analyser/AnalyserResultFinalizer.php index 34efa63e7b8..cc06cb3eee3 100644 --- a/src/Analyser/AnalyserResultFinalizer.php +++ b/src/Analyser/AnalyserResultFinalizer.php @@ -145,6 +145,7 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $ collectedData: $analyserResult->getCollectedData(), dependencies: $analyserResult->getDependencies(), usedTraitDependencies: $analyserResult->getUsedTraitDependencies(), + packageDependencies: $analyserResult->getPackageDependencies(), exportedNodes: $analyserResult->getExportedNodes(), reachedInternalErrorsCountLimit: $analyserResult->hasReachedInternalErrorsCountLimit(), peakMemoryUsageBytes: $analyserResult->getPeakMemoryUsageBytes(), @@ -165,6 +166,7 @@ private function mergeFilteredPhpErrors(AnalyserResult $analyserResult): Analyse collectedData: $analyserResult->getCollectedData(), dependencies: $analyserResult->getDependencies(), usedTraitDependencies: $analyserResult->getUsedTraitDependencies(), + packageDependencies: $analyserResult->getPackageDependencies(), exportedNodes: $analyserResult->getExportedNodes(), reachedInternalErrorsCountLimit: $analyserResult->hasReachedInternalErrorsCountLimit(), peakMemoryUsageBytes: $analyserResult->getPeakMemoryUsageBytes(), @@ -230,6 +232,7 @@ private function addUnmatchedIgnoredErrors( collectedData: $analyserResult->getCollectedData(), dependencies: $analyserResult->getDependencies(), usedTraitDependencies: $analyserResult->getUsedTraitDependencies(), + packageDependencies: $analyserResult->getPackageDependencies(), exportedNodes: $analyserResult->getExportedNodes(), reachedInternalErrorsCountLimit: $analyserResult->hasReachedInternalErrorsCountLimit(), peakMemoryUsageBytes: $analyserResult->getPeakMemoryUsageBytes(), diff --git a/src/Analyser/FileAnalyser.php b/src/Analyser/FileAnalyser.php index c59e3545fb2..1870b581a69 100644 --- a/src/Analyser/FileAnalyser.php +++ b/src/Analyser/FileAnalyser.php @@ -10,6 +10,7 @@ use PHPStan\Collectors\CollectedData; use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; +use PHPStan\Dependency\PackageDependencyResolver; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\FileNode; @@ -57,6 +58,7 @@ public function __construct( #[AutowiredParameter(ref: '@defaultAnalysisParser')] private Parser $parser, private DependencyResolver $dependencyResolver, + private PackageDependencyResolver $packageDependencyResolver, private IgnoreErrorExtensionProvider $ignoreErrorExtensionProvider, private RuleErrorTransformer $ruleErrorTransformer, private LocalIgnoresProcessor $localIgnoresProcessor, @@ -92,6 +94,7 @@ public function analyseFile( $fileDependencies = []; $usedTraitFileDependencies = []; + $filePackageDependencies = []; $exportedNodes = []; $linesToIgnore = []; $unmatchedLineIgnores = []; @@ -111,6 +114,7 @@ public function analyseFile( $this->ignoreErrorExtensionProvider->getExtensions(), $this->parser, $this->dependencyResolver, + $this->packageDependencyResolver, $this->ruleErrorTransformer, $processedFiles, ); @@ -125,6 +129,7 @@ public function analyseFile( $fileCollectedData = $nodeCallback->getFileCollectedData(); $fileDependencies = $nodeCallback->getFileDependencies(); $usedTraitFileDependencies = $nodeCallback->getUsedTraitFileDependencies(); + $filePackageDependencies = $nodeCallback->getPackageDependencies(); $exportedNodes = $nodeCallback->getExportedNodes(); $linesToIgnore = $nodeCallback->getLinesToIgnore(); $unmatchedLineIgnores = $nodeCallback->getUnmatchedLineIgnores(); @@ -243,6 +248,7 @@ public function analyseFile( $fileCollectedData, array_values(array_unique($fileDependencies)), array_values(array_unique($usedTraitFileDependencies)), + array_values(array_unique($filePackageDependencies)), $exportedNodes, $linesToIgnore, $unmatchedLineIgnores, diff --git a/src/Analyser/FileAnalyserCallback.php b/src/Analyser/FileAnalyserCallback.php index 07e5371a04e..566390a15bb 100644 --- a/src/Analyser/FileAnalyserCallback.php +++ b/src/Analyser/FileAnalyserCallback.php @@ -10,6 +10,7 @@ use PHPStan\Collectors\CollectedData; use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; +use PHPStan\Dependency\PackageDependencyResolver; use PHPStan\Dependency\RootExportedNode; use PHPStan\Node\EmitCollectedDataNode; use PHPStan\Node\InClassNode; @@ -40,6 +41,9 @@ final class FileAnalyserCallback /** @var array */ private array $usedTraitFileDependencies = []; + /** @var array */ + private array $filePackageDependencies = []; + /** @var list */ private array $exportedNodes = []; @@ -69,6 +73,7 @@ public function __construct( private array $ignoreErrorExtensions, private Parser $parser, private DependencyResolver $dependencyResolver, + private PackageDependencyResolver $packageDependencyResolver, private RuleErrorTransformer $ruleErrorTransformer, private array $processedFiles, ) @@ -215,6 +220,9 @@ public function __invoke(Node $node, Scope $scope): void foreach ($dependencies->getFileDependencies($scope->getFile(), $this->analysedFiles) as $dependentFile) { $this->fileDependencies[] = $dependentFile; } + foreach ($dependencies->getPackageDependencies($scope->getFile(), $this->analysedFiles, $this->packageDependencyResolver) as $package) { + $this->filePackageDependencies[] = $package; + } if ($dependencies->getExportedNode() !== null) { $this->exportedNodes[] = $dependencies->getExportedNode(); } @@ -234,6 +242,9 @@ public function __invoke(Node $node, Scope $scope): void foreach ($usedTraitDependencies->getFileDependencies($scope->getFile(), $this->analysedFiles) as $dependentFile) { $this->usedTraitFileDependencies[] = $dependentFile; } + foreach ($usedTraitDependencies->getPackageDependencies($scope->getFile(), $this->analysedFiles, $this->packageDependencyResolver) as $package) { + $this->filePackageDependencies[] = $package; + } } /** @@ -274,6 +285,14 @@ public function getFileDependencies(): array return $this->fileDependencies; } + /** + * @return array + */ + public function getPackageDependencies(): array + { + return $this->filePackageDependencies; + } + /** * @return array */ diff --git a/src/Analyser/FileAnalyserResult.php b/src/Analyser/FileAnalyserResult.php index 320b736c6b7..5d9c5f15226 100644 --- a/src/Analyser/FileAnalyserResult.php +++ b/src/Analyser/FileAnalyserResult.php @@ -21,6 +21,7 @@ final class FileAnalyserResult * @param CollectorData $collectedData * @param list $dependencies * @param list $usedTraitDependencies + * @param list $packageDependencies * @param list $exportedNodes * @param LinesToIgnore $linesToIgnore * @param LinesToIgnore $unmatchedLineIgnores @@ -34,6 +35,7 @@ public function __construct( private array $collectedData, private array $dependencies, private array $usedTraitDependencies, + private array $packageDependencies, private array $exportedNodes, private array $linesToIgnore, private array $unmatchedLineIgnores, @@ -98,6 +100,14 @@ public function getUsedTraitDependencies(): array return $this->usedTraitDependencies; } + /** + * @return list + */ + public function getPackageDependencies(): array + { + return $this->packageDependencies; + } + /** * @return list */ diff --git a/src/Analyser/ResultCache/ResultCache.php b/src/Analyser/ResultCache/ResultCache.php index 5cfefc6f46a..b3fa2cdc01c 100644 --- a/src/Analyser/ResultCache/ResultCache.php +++ b/src/Analyser/ResultCache/ResultCache.php @@ -24,6 +24,7 @@ final class ResultCache * @param CollectorData $collectedData * @param array> $dependencies * @param array> $usedTraitDependencies + * @param array> $packageDependencies * @param array> $exportedNodes * @param array $projectExtensionFiles * @param array $currentFileHashes @@ -40,6 +41,7 @@ public function __construct( private array $collectedData, private array $dependencies, private array $usedTraitDependencies, + private array $packageDependencies, private array $exportedNodes, private array $projectExtensionFiles, private array $currentFileHashes, @@ -129,6 +131,14 @@ public function getUsedTraitDependencies(): array return $this->usedTraitDependencies; } + /** + * @return array> + */ + public function getPackageDependencies(): array + { + return $this->packageDependencies; + } + /** * @return array> */ diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php index 0bc1e756e5b..e3172812c5b 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -10,6 +10,7 @@ use PHPStan\Command\Output; use PHPStan\Dependency\ExportedNode\ExportedTraitNode; use PHPStan\Dependency\ExportedNodeFetcher; +use PHPStan\Dependency\PackageDependencyResolver; use PHPStan\Dependency\RootExportedNode; use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\Container; @@ -61,7 +62,7 @@ final class ResultCacheManager { - private const CACHE_VERSION = 'v12-linesToIgnore'; + private const CACHE_VERSION = 'v13-packageDependencies'; /** @var array */ private array $fileHashes = []; @@ -86,6 +87,7 @@ public function __construct( private FileFinder $scanFileFinder, private StubFilesProvider $stubFilesProvider, private FileHelper $fileHelper, + private PackageDependencyResolver $packageDependencyResolver, #[AutowiredParameter(ref: '%resultCachePath%')] private string $cacheFilePath, #[AutowiredParameter] @@ -145,6 +147,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? collectedData: [], dependencies: [], usedTraitDependencies: [], + packageDependencies: [], exportedNodes: [], projectExtensionFiles: [], currentFileHashes: $currentFileHashes, @@ -166,6 +169,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? collectedData: [], dependencies: [], usedTraitDependencies: [], + packageDependencies: [], exportedNodes: [], projectExtensionFiles: [], currentFileHashes: $currentFileHashes, @@ -189,6 +193,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? collectedData: [], dependencies: [], usedTraitDependencies: [], + packageDependencies: [], exportedNodes: [], projectExtensionFiles: [], currentFileHashes: $currentFileHashes, @@ -216,6 +221,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? collectedData: [], dependencies: [], usedTraitDependencies: [], + packageDependencies: [], exportedNodes: [], projectExtensionFiles: [], currentFileHashes: $currentFileHashes, @@ -240,6 +246,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? collectedData: [], dependencies: [], usedTraitDependencies: [], + packageDependencies: [], exportedNodes: [], projectExtensionFiles: [], currentFileHashes: $currentFileHashes, @@ -247,27 +254,58 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? } $meta = $this->getMeta($allAnalysedFiles, $projectConfigArray); + $packageDependencies = $data['packageDependencies'] ?? []; + $packageSeededFiles = []; if ($this->isMetaDifferent($data['meta'], $meta)) { + $diffs = $this->getMetaKeyDifferences($data['meta'], $meta); + + // If the metadata differ ONLY in the Composer lock/installed files, the generated container + // and analysis are unchanged except for code coming from packages whose version actually + // changed. Re-analyse just the files depending on a changed package instead of everything; + // the existing incremental loop below then propagates to their dependents on signature change. + // Any other meta difference, or an undetermined change set, falls back to a full re-analysis. + $changedPackages = array_diff($diffs, ['composerLocks', 'composerInstalled']) === [] + ? $this->packageDependencyResolver->getChangedComposerPackages($data['meta'], $meta) + : null; + + if ($changedPackages === null || $changedPackages === []) { + if ($output->isVeryVerbose()) { + $output->writeLineFormatted('Result cache not used because the metadata do not match: ' . implode(', ', $diffs)); + } + return new ResultCache( + filesToAnalyse: $allAnalysedFiles, + fullAnalysis: true, + lastFullAnalysisTime: time(), + meta: $meta, + errors: [], + locallyIgnoredErrors: [], + linesToIgnore: [], + unmatchedLineIgnores: [], + collectedData: [], + dependencies: [], + usedTraitDependencies: [], + packageDependencies: [], + exportedNodes: [], + projectExtensionFiles: [], + currentFileHashes: $currentFileHashes, + ); + } + if ($output->isVeryVerbose()) { - $diffs = $this->getMetaKeyDifferences($data['meta'], $meta); - $output->writeLineFormatted('Result cache not used because the metadata do not match: ' . implode(', ', $diffs)); + $output->writeLineFormatted(sprintf( + 'Composer packages changed (%s); re-analysing only the files depending on them.', + implode(', ', $changedPackages), + )); + } + $changedPackagesLookup = array_fill_keys($changedPackages, true); + foreach ($packageDependencies as $packageDependentFile => $filePackages) { + foreach ($filePackages as $filePackage) { + if (isset($changedPackagesLookup[$filePackage])) { + $packageSeededFiles[] = $packageDependentFile; + break; + } + } } - return new ResultCache( - filesToAnalyse: $allAnalysedFiles, - fullAnalysis: true, - lastFullAnalysisTime: time(), - meta: $meta, - errors: [], - locallyIgnoredErrors: [], - linesToIgnore: [], - unmatchedLineIgnores: [], - collectedData: [], - dependencies: [], - usedTraitDependencies: [], - exportedNodes: [], - projectExtensionFiles: [], - currentFileHashes: $currentFileHashes, - ); } $daysOldForSkip = $this->skipResultCacheIfOlderThanDays; @@ -289,6 +327,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? collectedData: [], dependencies: [], usedTraitDependencies: [], + packageDependencies: [], exportedNodes: [], projectExtensionFiles: [], currentFileHashes: $currentFileHashes, @@ -319,6 +358,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? collectedData: [], dependencies: [], usedTraitDependencies: [], + packageDependencies: [], exportedNodes: [], projectExtensionFiles: [], currentFileHashes: $currentFileHashes, @@ -345,6 +385,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? collectedData: [], dependencies: [], usedTraitDependencies: [], + packageDependencies: [], exportedNodes: [], projectExtensionFiles: [], currentFileHashes: $currentFileHashes, @@ -483,6 +524,13 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? } } + foreach ($packageSeededFiles as $packageSeededFile) { + if (!is_file($packageSeededFile)) { + continue; + } + $filesToAnalyse[] = $packageSeededFile; + } + $filesToAnalyse = array_unique($filesToAnalyse); $filesToAnalyseCount = count($filesToAnalyse); @@ -512,6 +560,7 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ? collectedData: $filteredCollectedData, dependencies: $invertedDependenciesToReturn, usedTraitDependencies: $invertedUsedTraitDependenciesToReturn, + packageDependencies: $packageDependencies, exportedNodes: $filteredExportedNodes, projectExtensionFiles: $data['projectExtensionFiles'], currentFileHashes: $currentFileHashes, @@ -542,6 +591,16 @@ private function isMetaDifferent(array $cachedMeta, array $currentMeta): bool */ private function getMetaKeyDifferences(array $cachedMeta, array $currentMeta): array { + // Normalize projectConfig the same way isMetaDifferent() does: the cached value is a + // Neon-encoded string while the current one is a raw array, so a plain === would always + // report projectConfig as different. + $projectConfig = $currentMeta['projectConfig']; + if ($projectConfig !== null) { + ksort($currentMeta['projectConfig']); + + $currentMeta['projectConfig'] = Neon::encode($currentMeta['projectConfig']); + } + $diffs = []; foreach ($cachedMeta as $key => $value) { if (!array_key_exists($key, $currentMeta)) { @@ -624,7 +683,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 $packageDependencies, array $exportedNodes, array $projectExtensionFiles) 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.'); @@ -643,6 +702,12 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache } return false; } + if ($packageDependencies === null) { + if ($output->isVeryVerbose()) { + $output->writeLineFormatted('Result cache was not saved because of error in package dependencies.'); + } + return false; + } if (count($internalErrors) > 0) { if ($output->isVeryVerbose()) { @@ -672,7 +737,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, $packageDependencies, $exportedNodes, $projectExtensionFiles, $resultCache->getCurrentFileHashes(), $meta); if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache is saved.'); @@ -688,7 +753,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->getPackageDependencies(), $analyserResult->getExportedNodes(), $projectExtensionFiles); } else { if ($output->isVeryVerbose()) { $output->writeLineFormatted('Result cache was not saved because it was not requested.'); @@ -703,6 +768,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache $collectedDataByFile = $this->mergeCollectedData($resultCache, $freshCollectedDataByFile); $dependencies = $this->mergeDependencies($resultCache->getDependencies(), $resultCache->getFilesToAnalyse(), $analyserResult->getDependencies()); $usedTraitDependencies = $this->mergeDependencies($resultCache->getUsedTraitDependencies(), $resultCache->getFilesToAnalyse(), $analyserResult->getUsedTraitDependencies()); + $packageDependencies = $this->mergePackageDependencies($resultCache->getPackageDependencies(), $resultCache->getFilesToAnalyse(), $analyserResult->getPackageDependencies()); $exportedNodes = $this->mergeExportedNodes($resultCache, $analyserResult->getExportedNodes()); $linesToIgnore = $this->mergeLinesToIgnore($resultCache, $analyserResult->getLinesToIgnore()); $unmatchedLineIgnores = $this->mergeUnmatchedLineIgnores($resultCache, $analyserResult->getUnmatchedLineIgnores()); @@ -729,7 +795,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, $packageDependencies, $exportedNodes, $projectExtensionFiles); } $flatErrors = []; @@ -757,6 +823,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache collectedData: $collectedDataByFile, dependencies: $dependencies, usedTraitDependencies: $usedTraitDependencies, + packageDependencies: $packageDependencies, exportedNodes: $exportedNodes, reachedInternalErrorsCountLimit: $analyserResult->hasReachedInternalErrorsCountLimit(), peakMemoryUsageBytes: $analyserResult->getPeakMemoryUsageBytes(), @@ -899,6 +966,44 @@ private function mergeExportedNodes(ResultCache $resultCache, array $freshExport return $newExportedNodes; } + /** + * @param array> $resultCachePackageDependencies + * @param string[] $filesToAnalyse + * @param array>|null $freshPackageDependencies + * @return array>|null + */ + private function mergePackageDependencies(array $resultCachePackageDependencies, array $filesToAnalyse, ?array $freshPackageDependencies): ?array + { + if ($freshPackageDependencies === null) { + return null; + } + + $newPackageDependencies = $resultCachePackageDependencies; + foreach ($filesToAnalyse as $file) { + if (array_key_exists($file, $this->fileReplacements)) { + unset($newPackageDependencies[$file]); + $file = $this->fileReplacements[$file]; + } + if (!array_key_exists($file, $freshPackageDependencies)) { + unset($newPackageDependencies[$file]); + continue; + } + + $newPackageDependencies[$file] = $freshPackageDependencies[$file]; + } + + return $newPackageDependencies; + } + + /** + * Project package names whose installed version/reference changed between two metadata snapshots, + * or null if the change set cannot be reliably determined (then the caller falls back to a full + * re-analysis rather than risk under-invalidation). + * + * @param mixed[] $cachedMeta + * @param mixed[] $currentMeta + * @return list|null + */ /** * @param array $freshLinesToIgnore * @return array @@ -953,6 +1058,7 @@ private function mergeUnmatchedLineIgnores(ResultCache $resultCache, array $fres * @param array>> $collectedData * @param array> $dependencies * @param array> $usedTraitDependencies + * @param array> $packageDependencies * @param array> $exportedNodes * @param array $projectExtensionFiles * @param array $currentFileHashes @@ -967,6 +1073,7 @@ private function save( array $collectedData, array $dependencies, array $usedTraitDependencies, + array $packageDependencies, array $exportedNodes, array $projectExtensionFiles, array $currentFileHashes, @@ -1060,6 +1167,7 @@ private function save( 'unmatchedLineIgnores' => " . var_export($unmatchedLineIgnores, true) . ", 'collectedDataCallback' => static function (): array { return " . var_export($collectedData, true) . "; }, 'dependencies' => " . var_export($invertedDependencies, true) . ", + 'packageDependencies' => " . var_export($packageDependencies, true) . ", 'exportedNodesCallback' => static function (): array { return " . var_export($exportedNodes, true) . '; }, ]; ', diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php index e78212f492c..2cc886eb0f9 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -119,6 +119,7 @@ public function analyse( collectedData: $intermediateAnalyserResult->getCollectedData(), dependencies: $intermediateAnalyserResult->getDependencies(), usedTraitDependencies: $intermediateAnalyserResult->getUsedTraitDependencies(), + packageDependencies: $intermediateAnalyserResult->getPackageDependencies(), exportedNodes: $intermediateAnalyserResult->getExportedNodes(), reachedInternalErrorsCountLimit: $intermediateAnalyserResult->hasReachedInternalErrorsCountLimit(), peakMemoryUsageBytes: $intermediateAnalyserResult->getPeakMemoryUsageBytes(), @@ -241,6 +242,7 @@ private function runAnalyser( collectedData: [], dependencies: [], usedTraitDependencies: [], + packageDependencies: [], exportedNodes: [], reachedInternalErrorsCountLimit: false, peakMemoryUsageBytes: memory_get_peak_usage(true), @@ -328,6 +330,10 @@ private function switchTmpFileInAnalyserResult( if ($analyserResult->getUsedTraitDependencies() !== null) { $usedTraitDependencies = $this->switchTmpFileInDependencies($analyserResult->getUsedTraitDependencies(), $insteadOfFile, $tmpFile); } + $packageDependencies = null; + if ($analyserResult->getPackageDependencies() !== null) { + $packageDependencies = $this->switchTmpFileInDependencies($analyserResult->getPackageDependencies(), $insteadOfFile, $tmpFile); + } $exportedNodes = []; foreach ($analyserResult->getExportedNodes() as $file => $fileExportedNodes) { @@ -349,6 +355,7 @@ private function switchTmpFileInAnalyserResult( collectedData: $newCollectedData, dependencies: $dependencies, usedTraitDependencies: $usedTraitDependencies, + packageDependencies: $packageDependencies, exportedNodes: $exportedNodes, reachedInternalErrorsCountLimit: $analyserResult->hasReachedInternalErrorsCountLimit(), peakMemoryUsageBytes: $analyserResult->getPeakMemoryUsageBytes(), diff --git a/src/Command/AnalyserRunner.php b/src/Command/AnalyserRunner.php index fba6e420608..dac67670a14 100644 --- a/src/Command/AnalyserRunner.php +++ b/src/Command/AnalyserRunner.php @@ -66,6 +66,7 @@ public function runAnalyser( collectedData: [], dependencies: [], usedTraitDependencies: [], + packageDependencies: [], exportedNodes: [], reachedInternalErrorsCountLimit: false, peakMemoryUsageBytes: memory_get_peak_usage(true), diff --git a/src/Command/FixerWorkerRunner.php b/src/Command/FixerWorkerRunner.php index 9a46b1501b2..dbc0494cdc9 100644 --- a/src/Command/FixerWorkerRunner.php +++ b/src/Command/FixerWorkerRunner.php @@ -345,6 +345,7 @@ private function runAnalyser(LoopInterface $loop, array $files, array $allAnalys collectedData: [], dependencies: [], usedTraitDependencies: [], + packageDependencies: [], exportedNodes: [], reachedInternalErrorsCountLimit: false, peakMemoryUsageBytes: memory_get_peak_usage(true), diff --git a/src/Dependency/NodeDependencies.php b/src/Dependency/NodeDependencies.php index 3ccb203762e..6c944853d08 100644 --- a/src/Dependency/NodeDependencies.php +++ b/src/Dependency/NodeDependencies.php @@ -62,6 +62,46 @@ public function getFileDependencies(string $currentFile, array $analysedFiles): return array_values($dependencies); } + /** + * Project packages this file's reflections depend on. Mirrors getFileDependencies() but keeps the + * vendor files it drops, resolved to the analysed project's Composer package, so a composer.lock + * change can re-analyse only the files that depend on a package whose version changed. + * + * @param array $analysedFiles + * @return list + */ + public function getPackageDependencies(string $currentFile, array $analysedFiles, PackageDependencyResolver $packageDependencyResolver): array + { + $packages = []; + + foreach ($this->reflections as $dependencyReflection) { + $dependencyFile = $dependencyReflection->getFileName(); + if ($dependencyFile === null) { + continue; + } + + $dependencyFile = $this->fileHelper->normalizePath($dependencyFile); + + if ($currentFile === $dependencyFile) { + continue; + } + + if (isset($analysedFiles[$dependencyFile])) { + // Analysed file: already tracked as a file-to-file dependency. + continue; + } + + $package = $packageDependencyResolver->resolvePackage($dependencyFile); + if ($package === null) { + continue; + } + + $packages[$package] = $package; + } + + return array_values($packages); + } + public function getExportedNode(): ?RootExportedNode { return $this->exportedNode; diff --git a/src/Dependency/PackageDependencyResolver.php b/src/Dependency/PackageDependencyResolver.php new file mode 100644 index 00000000000..b707e7a5f1a --- /dev/null +++ b/src/Dependency/PackageDependencyResolver.php @@ -0,0 +1,162 @@ +|null normalized install path => package name, longest path first */ + private ?array $installPathToPackage = null; + + /** @param string[] $composerAutoloaderProjectPaths */ + public function __construct( + private array $composerAutoloaderProjectPaths, + private FileHelper $fileHelper, + ) + { + } + + public function resolvePackage(string $file): ?string + { + // Normalize with a forward slash regardless of platform: normalizePath() defaults to + // DIRECTORY_SEPARATOR, so on Windows the paths would use '\' while the prefix check below + // appends '/', and nothing would ever match. + $normalizedFile = $this->fileHelper->normalizePath($file, '/'); + foreach ($this->getInstallPathToPackage() as $installPath => $package) { + if (str_starts_with($normalizedFile, $installPath . '/')) { + return $package; + } + } + + return null; + } + + /** + * Names of packages whose recorded version/reference differs between two result-cache metas, or + * null when either meta's composerInstalled cannot be parsed (the caller falls back to a full + * re-analysis rather than risk under-invalidation). + * + * @param mixed[] $cachedMeta + * @param mixed[] $currentMeta + * @return list|null + */ + public function getChangedComposerPackages(array $cachedMeta, array $currentMeta): ?array + { + $cached = $this->extractComposerPackageVersions($cachedMeta['composerInstalled'] ?? null); + $current = $this->extractComposerPackageVersions($currentMeta['composerInstalled'] ?? null); + if ($cached === null || $current === null) { + return null; + } + + $changed = []; + foreach ($current as $package => $version) { + if (array_key_exists($package, $cached) && $cached[$package] === $version) { + continue; + } + + $changed[$package] = $package; + } + foreach (array_keys($cached) as $package) { + if (array_key_exists($package, $current)) { + continue; + } + + $changed[$package] = $package; + } + + return array_values($changed); + } + + /** + * @return array|null + */ + public function extractComposerPackageVersions(mixed $composerInstalled): ?array + { + if (!is_array($composerInstalled)) { + return null; + } + + $versions = []; + foreach ($composerInstalled as $installed) { + if (!is_array($installed) || !isset($installed['versions']) || !is_array($installed['versions'])) { + return null; + } + foreach ($installed['versions'] as $package => $info) { + if (!is_string($package) || !is_array($info)) { + return null; + } + $reference = $info['reference'] ?? $info['version'] ?? $info['pretty_version'] ?? null; + $versions[$package] = is_string($reference) ? $reference : ''; + } + } + + return $versions; + } + + /** @return array */ + private function getInstallPathToPackage(): array + { + if ($this->installPathToPackage !== null) { + return $this->installPathToPackage; + } + + $map = []; + foreach ($this->composerAutoloaderProjectPaths as $autoloadPath) { + $composer = ComposerHelper::getComposerConfig($autoloadPath); + if ($composer === null) { + continue; + } + + $installedPhp = ComposerHelper::getVendorDirFromComposerConfig($autoloadPath, $composer) . '/composer/installed.php'; + if (!is_file($installedPhp)) { + continue; + } + + $installed = require $installedPhp; + if (!is_array($installed) || !isset($installed['versions']) || !is_array($installed['versions'])) { + continue; + } + + $root = $installed['root'] ?? null; + $rootName = is_array($root) && isset($root['name']) && is_string($root['name']) ? $root['name'] : null; + + foreach ($installed['versions'] as $package => $info) { + if (!is_string($package) || $package === $rootName) { + continue; + } + if (!is_array($info) || !isset($info['install_path']) || !is_string($info['install_path'])) { + continue; + } + + $map[$this->fileHelper->normalizePath($info['install_path'], '/')] = $package; + } + } + + // Longest install path first, so a package nested under another package's directory matches first. + uksort($map, static fn (string $a, string $b): int => strlen($b) <=> strlen($a)); + + return $this->installPathToPackage = $map; + } + +} diff --git a/src/Parallel/ParallelAnalyser.php b/src/Parallel/ParallelAnalyser.php index c5be7a4e546..4d229f68e76 100644 --- a/src/Parallel/ParallelAnalyser.php +++ b/src/Parallel/ParallelAnalyser.php @@ -94,6 +94,7 @@ public function analyse( $collectedData = []; $dependencies = []; $usedTraitDependencies = []; + $packageDependencies = []; $reachedInternalErrorsCountLimit = false; $exportedNodes = []; /** @var list $allProcessedFiles */ @@ -103,7 +104,7 @@ public function analyse( $deferred = new Deferred(); $server = new TcpServer('127.0.0.1:0', $loop); - $this->processPool = new ProcessPool($server, static function () use ($deferred, &$jobs, &$internalErrors, &$internalErrorsCount, &$reachedInternalErrorsCountLimit, &$errors, &$filteredPhpErrors, &$allPhpErrors, &$locallyIgnoredErrors, &$linesToIgnore, &$unmatchedLineIgnores, &$collectedData, &$dependencies, &$usedTraitDependencies, &$exportedNodes, &$peakMemoryUsages, &$allProcessedFiles): void { + $this->processPool = new ProcessPool($server, static function () use ($deferred, &$jobs, &$internalErrors, &$internalErrorsCount, &$reachedInternalErrorsCountLimit, &$errors, &$filteredPhpErrors, &$allPhpErrors, &$locallyIgnoredErrors, &$linesToIgnore, &$unmatchedLineIgnores, &$collectedData, &$dependencies, &$usedTraitDependencies, &$packageDependencies, &$exportedNodes, &$peakMemoryUsages, &$allProcessedFiles): void { if (count($jobs) > 0 && $internalErrorsCount === 0) { $internalErrors[] = new InternalError( 'Some parallel worker jobs have not finished.', @@ -126,6 +127,7 @@ public function analyse( collectedData: $collectedData, dependencies: $internalErrorsCount === 0 ? $dependencies : null, usedTraitDependencies: $internalErrorsCount === 0 ? $usedTraitDependencies : null, + packageDependencies: $internalErrorsCount === 0 ? $packageDependencies : null, exportedNodes: $exportedNodes, reachedInternalErrorsCountLimit: $reachedInternalErrorsCountLimit, peakMemoryUsageBytes: array_sum($peakMemoryUsages), // not 100% correct as the peak usages of workers might not have met @@ -210,7 +212,7 @@ public function analyse( $insteadOfFile, $input, ); - $process->start(function (array $json) use ($process, &$internalErrors, &$errors, &$filteredPhpErrors, &$allPhpErrors, &$locallyIgnoredErrors, &$linesToIgnore, &$unmatchedLineIgnores, &$collectedData, &$dependencies, &$usedTraitDependencies, &$exportedNodes, &$peakMemoryUsages, &$jobs, $postFileCallback, &$internalErrorsCount, &$reachedInternalErrorsCountLimit, $processIdentifier, $onFileAnalysisHandler, &$allProcessedFiles): void { + $process->start(function (array $json) use ($process, &$internalErrors, &$errors, &$filteredPhpErrors, &$allPhpErrors, &$locallyIgnoredErrors, &$linesToIgnore, &$unmatchedLineIgnores, &$collectedData, &$dependencies, &$usedTraitDependencies, &$packageDependencies, &$exportedNodes, &$peakMemoryUsages, &$jobs, $postFileCallback, &$internalErrorsCount, &$reachedInternalErrorsCountLimit, $processIdentifier, $onFileAnalysisHandler, &$allProcessedFiles): void { $fileErrors = []; foreach ($json['errors'] as $jsonError) { $fileErrors[] = Error::decode($jsonError); @@ -268,6 +270,14 @@ public function analyse( $usedTraitDependencies[$file] = $fileUsedTraitDependencies; } + /** + * @var string $file + * @var array $filePackageDependencies + */ + foreach ($json['packageDependencies'] as $file => $filePackageDependencies) { + $packageDependencies[$file] = $filePackageDependencies; + } + foreach ($json['linesToIgnore'] as $file => $fileLinesToIgnore) { if (count($fileLinesToIgnore) === 0) { continue; diff --git a/src/Parallel/WorkerRunner.php b/src/Parallel/WorkerRunner.php index 6cb597dc2d8..c570454e7f9 100644 --- a/src/Parallel/WorkerRunner.php +++ b/src/Parallel/WorkerRunner.php @@ -128,6 +128,7 @@ private function runWorker( 'collectedData' => [], 'memoryUsage' => memory_get_peak_usage(true), 'dependencies' => [], + 'packageDependencies' => [], 'exportedNodes' => [], 'files' => [], 'internalErrorsCount' => 1, @@ -157,6 +158,7 @@ private function runWorker( $collectedData = []; $dependencies = []; $usedTraitDependencies = []; + $packageDependencies = []; $exportedNodes = []; $processedFiles = []; foreach ($files as $file) { @@ -172,6 +174,7 @@ private function runWorker( $unmatchedLineIgnores[$file] = $fileAnalyserResult->getUnmatchedLineIgnores(); $dependencies[$file] = $fileAnalyserResult->getDependencies(); $usedTraitDependencies[$file] = $fileAnalyserResult->getUsedTraitDependencies(); + $packageDependencies[$file] = $fileAnalyserResult->getPackageDependencies(); $exportedNodes[$file] = $fileAnalyserResult->getExportedNodes(); $processedFiles = array_merge($processedFiles, $fileAnalyserResult->getProcessedFiles()); foreach ($fileErrors as $fileError) { @@ -213,6 +216,7 @@ private function runWorker( 'memoryUsage' => memory_get_peak_usage(true), 'dependencies' => $dependencies, 'usedTraitDependencies' => $usedTraitDependencies, + 'packageDependencies' => $packageDependencies, 'exportedNodes' => $exportedNodes, 'files' => $files, 'processedFiles' => $processedFiles, diff --git a/src/Testing/RuleTestCase.php b/src/Testing/RuleTestCase.php index d8d0ba20e1c..7f137660159 100644 --- a/src/Testing/RuleTestCase.php +++ b/src/Testing/RuleTestCase.php @@ -18,6 +18,7 @@ use PHPStan\Collectors\Collector; use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; +use PHPStan\Dependency\PackageDependencyResolver; use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider; @@ -136,6 +137,7 @@ private function getAnalyser(DirectRuleRegistry $ruleRegistry): Analyser $nodeScopeResolver, $this->getParser(), self::getContainer()->getByType(DependencyResolver::class), + self::getContainer()->getByType(PackageDependencyResolver::class), new IgnoreErrorExtensionProvider(self::getContainer()), self::getContainer()->getByType(RuleErrorTransformer::class), new LocalIgnoresProcessor(), diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index 39da6a5be9e..6f781f28a25 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -12,6 +12,7 @@ use PHPStan\Collectors\Registry as CollectorRegistry; use PHPStan\Dependency\DependencyResolver; use PHPStan\Dependency\ExportedNodeResolver; +use PHPStan\Dependency\PackageDependencyResolver; use PHPStan\DependencyInjection\Nette\NetteContainer; use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider; use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider; @@ -846,6 +847,7 @@ private function createAnalyser(): Analyser new IgnoreLexer(), ), new DependencyResolver($fileHelper, $reflectionProvider, new ExportedNodeResolver($reflectionProvider, $fileTypeMapper, new ExprPrinter(new Printer())), $fileTypeMapper), + new PackageDependencyResolver([], $fileHelper), new IgnoreErrorExtensionProvider(new NetteContainer(new Container([]))), $container->getByType(RuleErrorTransformer::class), new LocalIgnoresProcessor(), diff --git a/tests/PHPStan/Dependency/PackageDependencyResolverTest.php b/tests/PHPStan/Dependency/PackageDependencyResolverTest.php new file mode 100644 index 00000000000..5928940c684 --- /dev/null +++ b/tests/PHPStan/Dependency/PackageDependencyResolverTest.php @@ -0,0 +1,91 @@ +getByType(FileHelper::class); + $resolver = new PackageDependencyResolver([$fixtureRoot], $fileHelper); + + // Files under a project package install path resolve to that package. + $this->assertSame('acme/widget', $resolver->resolvePackage($fixtureRoot . '/vendor/acme/widget/src/Widget.php')); + $this->assertSame('acme/gadget', $resolver->resolvePackage($fixtureRoot . '/vendor/acme/gadget/Gadget.php')); + + // The root package is skipped, so project sources belong to no package. + $this->assertNull($resolver->resolvePackage($fixtureRoot . '/src/App.php')); + + // A path outside the project entirely. + $this->assertNull($resolver->resolvePackage('/outside/the/project/File.php')); + } + + public function testExtractComposerPackageVersions(): void + { + $resolver = new PackageDependencyResolver([], self::getContainer()->getByType(FileHelper::class)); + + // reference is preferred, then version, then pretty_version, else the empty string. + $this->assertSame( + ['acme/a' => 'ref-a', 'acme/b' => '2.0.0', 'acme/c' => 'dev-main', 'acme/d' => ''], + $resolver->extractComposerPackageVersions([ + [ + 'versions' => [ + 'acme/a' => ['reference' => 'ref-a', 'version' => '1.0.0'], + 'acme/b' => ['version' => '2.0.0'], + 'acme/c' => ['pretty_version' => 'dev-main'], + 'acme/d' => [], + ], + ], + ]), + ); + + // Multiple installed.php entries (one per autoload path) merge. + $this->assertSame( + ['acme/a' => 'ref-a', 'acme/b' => 'ref-b'], + $resolver->extractComposerPackageVersions([ + ['versions' => ['acme/a' => ['reference' => 'ref-a']]], + ['versions' => ['acme/b' => ['reference' => 'ref-b']]], + ]), + ); + + // Unparseable shapes return null, so the caller falls back to a full re-analysis. + $this->assertNull($resolver->extractComposerPackageVersions(null)); + $this->assertNull($resolver->extractComposerPackageVersions('not-an-array')); + $this->assertNull($resolver->extractComposerPackageVersions([['no-versions-key' => []]])); + } + + public function testGetChangedComposerPackages(): void + { + $resolver = new PackageDependencyResolver([], self::getContainer()->getByType(FileHelper::class)); + + $cached = ['composerInstalled' => [['versions' => [ + 'acme/stable' => ['reference' => 'r1'], + 'acme/bumped' => ['reference' => 'old'], + 'acme/removed' => ['reference' => 'r2'], + ]]]]; + $current = ['composerInstalled' => [['versions' => [ + 'acme/stable' => ['reference' => 'r1'], + 'acme/bumped' => ['reference' => 'new'], + 'acme/added' => ['reference' => 'r3'], + ]]]]; + + // A bumped reference, a newly added package and a removed package all count as changed; + // the unchanged package does not. + $this->assertSame( + ['acme/bumped', 'acme/added', 'acme/removed'], + $resolver->getChangedComposerPackages($cached, $current), + ); + + // Identical metas mean nothing changed. + $this->assertSame([], $resolver->getChangedComposerPackages($cached, $cached)); + + // An unparseable meta returns null, so the caller falls back to a full re-analysis. + $this->assertNull($resolver->getChangedComposerPackages(['composerInstalled' => 'broken'], $current)); + } + +} diff --git a/tests/PHPStan/Dependency/data/package-resolver/composer.json b/tests/PHPStan/Dependency/data/package-resolver/composer.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/tests/PHPStan/Dependency/data/package-resolver/composer.json @@ -0,0 +1 @@ +{} diff --git a/tests/PHPStan/Dependency/data/package-resolver/vendor/composer/installed.php b/tests/PHPStan/Dependency/data/package-resolver/vendor/composer/installed.php new file mode 100644 index 00000000000..4f4adaa18cf --- /dev/null +++ b/tests/PHPStan/Dependency/data/package-resolver/vendor/composer/installed.php @@ -0,0 +1,24 @@ + [ + 'name' => 'acme/project', + 'install_path' => __DIR__ . '/../../', + ], + 'versions' => [ + 'acme/project' => [ + 'pretty_version' => '1.0.0+no-version-set', + 'install_path' => __DIR__ . '/../../', + ], + 'acme/widget' => [ + 'pretty_version' => '1.0.0', + 'reference' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + 'install_path' => __DIR__ . '/../acme/widget', + ], + 'acme/gadget' => [ + 'pretty_version' => '2.0.0', + 'reference' => 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + 'install_path' => __DIR__ . '/../acme/gadget', + ], + ], +];