diff --git a/conf/services.neon b/conf/services.neon index bccbdbcf5c4..c5c1d4019ca 100644 --- a/conf/services.neon +++ b/conf/services.neon @@ -105,6 +105,7 @@ services: composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths% allConfigFiles: %allConfigFiles% configPhpVersion: %phpVersion% + simpleRelativePathHelper: @simpleRelativePathHelper autowired: false # not registered using attributes because there is 2+ instances diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 971e51ec9fc..bb51fac85b5 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -234,12 +234,6 @@ parameters: count: 1 path: src/DependencyInjection/NeonAdapter.php - - - rawMessage: Creating new ReflectionClass is a runtime reflection concept that might not work in PHPStan because it uses fully static reflection engine. Use objects retrieved from ReflectionProvider instead. - identifier: phpstanApi.runtimeReflection - count: 1 - path: src/Diagnose/PHPStanDiagnoseExtension.php - - rawMessage: 'Parameter #1 $path of function dirname expects string, string|false given.' identifier: argument.type diff --git a/src/Analyser/Analyser.php b/src/Analyser/Analyser.php index b4928e66550..475781e17b6 100644 --- a/src/Analyser/Analyser.php +++ b/src/Analyser/Analyser.php @@ -36,7 +36,7 @@ public function __construct( /** * @param string[] $files * @param Closure(string $file): void|null $preFileCallback - * @param Closure(int, list): void|null $postFileCallback + * @param Closure(int, list=): void|null $postFileCallback * @param string[]|null $allAnalysedFiles */ public function analyse( @@ -75,6 +75,7 @@ public function analyse( $dependencies = []; $usedTraitDependencies = []; $exportedNodes = []; + $allProcessedFiles = []; foreach ($files as $file) { if ($preFileCallback !== null) { $preFileCallback($file); @@ -92,6 +93,7 @@ public function analyse( $filteredPhpErrors = array_merge($filteredPhpErrors, $fileAnalyserResult->getFilteredPhpErrors()); $allPhpErrors = array_merge($allPhpErrors, $fileAnalyserResult->getAllPhpErrors()); $processedFiles = $fileAnalyserResult->getProcessedFiles(); + $allProcessedFiles = array_merge($allProcessedFiles, $processedFiles); $locallyIgnoredErrors = array_merge($locallyIgnoredErrors, $fileAnalyserResult->getLocallyIgnoredErrors()); $linesToIgnore[$file] = $fileAnalyserResult->getLinesToIgnore(); @@ -143,6 +145,7 @@ public function analyse( exportedNodes: $exportedNodes, reachedInternalErrorsCountLimit: $reachedInternalErrorsCountLimit, peakMemoryUsageBytes: memory_get_peak_usage(true), + processedFiles: $allProcessedFiles, ); } diff --git a/src/Analyser/AnalyserResult.php b/src/Analyser/AnalyserResult.php index 576471d1ff3..31b88e27300 100644 --- a/src/Analyser/AnalyserResult.php +++ b/src/Analyser/AnalyserResult.php @@ -28,6 +28,7 @@ final class AnalyserResult * @param array>|null $dependencies * @param array>|null $usedTraitDependencies * @param array> $exportedNodes + * @param list $processedFiles */ public function __construct( private array $unorderedErrors, @@ -43,6 +44,7 @@ public function __construct( private array $exportedNodes, private bool $reachedInternalErrorsCountLimit, private int $peakMemoryUsageBytes, + private array $processedFiles, ) { } @@ -169,4 +171,12 @@ public function getPeakMemoryUsageBytes(): int return $this->peakMemoryUsageBytes; } + /** + * @return list + */ + public function getProcessedFiles(): array + { + return $this->processedFiles; + } + } diff --git a/src/Analyser/AnalyserResultFinalizer.php b/src/Analyser/AnalyserResultFinalizer.php index e4d5cbd7587..627c946e691 100644 --- a/src/Analyser/AnalyserResultFinalizer.php +++ b/src/Analyser/AnalyserResultFinalizer.php @@ -148,6 +148,7 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $ exportedNodes: $analyserResult->getExportedNodes(), reachedInternalErrorsCountLimit: $analyserResult->hasReachedInternalErrorsCountLimit(), peakMemoryUsageBytes: $analyserResult->getPeakMemoryUsageBytes(), + processedFiles: $analyserResult->getProcessedFiles(), ), $collectorErrors, $locallyIgnoredCollectorErrors); } @@ -167,6 +168,7 @@ private function mergeFilteredPhpErrors(AnalyserResult $analyserResult): Analyse exportedNodes: $analyserResult->getExportedNodes(), reachedInternalErrorsCountLimit: $analyserResult->hasReachedInternalErrorsCountLimit(), peakMemoryUsageBytes: $analyserResult->getPeakMemoryUsageBytes(), + processedFiles: $analyserResult->getProcessedFiles(), ); } @@ -231,6 +233,7 @@ private function addUnmatchedIgnoredErrors( exportedNodes: $analyserResult->getExportedNodes(), reachedInternalErrorsCountLimit: $analyserResult->hasReachedInternalErrorsCountLimit(), peakMemoryUsageBytes: $analyserResult->getPeakMemoryUsageBytes(), + processedFiles: $analyserResult->getProcessedFiles(), ), $collectorErrors, $locallyIgnoredCollectorErrors, diff --git a/src/Analyser/ResultCache/ResultCacheManager.php b/src/Analyser/ResultCache/ResultCacheManager.php index 3b929cc59ee..4172421e6e7 100644 --- a/src/Analyser/ResultCache/ResultCacheManager.php +++ b/src/Analyser/ResultCache/ResultCacheManager.php @@ -760,6 +760,7 @@ public function process(AnalyserResult $analyserResult, ResultCache $resultCache exportedNodes: $exportedNodes, reachedInternalErrorsCountLimit: $analyserResult->hasReachedInternalErrorsCountLimit(), peakMemoryUsageBytes: $analyserResult->getPeakMemoryUsageBytes(), + processedFiles: $analyserResult->getProcessedFiles(), ), $saved); } diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php index 3b6d5842b4f..e78212f492c 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -80,6 +80,7 @@ public function analyse( $collectedData = []; $savedResultCache = false; $memoryUsageBytes = memory_get_peak_usage(true); + $processedFiles = []; if ($errorOutput->isVeryVerbose()) { $errorOutput->writeLineFormatted('Result cache was not saved because of ignoredErrorHelperResult errors.'); } @@ -121,9 +122,12 @@ public function analyse( exportedNodes: $intermediateAnalyserResult->getExportedNodes(), reachedInternalErrorsCountLimit: $intermediateAnalyserResult->hasReachedInternalErrorsCountLimit(), peakMemoryUsageBytes: $intermediateAnalyserResult->getPeakMemoryUsageBytes(), + processedFiles: $intermediateAnalyserResult->getProcessedFiles(), ); } + $processedFiles = $intermediateAnalyserResult->getProcessedFiles(); + $resultCacheResult = $resultCacheManager->process($intermediateAnalyserResult, $resultCache, $errorOutput, $onlyFiles, true); $analyserResult = $this->analyserResultFinalizer->finalize( $this->switchTmpFileInAnalyserResult($resultCacheResult->getAnalyserResult(), $insteadOfFile, $tmpFile), @@ -184,6 +188,7 @@ public function analyse( $memoryUsageBytes, $isResultCacheUsed, $changedProjectExtensionFilesOutsideOfAnalysedPaths, + $processedFiles, ); } @@ -239,6 +244,7 @@ private function runAnalyser( exportedNodes: [], reachedInternalErrorsCountLimit: false, peakMemoryUsageBytes: memory_get_peak_usage(true), + processedFiles: [], ); } @@ -346,6 +352,7 @@ private function switchTmpFileInAnalyserResult( exportedNodes: $exportedNodes, reachedInternalErrorsCountLimit: $analyserResult->hasReachedInternalErrorsCountLimit(), peakMemoryUsageBytes: $analyserResult->getPeakMemoryUsageBytes(), + processedFiles: $analyserResult->getProcessedFiles(), ); } diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index d63265fcf84..f7865ad7508 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -459,7 +459,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($generateBaselineFile !== null) { - $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput(), $analysisResult->getProcessedFiles()); if (count($internalErrorsTuples) > 0) { foreach ($internalErrorsTuples as [$internalError]) { $inceptionResult->getStdOutput()->writeLineFormatted($internalError->getMessage()); @@ -493,11 +493,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $analysisResult->getPeakMemoryUsageBytes(), $analysisResult->isResultCacheUsed(), $analysisResult->getChangedProjectExtensionFilesOutsideOfAnalysedPaths(), + $analysisResult->getProcessedFiles(), ); $exitCode = $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()); - $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput(), $analysisResult->getProcessedFiles()); $errorOutput->writeLineFormatted('⚠️ Result is incomplete because of severe errors. ⚠️'); $errorOutput->writeLineFormatted(' Fix these errors first and then re-run PHPStan'); @@ -649,7 +650,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } - $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput(), $analysisResult->getProcessedFiles()); return $inceptionResult->handleReturn( $exitCode, @@ -847,7 +848,10 @@ private function runFixer(InceptionResult $inceptionResult, Container $container ); } - private function runDiagnoseExtensions(Container $container, Output $errorOutput): void + /** + * @param list $processedFiles + */ + private function runDiagnoseExtensions(Container $container, Output $errorOutput, array $processedFiles = []): void { if (!$errorOutput->isDebug()) { return; @@ -857,7 +861,7 @@ private function runDiagnoseExtensions(Container $container, Output $errorOutput $phpstanDiagnoseExtension = $container->getService('phpstanDiagnoseExtension'); // not using tag for this extension to make sure it's always first - $phpstanDiagnoseExtension->print($errorOutput); + $phpstanDiagnoseExtension->print($errorOutput, $processedFiles); /** @var DiagnoseExtension $extension */ foreach ($container->getServicesByTag(DiagnoseExtension::EXTENSION_TAG) as $extension) { diff --git a/src/Command/AnalyserRunner.php b/src/Command/AnalyserRunner.php index 1da03e53ed3..e0642493890 100644 --- a/src/Command/AnalyserRunner.php +++ b/src/Command/AnalyserRunner.php @@ -68,6 +68,7 @@ public function runAnalyser( exportedNodes: [], reachedInternalErrorsCountLimit: false, peakMemoryUsageBytes: memory_get_peak_usage(true), + processedFiles: [], ); } diff --git a/src/Command/AnalysisResult.php b/src/Command/AnalysisResult.php index ad7162b32bf..bdcb5fd1961 100644 --- a/src/Command/AnalysisResult.php +++ b/src/Command/AnalysisResult.php @@ -24,6 +24,7 @@ final class AnalysisResult * @param list $warnings * @param list $collectedData * @param array $changedProjectExtensionFilesOutsideOfAnalysedPaths + * @param list $processedFiles */ public function __construct( array $fileSpecificErrors, @@ -37,6 +38,7 @@ public function __construct( private int $peakMemoryUsageBytes, private bool $isResultCacheUsed, private array $changedProjectExtensionFilesOutsideOfAnalysedPaths, + private array $processedFiles = [], ) { usort( @@ -148,6 +150,14 @@ public function getChangedProjectExtensionFilesOutsideOfAnalysedPaths(): array return $this->changedProjectExtensionFilesOutsideOfAnalysedPaths; } + /** + * @return list + */ + public function getProcessedFiles(): array + { + return $this->processedFiles; + } + /** * @api * @param list $fileSpecificErrors @@ -166,6 +176,7 @@ public function withFileSpecificErrors(array $fileSpecificErrors): self $this->peakMemoryUsageBytes, $this->isResultCacheUsed, $this->changedProjectExtensionFilesOutsideOfAnalysedPaths, + $this->processedFiles, ); } diff --git a/src/Command/DiagnoseCommand.php b/src/Command/DiagnoseCommand.php index 03d8d784558..371bfcfa1aa 100644 --- a/src/Command/DiagnoseCommand.php +++ b/src/Command/DiagnoseCommand.php @@ -99,7 +99,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $phpstanDiagnoseExtension = $container->getService('phpstanDiagnoseExtension'); // not using tag for this extension to make sure it's always first - $phpstanDiagnoseExtension->print($output); + $phpstanDiagnoseExtension->print($output, []); /** @var DiagnoseExtension $extension */ foreach ($container->getServicesByTag(DiagnoseExtension::EXTENSION_TAG) as $extension) { diff --git a/src/Command/FixerWorkerCommand.php b/src/Command/FixerWorkerCommand.php index a0d24fbaa56..cfe2af34c9e 100644 --- a/src/Command/FixerWorkerCommand.php +++ b/src/Command/FixerWorkerCommand.php @@ -404,6 +404,7 @@ private function runAnalyser(LoopInterface $loop, Container $container, array $f exportedNodes: [], reachedInternalErrorsCountLimit: false, peakMemoryUsageBytes: memory_get_peak_usage(true), + processedFiles: [], )); } diff --git a/src/Command/WorkerCommand.php b/src/Command/WorkerCommand.php index 2a221f6e846..d5f42b87956 100644 --- a/src/Command/WorkerCommand.php +++ b/src/Command/WorkerCommand.php @@ -228,6 +228,7 @@ private function runWorker( $dependencies = []; $usedTraitDependencies = []; $exportedNodes = []; + $processedFiles = []; foreach ($files as $file) { try { if ($file === $insteadOfFile) { @@ -242,6 +243,7 @@ private function runWorker( $dependencies[$file] = $fileAnalyserResult->getDependencies(); $usedTraitDependencies[$file] = $fileAnalyserResult->getUsedTraitDependencies(); $exportedNodes[$file] = $fileAnalyserResult->getExportedNodes(); + $processedFiles = array_merge($processedFiles, $fileAnalyserResult->getProcessedFiles()); foreach ($fileErrors as $fileError) { $errors[] = $fileError; } @@ -283,6 +285,7 @@ private function runWorker( 'usedTraitDependencies' => $usedTraitDependencies, 'exportedNodes' => $exportedNodes, 'files' => $files, + 'processedFiles' => $processedFiles, 'internalErrorsCount' => $internalErrorsCount, ]]); }); diff --git a/src/Diagnose/PHPStanDiagnoseExtension.php b/src/Diagnose/PHPStanDiagnoseExtension.php index 35cb6a862e4..37f9f021b25 100644 --- a/src/Diagnose/PHPStanDiagnoseExtension.php +++ b/src/Diagnose/PHPStanDiagnoseExtension.php @@ -6,12 +6,15 @@ use PHPStan\Command\Output; use PHPStan\ExtensionInstaller\GeneratedConfig; use PHPStan\File\FileHelper; +use PHPStan\File\RelativePathHelper; use PHPStan\Internal\ComposerHelper; use PHPStan\Php\ComposerPhpVersionFactory; use PHPStan\Php\PhpVersion; use ReflectionClass; +use function array_count_values; use function array_key_exists; use function array_slice; +use function arsort; use function class_exists; use function count; use function dirname; @@ -27,7 +30,7 @@ use function substr; use const PHP_VERSION_ID; -final class PHPStanDiagnoseExtension implements DiagnoseExtension +final class PHPStanDiagnoseExtension { /** @@ -42,11 +45,15 @@ public function __construct( private array $composerAutoloaderProjectPaths, private array $allConfigFiles, private ComposerPhpVersionFactory $composerPhpVersionFactory, + private RelativePathHelper $simpleRelativePathHelper, ) { } - public function print(Output $output): void + /** + * @param list $processedFiles + */ + public function print(Output $output, array $processedFiles): void { $phpRuntimeVersion = new PhpVersion(PHP_VERSION_ID); $output->writeLineFormatted(sprintf( @@ -203,6 +210,45 @@ public function print(Output $output): void $output->writeLineFormatted($composerAutoloaderProjectPath); } $output->writeLineFormatted(''); + + $topFiles = $this->getTopMostAnalysedFiles($processedFiles, 5); + if (count($topFiles) <= 0) { + return; + } + + $output->writeLineFormatted('Most often analysed files:'); + foreach ($topFiles as $file => $count) { + $output->writeLineFormatted(sprintf( + ' %s: %d times', + $this->simpleRelativePathHelper->getRelativePath($file), + $count, + )); + } + $output->writeLineFormatted(''); + } + + /** + * @param list $processedFiles + * @return array> + */ + private function getTopMostAnalysedFiles(array $processedFiles, int $limit): array + { + if ($processedFiles === []) { + return []; + } + + $counts = array_count_values($processedFiles); + arsort($counts); + + $result = []; + foreach (array_slice($counts, 0, $limit, true) as $file => $count) { + if ($count <= 1) { + continue; + } + $result[$file] = $count; + } + + return $result; } } diff --git a/src/Parallel/ParallelAnalyser.php b/src/Parallel/ParallelAnalyser.php index 7792a0cb6ba..248b52a4a34 100644 --- a/src/Parallel/ParallelAnalyser.php +++ b/src/Parallel/ParallelAnalyser.php @@ -58,7 +58,7 @@ public function __construct( } /** - * @param Closure(int ): void|null $postFileCallback + * @param Closure(int, list=): void|null $postFileCallback * @param (callable(list, list, string[]): void)|null $onFileAnalysisHandler * @return PromiseInterface */ @@ -92,12 +92,14 @@ public function analyse( $usedTraitDependencies = []; $reachedInternalErrorsCountLimit = false; $exportedNodes = []; + /** @var list $allProcessedFiles */ + $allProcessedFiles = []; /** @var Deferred $deferred */ $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): void { + $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 { if (count($jobs) > 0 && $internalErrorsCount === 0) { $internalErrors[] = new InternalError( 'Some parallel worker jobs have not finished.', @@ -123,6 +125,7 @@ public function analyse( exportedNodes: $exportedNodes, reachedInternalErrorsCountLimit: $reachedInternalErrorsCountLimit, peakMemoryUsageBytes: array_sum($peakMemoryUsages), // not 100% correct as the peak usages of workers might not have met + processedFiles: $allProcessedFiles, )); }); $server->on('connection', function (ConnectionInterface $connection) use (&$jobs): void { @@ -194,7 +197,7 @@ public function analyse( $commandOptions, $input, ), $loop, $this->processTimeout); - $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): void { + $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 { $fileErrors = []; foreach ($json['errors'] as $jsonError) { $fileErrors[] = Error::decode($jsonError); @@ -281,8 +284,12 @@ public function analyse( }, $fileExportedNodes); } + foreach ($json['processedFiles'] as $processedFile) { + $allProcessedFiles[] = $processedFile; + } + if ($postFileCallback !== null) { - $postFileCallback(count($json['files'])); + $postFileCallback(count($json['files']), $json['processedFiles']); } if (!isset($peakMemoryUsages[$processIdentifier]) || $peakMemoryUsages[$processIdentifier] < $json['memoryUsage']) {