From 19b3ca3b806ffbc9fcfd200fc67a83672782c2a5 Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Sat, 4 Apr 2026 08:07:33 +0000 Subject: [PATCH 01/13] List most-often-analysed trait files in -vvv diagnose output - Added ProcessedFilesCollector service to accumulate processed files during analysis - Added TraitAnalysisDiagnoseExtension to print top 5 most-analysed files in -vvv output - Modified WorkerCommand to send processedFiles in parallel worker JSON responses - Modified ParallelAnalyser to pass processedFiles through postFileCallback - Modified AnalyseApplication to collect processedFiles in both debug and non-debug modes - Added unit tests for ProcessedFilesCollector and TraitAnalysisDiagnoseExtension --- conf/services.neon | 10 +++ src/Command/AnalyseApplication.php | 10 ++- src/Command/WorkerCommand.php | 3 + src/Diagnose/ProcessedFilesCollector.php | 44 +++++++++++++ .../TraitAnalysisDiagnoseExtension.php | 39 ++++++++++++ src/Parallel/ParallelAnalyser.php | 4 +- .../Diagnose/ProcessedFilesCollectorTest.php | 62 +++++++++++++++++++ .../TraitAnalysisDiagnoseExtensionTest.php | 60 ++++++++++++++++++ 8 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 src/Diagnose/ProcessedFilesCollector.php create mode 100644 src/Diagnose/TraitAnalysisDiagnoseExtension.php create mode 100644 tests/PHPStan/Diagnose/ProcessedFilesCollectorTest.php create mode 100644 tests/PHPStan/Diagnose/TraitAnalysisDiagnoseExtensionTest.php diff --git a/conf/services.neon b/conf/services.neon index bccbdbcf5c4..ca2a50a6ec0 100644 --- a/conf/services.neon +++ b/conf/services.neon @@ -107,6 +107,16 @@ services: configPhpVersion: %phpVersion% autowired: false + - + class: PHPStan\Diagnose\ProcessedFilesCollector + + - + class: PHPStan\Diagnose\TraitAnalysisDiagnoseExtension + arguments: + simpleRelativePathHelper: @simpleRelativePathHelper + tags: + - phpstan.diagnoseExtension + # not registered using attributes because there is 2+ instances - class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php index 3b6d5842b4f..5a56de2dcce 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -10,6 +10,7 @@ use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; use PHPStan\Collectors\CollectedData; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Diagnose\ProcessedFilesCollector; use PHPStan\Internal\BytesHelper; use PHPStan\PhpDoc\StubFilesProvider; use PHPStan\PhpDoc\StubValidator; @@ -17,6 +18,7 @@ use Symfony\Component\Console\Input\InputInterface; use function array_merge; use function array_unique; +use function array_values; use function count; use function fclose; use function feof; @@ -43,6 +45,7 @@ public function __construct( private ResultCacheManagerFactory $resultCacheManagerFactory, private IgnoredErrorHelper $ignoredErrorHelper, private StubFilesProvider $stubFilesProvider, + private ProcessedFilesCollector $processedFilesCollector, ) { } @@ -244,8 +247,9 @@ private function runAnalyser( if (!$debug) { $preFileCallback = null; - $postFileCallback = static function (int $step) use ($errorOutput): void { + $postFileCallback = function (int $step, array $processedFiles = []) use ($errorOutput): void { $errorOutput->getStyle()->progressAdvance($step); + $this->processedFilesCollector->addProcessedFiles(array_values($processedFiles)); }; $errorOutput->getStyle()->progressStart($allAnalysedFilesCount); @@ -259,7 +263,7 @@ private function runAnalyser( $postFileCallback = null; if ($stdOutput->isDebug()) { $previousMemory = memory_get_peak_usage(true); - $postFileCallback = static function (int $step, array $processedFiles = []) use ($stdOutput, &$previousMemory, &$startTime, &$linesOfCode): void { + $postFileCallback = function (int $step, array $processedFiles = []) use ($stdOutput, &$previousMemory, &$startTime, &$linesOfCode): void { if ($startTime === null) { throw new ShouldNotHappenException(); } @@ -280,6 +284,8 @@ private function runAnalyser( fclose($handle); } + $this->processedFilesCollector->addProcessedFiles(array_values($processedFiles)); + $stdOutput->writeLineFormatted(sprintf('--- consumed %s, total %s, took %.2f s, %.3f LoC/s', BytesHelper::bytes($currentTotalMemory - $previousMemory), BytesHelper::bytes($currentTotalMemory), $elapsedTime, $linesOfCode / $elapsedTime)); $previousMemory = $currentTotalMemory; }; 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/ProcessedFilesCollector.php b/src/Diagnose/ProcessedFilesCollector.php new file mode 100644 index 00000000000..91025ce4005 --- /dev/null +++ b/src/Diagnose/ProcessedFilesCollector.php @@ -0,0 +1,44 @@ + */ + private array $processedFiles = []; + + /** + * @param list $files + */ + public function addProcessedFiles(array $files): void + { + foreach ($files as $file) { + $this->processedFiles[] = $file; + } + } + + /** + * @return array + */ + public function getTopMostAnalysedFiles(int $limit): array + { + $counts = array_count_values($this->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/Diagnose/TraitAnalysisDiagnoseExtension.php b/src/Diagnose/TraitAnalysisDiagnoseExtension.php new file mode 100644 index 00000000000..0b49e87b96b --- /dev/null +++ b/src/Diagnose/TraitAnalysisDiagnoseExtension.php @@ -0,0 +1,39 @@ +processedFilesCollector->getTopMostAnalysedFiles(5); + if (count($topFiles) === 0) { + return; + } + + $output->writeLineFormatted('Most often analysed files (likely trait files):'); + foreach ($topFiles as $file => $count) { + $output->writeLineFormatted(sprintf( + ' %s: %d %s', + $this->simpleRelativePathHelper->getRelativePath($file), + $count, + $count === 1 ? 'time' : 'times', + )); + } + $output->writeLineFormatted(''); + } + +} diff --git a/src/Parallel/ParallelAnalyser.php b/src/Parallel/ParallelAnalyser.php index 7792a0cb6ba..26283e45fdd 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 */ @@ -282,7 +282,7 @@ public function analyse( } if ($postFileCallback !== null) { - $postFileCallback(count($json['files'])); + $postFileCallback(count($json['files']), $json['processedFiles'] ?? []); } if (!isset($peakMemoryUsages[$processIdentifier]) || $peakMemoryUsages[$processIdentifier] < $json['memoryUsage']) { diff --git a/tests/PHPStan/Diagnose/ProcessedFilesCollectorTest.php b/tests/PHPStan/Diagnose/ProcessedFilesCollectorTest.php new file mode 100644 index 00000000000..85b3686f21d --- /dev/null +++ b/tests/PHPStan/Diagnose/ProcessedFilesCollectorTest.php @@ -0,0 +1,62 @@ +assertSame([], $collector->getTopMostAnalysedFiles(5)); + } + + public function testSingleFileNotReported(): void + { + $collector = new ProcessedFilesCollector(); + $collector->addProcessedFiles(['/path/to/file.php']); + $this->assertSame([], $collector->getTopMostAnalysedFiles(5)); + } + + public function testTopMostAnalysedFiles(): void + { + $collector = new ProcessedFilesCollector(); + + // Simulate: file A uses trait T1 and T2, file B uses trait T1 + $collector->addProcessedFiles(['/path/to/A.php', '/path/to/T1.php', '/path/to/T2.php']); + $collector->addProcessedFiles(['/path/to/B.php', '/path/to/T1.php']); + + $top = $collector->getTopMostAnalysedFiles(5); + $this->assertSame(['/path/to/T1.php' => 2], $top); + } + + public function testLimit(): void + { + $collector = new ProcessedFilesCollector(); + + // Create 7 trait files with varying usage counts + for ($i = 0; $i < 7; $i++) { + $files = ['/path/to/main' . $i . '.php']; + for ($j = 0; $j <= $i; $j++) { + $files[] = '/path/to/trait' . $j . '.php'; + } + $collector->addProcessedFiles($files); + } + + $top = $collector->getTopMostAnalysedFiles(3); + $this->assertCount(3, $top); + + // trait0.php used 7 times, trait1.php 6 times, trait2.php 5 times + $files = array_keys($top); + $this->assertSame('/path/to/trait0.php', $files[0]); + $this->assertSame(7, $top['/path/to/trait0.php']); + $this->assertSame('/path/to/trait1.php', $files[1]); + $this->assertSame(6, $top['/path/to/trait1.php']); + $this->assertSame('/path/to/trait2.php', $files[2]); + $this->assertSame(5, $top['/path/to/trait2.php']); + } + +} diff --git a/tests/PHPStan/Diagnose/TraitAnalysisDiagnoseExtensionTest.php b/tests/PHPStan/Diagnose/TraitAnalysisDiagnoseExtensionTest.php new file mode 100644 index 00000000000..5870b201cbb --- /dev/null +++ b/tests/PHPStan/Diagnose/TraitAnalysisDiagnoseExtensionTest.php @@ -0,0 +1,60 @@ +createOutput($lines); + + $extension->print($output); + $this->assertSame([], $lines); + } + + public function testPrintsTopFiles(): void + { + $collector = new ProcessedFilesCollector(); + $collector->addProcessedFiles(['/src/A.php', '/src/Trait1.php', '/src/Trait2.php']); + $collector->addProcessedFiles(['/src/B.php', '/src/Trait1.php', '/src/Trait2.php']); + $collector->addProcessedFiles(['/src/C.php', '/src/Trait1.php']); + + $extension = new TraitAnalysisDiagnoseExtension($collector, new NullRelativePathHelper()); + + $lines = []; + $output = $this->createOutput($lines); + + $extension->print($output); + + $this->assertCount(4, $lines); + $this->assertStringContainsString('Most often analysed files', $lines[0]); + $this->assertStringContainsString('/src/Trait1.php', $lines[1]); + $this->assertStringContainsString('3 times', $lines[1]); + $this->assertStringContainsString('/src/Trait2.php', $lines[2]); + $this->assertStringContainsString('2 times', $lines[2]); + $this->assertSame('', $lines[3]); + } + + /** + * @param list $lines + */ + private function createOutput(array &$lines): Output + { + $output = $this->createMock(Output::class); + $output->method('writeLineFormatted')->willReturnCallback(static function (string $message) use (&$lines): void { + $lines[] = $message; + }); + + return $output; + } + +} From ede5820ec445572f3297b8b157259a8b702bd6c3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 4 Apr 2026 10:32:33 +0200 Subject: [PATCH 02/13] simplify --- src/Diagnose/ProcessedFilesCollector.php | 2 +- src/Diagnose/TraitAnalysisDiagnoseExtension.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Diagnose/ProcessedFilesCollector.php b/src/Diagnose/ProcessedFilesCollector.php index 91025ce4005..228420ceb25 100644 --- a/src/Diagnose/ProcessedFilesCollector.php +++ b/src/Diagnose/ProcessedFilesCollector.php @@ -23,7 +23,7 @@ public function addProcessedFiles(array $files): void } /** - * @return array + * @return array> */ public function getTopMostAnalysedFiles(int $limit): array { diff --git a/src/Diagnose/TraitAnalysisDiagnoseExtension.php b/src/Diagnose/TraitAnalysisDiagnoseExtension.php index 0b49e87b96b..bd752234b92 100644 --- a/src/Diagnose/TraitAnalysisDiagnoseExtension.php +++ b/src/Diagnose/TraitAnalysisDiagnoseExtension.php @@ -27,10 +27,9 @@ public function print(Output $output): void $output->writeLineFormatted('Most often analysed files (likely trait files):'); foreach ($topFiles as $file => $count) { $output->writeLineFormatted(sprintf( - ' %s: %d %s', + ' %s: %d times', $this->simpleRelativePathHelper->getRelativePath($file), $count, - $count === 1 ? 'time' : 'times', )); } $output->writeLineFormatted(''); From ecbb2d789b385e09afe2f82f53f8028b4a250705 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 4 Apr 2026 10:33:10 +0200 Subject: [PATCH 03/13] Update TraitAnalysisDiagnoseExtension.php --- src/Diagnose/TraitAnalysisDiagnoseExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Diagnose/TraitAnalysisDiagnoseExtension.php b/src/Diagnose/TraitAnalysisDiagnoseExtension.php index bd752234b92..b11c85e9a8a 100644 --- a/src/Diagnose/TraitAnalysisDiagnoseExtension.php +++ b/src/Diagnose/TraitAnalysisDiagnoseExtension.php @@ -24,7 +24,7 @@ public function print(Output $output): void return; } - $output->writeLineFormatted('Most often analysed files (likely trait files):'); + $output->writeLineFormatted('Most often analysed files:'); foreach ($topFiles as $file => $count) { $output->writeLineFormatted(sprintf( ' %s: %d times', From e5d11e759a81df18d657f8d9864b7db7a09b9ec5 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 4 Apr 2026 10:35:18 +0200 Subject: [PATCH 04/13] Update ParallelAnalyser.php --- src/Parallel/ParallelAnalyser.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Parallel/ParallelAnalyser.php b/src/Parallel/ParallelAnalyser.php index 26283e45fdd..10c1e6491d5 100644 --- a/src/Parallel/ParallelAnalyser.php +++ b/src/Parallel/ParallelAnalyser.php @@ -282,7 +282,7 @@ public function analyse( } if ($postFileCallback !== null) { - $postFileCallback(count($json['files']), $json['processedFiles'] ?? []); + $postFileCallback(count($json['files']), $json['processedFiles']); } if (!isset($peakMemoryUsages[$processIdentifier]) || $peakMemoryUsages[$processIdentifier] < $json['memoryUsage']) { From f3b2343325a80ff683fea95b1f4bf3d2367a1b8e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 4 Apr 2026 10:37:08 +0200 Subject: [PATCH 05/13] better names --- conf/services.neon | 2 +- ...sion.php => ProcessedFilesAnalysisDiagnoseExtension.php} | 2 +- ....php => ProcessedFilesAnalysisDiagnoseExtensionTest.php} | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/Diagnose/{TraitAnalysisDiagnoseExtension.php => ProcessedFilesAnalysisDiagnoseExtension.php} (90%) rename tests/PHPStan/Diagnose/{TraitAnalysisDiagnoseExtensionTest.php => ProcessedFilesAnalysisDiagnoseExtensionTest.php} (84%) diff --git a/conf/services.neon b/conf/services.neon index ca2a50a6ec0..405086e6167 100644 --- a/conf/services.neon +++ b/conf/services.neon @@ -111,7 +111,7 @@ services: class: PHPStan\Diagnose\ProcessedFilesCollector - - class: PHPStan\Diagnose\TraitAnalysisDiagnoseExtension + class: PHPStan\Diagnose\ProcessedFilesAnalysisDiagnoseExtension arguments: simpleRelativePathHelper: @simpleRelativePathHelper tags: diff --git a/src/Diagnose/TraitAnalysisDiagnoseExtension.php b/src/Diagnose/ProcessedFilesAnalysisDiagnoseExtension.php similarity index 90% rename from src/Diagnose/TraitAnalysisDiagnoseExtension.php rename to src/Diagnose/ProcessedFilesAnalysisDiagnoseExtension.php index b11c85e9a8a..e9ad34e6545 100644 --- a/src/Diagnose/TraitAnalysisDiagnoseExtension.php +++ b/src/Diagnose/ProcessedFilesAnalysisDiagnoseExtension.php @@ -7,7 +7,7 @@ use function count; use function sprintf; -final class TraitAnalysisDiagnoseExtension implements DiagnoseExtension +final class ProcessedFilesAnalysisDiagnoseExtension implements DiagnoseExtension { public function __construct( diff --git a/tests/PHPStan/Diagnose/TraitAnalysisDiagnoseExtensionTest.php b/tests/PHPStan/Diagnose/ProcessedFilesAnalysisDiagnoseExtensionTest.php similarity index 84% rename from tests/PHPStan/Diagnose/TraitAnalysisDiagnoseExtensionTest.php rename to tests/PHPStan/Diagnose/ProcessedFilesAnalysisDiagnoseExtensionTest.php index 5870b201cbb..96a3055056b 100644 --- a/tests/PHPStan/Diagnose/TraitAnalysisDiagnoseExtensionTest.php +++ b/tests/PHPStan/Diagnose/ProcessedFilesAnalysisDiagnoseExtensionTest.php @@ -6,13 +6,13 @@ use PHPStan\File\NullRelativePathHelper; use PHPUnit\Framework\TestCase; -class TraitAnalysisDiagnoseExtensionTest extends TestCase +class ProcessedFilesAnalysisDiagnoseExtensionTest extends TestCase { public function testNoOutput(): void { $collector = new ProcessedFilesCollector(); - $extension = new TraitAnalysisDiagnoseExtension($collector, new NullRelativePathHelper()); + $extension = new ProcessedFilesAnalysisDiagnoseExtension($collector, new NullRelativePathHelper()); $lines = []; $output = $this->createOutput($lines); @@ -28,7 +28,7 @@ public function testPrintsTopFiles(): void $collector->addProcessedFiles(['/src/B.php', '/src/Trait1.php', '/src/Trait2.php']); $collector->addProcessedFiles(['/src/C.php', '/src/Trait1.php']); - $extension = new TraitAnalysisDiagnoseExtension($collector, new NullRelativePathHelper()); + $extension = new ProcessedFilesAnalysisDiagnoseExtension($collector, new NullRelativePathHelper()); $lines = []; $output = $this->createOutput($lines); From efb9618aa527b9de2b4c2fcfcbc250032ed40b6a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 4 Apr 2026 18:05:27 +0200 Subject: [PATCH 06/13] move files into Analyser namespace --- conf/services.neon | 4 ++-- src/{Diagnose => Analyser}/ProcessedFilesCollector.php | 2 +- .../ProcessedFilesDiagnoseExtension.php} | 5 +++-- src/Command/AnalyseApplication.php | 2 +- .../ProcessedFilesCollectorTest.php | 2 +- .../ProcessedFilesDiagnoseExtensionTest.php} | 8 ++++---- 6 files changed, 12 insertions(+), 11 deletions(-) rename src/{Diagnose => Analyser}/ProcessedFilesCollector.php (96%) rename src/{Diagnose/ProcessedFilesAnalysisDiagnoseExtension.php => Analyser/ProcessedFilesDiagnoseExtension.php} (84%) rename tests/PHPStan/{Diagnose => Analyser}/ProcessedFilesCollectorTest.php (98%) rename tests/PHPStan/{Diagnose/ProcessedFilesAnalysisDiagnoseExtensionTest.php => Analyser/ProcessedFilesDiagnoseExtensionTest.php} (83%) diff --git a/conf/services.neon b/conf/services.neon index 405086e6167..a582327381d 100644 --- a/conf/services.neon +++ b/conf/services.neon @@ -108,10 +108,10 @@ services: autowired: false - - class: PHPStan\Diagnose\ProcessedFilesCollector + class: PHPStan\Analyser\ProcessedFilesCollector - - class: PHPStan\Diagnose\ProcessedFilesAnalysisDiagnoseExtension + class: PHPStan\Analyser\ProcessedFilesDiagnoseExtension arguments: simpleRelativePathHelper: @simpleRelativePathHelper tags: diff --git a/src/Diagnose/ProcessedFilesCollector.php b/src/Analyser/ProcessedFilesCollector.php similarity index 96% rename from src/Diagnose/ProcessedFilesCollector.php rename to src/Analyser/ProcessedFilesCollector.php index 228420ceb25..efec7306268 100644 --- a/src/Diagnose/ProcessedFilesCollector.php +++ b/src/Analyser/ProcessedFilesCollector.php @@ -1,6 +1,6 @@ createOutput($lines); @@ -28,7 +28,7 @@ public function testPrintsTopFiles(): void $collector->addProcessedFiles(['/src/B.php', '/src/Trait1.php', '/src/Trait2.php']); $collector->addProcessedFiles(['/src/C.php', '/src/Trait1.php']); - $extension = new ProcessedFilesAnalysisDiagnoseExtension($collector, new NullRelativePathHelper()); + $extension = new ProcessedFilesDiagnoseExtension($collector, new NullRelativePathHelper()); $lines = []; $output = $this->createOutput($lines); From 7485c0a46a28fc501dc9ceda86f2d1c5df660292 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 4 Apr 2026 18:11:59 +0200 Subject: [PATCH 07/13] register with attributes --- conf/services.neon | 10 ---------- src/Analyser/ProcessedFilesCollector.php | 2 ++ src/Analyser/ProcessedFilesDiagnoseExtension.php | 4 ++++ 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/conf/services.neon b/conf/services.neon index a582327381d..bccbdbcf5c4 100644 --- a/conf/services.neon +++ b/conf/services.neon @@ -107,16 +107,6 @@ services: configPhpVersion: %phpVersion% autowired: false - - - class: PHPStan\Analyser\ProcessedFilesCollector - - - - class: PHPStan\Analyser\ProcessedFilesDiagnoseExtension - arguments: - simpleRelativePathHelper: @simpleRelativePathHelper - tags: - - phpstan.diagnoseExtension - # not registered using attributes because there is 2+ instances - class: PHPStan\Type\Php\ReflectionGetAttributesMethodReturnTypeExtension diff --git a/src/Analyser/ProcessedFilesCollector.php b/src/Analyser/ProcessedFilesCollector.php index efec7306268..7837ae5d874 100644 --- a/src/Analyser/ProcessedFilesCollector.php +++ b/src/Analyser/ProcessedFilesCollector.php @@ -2,10 +2,12 @@ namespace PHPStan\Analyser; +use PHPStan\DependencyInjection\AutowiredService; use function array_count_values; use function array_slice; use function arsort; +#[AutowiredService] final class ProcessedFilesCollector { diff --git a/src/Analyser/ProcessedFilesDiagnoseExtension.php b/src/Analyser/ProcessedFilesDiagnoseExtension.php index 31aceea007b..c2c273fbcca 100644 --- a/src/Analyser/ProcessedFilesDiagnoseExtension.php +++ b/src/Analyser/ProcessedFilesDiagnoseExtension.php @@ -3,16 +3,20 @@ namespace PHPStan\Analyser; use PHPStan\Command\Output; +use PHPStan\DependencyInjection\AutowiredParameter; +use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Diagnose\DiagnoseExtension; use PHPStan\File\RelativePathHelper; use function count; use function sprintf; +#[AutowiredService] final class ProcessedFilesDiagnoseExtension implements DiagnoseExtension { public function __construct( private ProcessedFilesCollector $processedFilesCollector, + #[AutowiredParameter(ref: '@simpleRelativePathHelper')] private RelativePathHelper $simpleRelativePathHelper, ) { From a7e01aadb9742483913f86f1dcb04ed734b42453 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sun, 5 Apr 2026 08:25:47 +0000 Subject: [PATCH 08/13] Merge ProcessedFilesDiagnoseExtension into PHPStanDiagnoseExtension Address review feedback: instead of having a separate DiagnoseExtension for processed files, merge the logic into the existing PHPStanDiagnoseExtension. Co-Authored-By: Claude Opus 4.6 --- conf/services.neon | 1 + .../ProcessedFilesDiagnoseExtension.php | 43 ------------- src/Diagnose/PHPStanDiagnoseExtension.php | 17 ++++++ .../ProcessedFilesDiagnoseExtensionTest.php | 60 ------------------- 4 files changed, 18 insertions(+), 103 deletions(-) delete mode 100644 src/Analyser/ProcessedFilesDiagnoseExtension.php delete mode 100644 tests/PHPStan/Analyser/ProcessedFilesDiagnoseExtensionTest.php 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/src/Analyser/ProcessedFilesDiagnoseExtension.php b/src/Analyser/ProcessedFilesDiagnoseExtension.php deleted file mode 100644 index c2c273fbcca..00000000000 --- a/src/Analyser/ProcessedFilesDiagnoseExtension.php +++ /dev/null @@ -1,43 +0,0 @@ -processedFilesCollector->getTopMostAnalysedFiles(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(''); - } - -} diff --git a/src/Diagnose/PHPStanDiagnoseExtension.php b/src/Diagnose/PHPStanDiagnoseExtension.php index 35cb6a862e4..17bda3c754f 100644 --- a/src/Diagnose/PHPStanDiagnoseExtension.php +++ b/src/Diagnose/PHPStanDiagnoseExtension.php @@ -3,9 +3,11 @@ namespace PHPStan\Diagnose; use Phar; +use PHPStan\Analyser\ProcessedFilesCollector; 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; @@ -42,6 +44,8 @@ public function __construct( private array $composerAutoloaderProjectPaths, private array $allConfigFiles, private ComposerPhpVersionFactory $composerPhpVersionFactory, + private ProcessedFilesCollector $processedFilesCollector, + private RelativePathHelper $simpleRelativePathHelper, ) { } @@ -203,6 +207,19 @@ public function print(Output $output): void $output->writeLineFormatted($composerAutoloaderProjectPath); } $output->writeLineFormatted(''); + + $topFiles = $this->processedFilesCollector->getTopMostAnalysedFiles(5); + if (count($topFiles) > 0) { + $output->writeLineFormatted('Most often analysed files:'); + foreach ($topFiles as $file => $count) { + $output->writeLineFormatted(sprintf( + ' %s: %d times', + $this->simpleRelativePathHelper->getRelativePath($file), + $count, + )); + } + $output->writeLineFormatted(''); + } } } diff --git a/tests/PHPStan/Analyser/ProcessedFilesDiagnoseExtensionTest.php b/tests/PHPStan/Analyser/ProcessedFilesDiagnoseExtensionTest.php deleted file mode 100644 index 42ee7decd36..00000000000 --- a/tests/PHPStan/Analyser/ProcessedFilesDiagnoseExtensionTest.php +++ /dev/null @@ -1,60 +0,0 @@ -createOutput($lines); - - $extension->print($output); - $this->assertSame([], $lines); - } - - public function testPrintsTopFiles(): void - { - $collector = new ProcessedFilesCollector(); - $collector->addProcessedFiles(['/src/A.php', '/src/Trait1.php', '/src/Trait2.php']); - $collector->addProcessedFiles(['/src/B.php', '/src/Trait1.php', '/src/Trait2.php']); - $collector->addProcessedFiles(['/src/C.php', '/src/Trait1.php']); - - $extension = new ProcessedFilesDiagnoseExtension($collector, new NullRelativePathHelper()); - - $lines = []; - $output = $this->createOutput($lines); - - $extension->print($output); - - $this->assertCount(4, $lines); - $this->assertStringContainsString('Most often analysed files', $lines[0]); - $this->assertStringContainsString('/src/Trait1.php', $lines[1]); - $this->assertStringContainsString('3 times', $lines[1]); - $this->assertStringContainsString('/src/Trait2.php', $lines[2]); - $this->assertStringContainsString('2 times', $lines[2]); - $this->assertSame('', $lines[3]); - } - - /** - * @param list $lines - */ - private function createOutput(array &$lines): Output - { - $output = $this->createMock(Output::class); - $output->method('writeLineFormatted')->willReturnCallback(static function (string $message) use (&$lines): void { - $lines[] = $message; - }); - - return $output; - } - -} From 22c81981a91b8c1661ef0d3f72a2e96639a066ba Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 5 Apr 2026 19:38:52 +0200 Subject: [PATCH 09/13] fix --- src/Command/AnalyseApplication.php | 4 ++-- src/Diagnose/PHPStanDiagnoseExtension.php | 22 ++++++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php index c1635e53637..73ba73ca37f 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -247,7 +247,7 @@ private function runAnalyser( if (!$debug) { $preFileCallback = null; - $postFileCallback = function (int $step, array $processedFiles = []) use ($errorOutput): void { + $postFileCallback = function (int $step, array $processedFiles) use ($errorOutput): void { $errorOutput->getStyle()->progressAdvance($step); $this->processedFilesCollector->addProcessedFiles(array_values($processedFiles)); }; @@ -263,7 +263,7 @@ private function runAnalyser( $postFileCallback = null; if ($stdOutput->isDebug()) { $previousMemory = memory_get_peak_usage(true); - $postFileCallback = function (int $step, array $processedFiles = []) use ($stdOutput, &$previousMemory, &$startTime, &$linesOfCode): void { + $postFileCallback = function (int $step, array $processedFiles) use ($stdOutput, &$previousMemory, &$startTime, &$linesOfCode): void { if ($startTime === null) { throw new ShouldNotHappenException(); } diff --git a/src/Diagnose/PHPStanDiagnoseExtension.php b/src/Diagnose/PHPStanDiagnoseExtension.php index 17bda3c754f..d35951c578c 100644 --- a/src/Diagnose/PHPStanDiagnoseExtension.php +++ b/src/Diagnose/PHPStanDiagnoseExtension.php @@ -209,17 +209,19 @@ public function print(Output $output): void $output->writeLineFormatted(''); $topFiles = $this->processedFilesCollector->getTopMostAnalysedFiles(5); - if (count($topFiles) > 0) { - $output->writeLineFormatted('Most often analysed files:'); - foreach ($topFiles as $file => $count) { - $output->writeLineFormatted(sprintf( - ' %s: %d times', - $this->simpleRelativePathHelper->getRelativePath($file), - $count, - )); - } - $output->writeLineFormatted(''); + 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(''); } } From 91794e049570a3c612c2c078bd0cadf32e1be342 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 5 Apr 2026 19:42:14 +0200 Subject: [PATCH 10/13] Update AnalyserRunner.php --- src/Command/AnalyserRunner.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Command/AnalyserRunner.php b/src/Command/AnalyserRunner.php index 1da03e53ed3..3447d6457a8 100644 --- a/src/Command/AnalyserRunner.php +++ b/src/Command/AnalyserRunner.php @@ -37,7 +37,7 @@ public function __construct( * @param string[] $files * @param string[] $allAnalysedFiles * @param Closure(string $file): void|null $preFileCallback - * @param Closure(int, list=): void|null $postFileCallback + * @param Closure(int, list): void|null $postFileCallback */ public function runAnalyser( array $files, From 880c799b5621980d75f7002969c0dae2fb1ecf10 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 8 Apr 2026 14:05:12 +0000 Subject: [PATCH 11/13] Remove mutable ProcessedFilesCollector, carry processedFiles through AnalyserResult DTO Address review feedback: DI services should be stateless. Instead of accumulating processedFiles in a mutable singleton, the data now flows through AnalyserResult as an immutable DTO field from Analyser and ParallelAnalyser, through AnalyseApplication, to AnalyseCommand's runDiagnoseExtensions and PHPStanDiagnoseExtension. - Add processedFiles field to AnalyserResult - Accumulate processedFiles in Analyser and ParallelAnalyser - Return processedFiles alongside AnalysisResult from AnalyseApplication - Remove implements DiagnoseExtension from PHPStanDiagnoseExtension - Pass processedFiles explicitly to PHPStanDiagnoseExtension::print() - Move top-files computation into PHPStanDiagnoseExtension - Delete ProcessedFilesCollector and its test Co-Authored-By: Claude Opus 4.6 --- phpstan-baseline.neon | 6 -- src/Analyser/Analyser.php | 3 + src/Analyser/AnalyserResult.php | 10 +++ src/Analyser/AnalyserResultFinalizer.php | 3 + src/Analyser/ProcessedFilesCollector.php | 46 -------------- .../ResultCache/ResultCacheManager.php | 1 + src/Command/AnalyseApplication.php | 47 +++++++------- src/Command/AnalyseCommand.php | 15 +++-- src/Command/DiagnoseCommand.php | 2 +- src/Diagnose/PHPStanDiagnoseExtension.php | 37 +++++++++-- src/Parallel/ParallelAnalyser.php | 11 +++- .../Analyser/ProcessedFilesCollectorTest.php | 62 ------------------- .../AnalyseApplicationIntegrationTest.php | 2 +- 13 files changed, 94 insertions(+), 151 deletions(-) delete mode 100644 src/Analyser/ProcessedFilesCollector.php delete mode 100644 tests/PHPStan/Analyser/ProcessedFilesCollectorTest.php 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..407aa978468 100644 --- a/src/Analyser/Analyser.php +++ b/src/Analyser/Analyser.php @@ -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..117d21409c2 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/ProcessedFilesCollector.php b/src/Analyser/ProcessedFilesCollector.php deleted file mode 100644 index 7837ae5d874..00000000000 --- a/src/Analyser/ProcessedFilesCollector.php +++ /dev/null @@ -1,46 +0,0 @@ - */ - private array $processedFiles = []; - - /** - * @param list $files - */ - public function addProcessedFiles(array $files): void - { - foreach ($files as $file) { - $this->processedFiles[] = $file; - } - } - - /** - * @return array> - */ - public function getTopMostAnalysedFiles(int $limit): array - { - $counts = array_count_values($this->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/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 73ba73ca37f..7590958218d 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -7,7 +7,6 @@ use PHPStan\Analyser\Error; use PHPStan\Analyser\FileAnalyserResult; use PHPStan\Analyser\Ignore\IgnoredErrorHelper; -use PHPStan\Analyser\ProcessedFilesCollector; use PHPStan\Analyser\ResultCache\ResultCacheManagerFactory; use PHPStan\Collectors\CollectedData; use PHPStan\DependencyInjection\AutowiredService; @@ -18,7 +17,6 @@ use Symfony\Component\Console\Input\InputInterface; use function array_merge; use function array_unique; -use function array_values; use function count; use function fclose; use function feof; @@ -45,7 +43,6 @@ public function __construct( private ResultCacheManagerFactory $resultCacheManagerFactory, private IgnoredErrorHelper $ignoredErrorHelper, private StubFilesProvider $stubFilesProvider, - private ProcessedFilesCollector $processedFilesCollector, ) { } @@ -53,6 +50,7 @@ public function __construct( /** * @param string[] $files * @param mixed[]|null $projectConfigArray + * @return array{AnalysisResult, list} */ public function analyse( array $files, @@ -66,7 +64,7 @@ public function analyse( ?string $tmpFile, ?string $insteadOfFile, InputInterface $input, - ): AnalysisResult + ): array { $isResultCacheUsed = false; $fileReplacements = []; @@ -83,6 +81,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.'); } @@ -124,9 +123,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), @@ -175,19 +177,22 @@ public function analyse( $savedResultCache = $resultCacheResult->isSaved(); } - return new AnalysisResult( - $fileSpecificErrors, - $notFileSpecificErrors, - $internalErrors, - [], - $this->mapCollectedData($collectedData), - $defaultLevelUsed, - $projectConfigFile, - $savedResultCache, - $memoryUsageBytes, - $isResultCacheUsed, - $changedProjectExtensionFilesOutsideOfAnalysedPaths, - ); + return [ + new AnalysisResult( + $fileSpecificErrors, + $notFileSpecificErrors, + $internalErrors, + [], + $this->mapCollectedData($collectedData), + $defaultLevelUsed, + $projectConfigFile, + $savedResultCache, + $memoryUsageBytes, + $isResultCacheUsed, + $changedProjectExtensionFilesOutsideOfAnalysedPaths, + ), + $processedFiles, + ]; } /** @@ -247,9 +252,8 @@ private function runAnalyser( if (!$debug) { $preFileCallback = null; - $postFileCallback = function (int $step, array $processedFiles) use ($errorOutput): void { + $postFileCallback = static function (int $step, array $processedFiles) use ($errorOutput): void { $errorOutput->getStyle()->progressAdvance($step); - $this->processedFilesCollector->addProcessedFiles(array_values($processedFiles)); }; $errorOutput->getStyle()->progressStart($allAnalysedFilesCount); @@ -263,7 +267,7 @@ private function runAnalyser( $postFileCallback = null; if ($stdOutput->isDebug()) { $previousMemory = memory_get_peak_usage(true); - $postFileCallback = function (int $step, array $processedFiles) use ($stdOutput, &$previousMemory, &$startTime, &$linesOfCode): void { + $postFileCallback = static function (int $step, array $processedFiles) use ($stdOutput, &$previousMemory, &$startTime, &$linesOfCode): void { if ($startTime === null) { throw new ShouldNotHappenException(); } @@ -284,8 +288,6 @@ private function runAnalyser( fclose($handle); } - $this->processedFilesCollector->addProcessedFiles(array_values($processedFiles)); - $stdOutput->writeLineFormatted(sprintf('--- consumed %s, total %s, took %.2f s, %.3f LoC/s', BytesHelper::bytes($currentTotalMemory - $previousMemory), BytesHelper::bytes($currentTotalMemory), $elapsedTime, $linesOfCode / $elapsedTime)); $previousMemory = $currentTotalMemory; }; @@ -352,6 +354,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..a90baf17365 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -341,7 +341,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } try { - $analysisResult = $application->analyse( + [$analysisResult, $processedFiles] = $application->analyse( $files, $onlyFiles, $inceptionResult->getStdOutput(), @@ -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(), $processedFiles); if (count($internalErrorsTuples) > 0) { foreach ($internalErrorsTuples as [$internalError]) { $inceptionResult->getStdOutput()->writeLineFormatted($internalError->getMessage()); @@ -497,7 +497,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $exitCode = $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()); - $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput(), $processedFiles); $errorOutput->writeLineFormatted('⚠️ Result is incomplete because of severe errors. ⚠️'); $errorOutput->writeLineFormatted(' Fix these errors first and then re-run PHPStan'); @@ -649,7 +649,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } - $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput()); + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput(), $processedFiles); return $inceptionResult->handleReturn( $exitCode, @@ -847,7 +847,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 +860,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/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/Diagnose/PHPStanDiagnoseExtension.php b/src/Diagnose/PHPStanDiagnoseExtension.php index d35951c578c..37f9f021b25 100644 --- a/src/Diagnose/PHPStanDiagnoseExtension.php +++ b/src/Diagnose/PHPStanDiagnoseExtension.php @@ -3,7 +3,6 @@ namespace PHPStan\Diagnose; use Phar; -use PHPStan\Analyser\ProcessedFilesCollector; use PHPStan\Command\Output; use PHPStan\ExtensionInstaller\GeneratedConfig; use PHPStan\File\FileHelper; @@ -12,8 +11,10 @@ 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; @@ -29,7 +30,7 @@ use function substr; use const PHP_VERSION_ID; -final class PHPStanDiagnoseExtension implements DiagnoseExtension +final class PHPStanDiagnoseExtension { /** @@ -44,13 +45,15 @@ public function __construct( private array $composerAutoloaderProjectPaths, private array $allConfigFiles, private ComposerPhpVersionFactory $composerPhpVersionFactory, - private ProcessedFilesCollector $processedFilesCollector, 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( @@ -208,7 +211,7 @@ public function print(Output $output): void } $output->writeLineFormatted(''); - $topFiles = $this->processedFilesCollector->getTopMostAnalysedFiles(5); + $topFiles = $this->getTopMostAnalysedFiles($processedFiles, 5); if (count($topFiles) <= 0) { return; } @@ -224,4 +227,28 @@ public function print(Output $output): void $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 10c1e6491d5..d315a63e478 100644 --- a/src/Parallel/ParallelAnalyser.php +++ b/src/Parallel/ParallelAnalyser.php @@ -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,6 +284,10 @@ public function analyse( }, $fileExportedNodes); } + foreach ($json['processedFiles'] as $processedFile) { + $allProcessedFiles[] = $processedFile; + } + if ($postFileCallback !== null) { $postFileCallback(count($json['files']), $json['processedFiles']); } diff --git a/tests/PHPStan/Analyser/ProcessedFilesCollectorTest.php b/tests/PHPStan/Analyser/ProcessedFilesCollectorTest.php deleted file mode 100644 index da7a811b56a..00000000000 --- a/tests/PHPStan/Analyser/ProcessedFilesCollectorTest.php +++ /dev/null @@ -1,62 +0,0 @@ -assertSame([], $collector->getTopMostAnalysedFiles(5)); - } - - public function testSingleFileNotReported(): void - { - $collector = new ProcessedFilesCollector(); - $collector->addProcessedFiles(['/path/to/file.php']); - $this->assertSame([], $collector->getTopMostAnalysedFiles(5)); - } - - public function testTopMostAnalysedFiles(): void - { - $collector = new ProcessedFilesCollector(); - - // Simulate: file A uses trait T1 and T2, file B uses trait T1 - $collector->addProcessedFiles(['/path/to/A.php', '/path/to/T1.php', '/path/to/T2.php']); - $collector->addProcessedFiles(['/path/to/B.php', '/path/to/T1.php']); - - $top = $collector->getTopMostAnalysedFiles(5); - $this->assertSame(['/path/to/T1.php' => 2], $top); - } - - public function testLimit(): void - { - $collector = new ProcessedFilesCollector(); - - // Create 7 trait files with varying usage counts - for ($i = 0; $i < 7; $i++) { - $files = ['/path/to/main' . $i . '.php']; - for ($j = 0; $j <= $i; $j++) { - $files[] = '/path/to/trait' . $j . '.php'; - } - $collector->addProcessedFiles($files); - } - - $top = $collector->getTopMostAnalysedFiles(3); - $this->assertCount(3, $top); - - // trait0.php used 7 times, trait1.php 6 times, trait2.php 5 times - $files = array_keys($top); - $this->assertSame('/path/to/trait0.php', $files[0]); - $this->assertSame(7, $top['/path/to/trait0.php']); - $this->assertSame('/path/to/trait1.php', $files[1]); - $this->assertSame(6, $top['/path/to/trait1.php']); - $this->assertSame('/path/to/trait2.php', $files[2]); - $this->assertSame(5, $top['/path/to/trait2.php']); - } - -} diff --git a/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php b/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php index f7d7b086f39..4bd4931a84c 100644 --- a/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php +++ b/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php @@ -78,7 +78,7 @@ private function runPath(string $path, int $expectedStatusCode): string null, CommandHelper::DEFAULT_LEVEL, ); - $analysisResult = $analyserApplication->analyse( + [$analysisResult] = $analyserApplication->analyse( [$path], true, $symfonyOutput, From ab11d4ae0be18423a46ece2baeaeecefa2edc392 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 8 Apr 2026 15:55:09 +0000 Subject: [PATCH 12/13] Store processedFiles in AnalysisResult, make analyse() return AnalysisResult again Move processedFiles from a separate array return value into the AnalysisResult DTO so that AnalyseApplication::analyse() returns AnalysisResult directly instead of array{AnalysisResult, list}. Co-Authored-By: Claude Opus 4.6 --- src/Command/AnalyseApplication.php | 31 +++++++++---------- src/Command/AnalyseCommand.php | 9 +++--- src/Command/AnalysisResult.php | 11 +++++++ .../AnalyseApplicationIntegrationTest.php | 2 +- 4 files changed, 31 insertions(+), 22 deletions(-) diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php index 7590958218d..d61529b0998 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -50,7 +50,6 @@ public function __construct( /** * @param string[] $files * @param mixed[]|null $projectConfigArray - * @return array{AnalysisResult, list} */ public function analyse( array $files, @@ -64,7 +63,7 @@ public function analyse( ?string $tmpFile, ?string $insteadOfFile, InputInterface $input, - ): array + ): AnalysisResult { $isResultCacheUsed = false; $fileReplacements = []; @@ -177,22 +176,20 @@ public function analyse( $savedResultCache = $resultCacheResult->isSaved(); } - return [ - new AnalysisResult( - $fileSpecificErrors, - $notFileSpecificErrors, - $internalErrors, - [], - $this->mapCollectedData($collectedData), - $defaultLevelUsed, - $projectConfigFile, - $savedResultCache, - $memoryUsageBytes, - $isResultCacheUsed, - $changedProjectExtensionFilesOutsideOfAnalysedPaths, - ), + return new AnalysisResult( + $fileSpecificErrors, + $notFileSpecificErrors, + $internalErrors, + [], + $this->mapCollectedData($collectedData), + $defaultLevelUsed, + $projectConfigFile, + $savedResultCache, + $memoryUsageBytes, + $isResultCacheUsed, + $changedProjectExtensionFilesOutsideOfAnalysedPaths, $processedFiles, - ]; + ); } /** diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index a90baf17365..f7865ad7508 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -341,7 +341,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } try { - [$analysisResult, $processedFiles] = $application->analyse( + $analysisResult = $application->analyse( $files, $onlyFiles, $inceptionResult->getStdOutput(), @@ -459,7 +459,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($generateBaselineFile !== null) { - $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput(), $processedFiles); + $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(), $processedFiles); + $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(), $processedFiles); + $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput(), $analysisResult->getProcessedFiles()); return $inceptionResult->handleReturn( $exitCode, 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/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php b/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php index 4bd4931a84c..f7d7b086f39 100644 --- a/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php +++ b/tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php @@ -78,7 +78,7 @@ private function runPath(string $path, int $expectedStatusCode): string null, CommandHelper::DEFAULT_LEVEL, ); - [$analysisResult] = $analyserApplication->analyse( + $analysisResult = $analyserApplication->analyse( [$path], true, $symfonyOutput, From f2db6d17eeba97e350101be6d839fa77ef47c9aa Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 8 Apr 2026 16:39:26 +0000 Subject: [PATCH 13/13] Make AnalyserResult::processedFiles non-optional, remove unused callback parameter Address review feedback: - Remove default value from AnalyserResult::$processedFiles, add explicit processedFiles: [] at all construction sites - Remove unused $processedFiles parameter from non-debug postFileCallback - Revert postFileCallback type annotations to optional second parameter Co-Authored-By: Claude Opus 4.6 --- src/Analyser/Analyser.php | 2 +- src/Analyser/AnalyserResult.php | 2 +- src/Command/AnalyseApplication.php | 5 +++-- src/Command/AnalyserRunner.php | 3 ++- src/Command/FixerWorkerCommand.php | 1 + src/Parallel/ParallelAnalyser.php | 2 +- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Analyser/Analyser.php b/src/Analyser/Analyser.php index 407aa978468..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( diff --git a/src/Analyser/AnalyserResult.php b/src/Analyser/AnalyserResult.php index 117d21409c2..31b88e27300 100644 --- a/src/Analyser/AnalyserResult.php +++ b/src/Analyser/AnalyserResult.php @@ -44,7 +44,7 @@ public function __construct( private array $exportedNodes, private bool $reachedInternalErrorsCountLimit, private int $peakMemoryUsageBytes, - private array $processedFiles = [], + private array $processedFiles, ) { } diff --git a/src/Command/AnalyseApplication.php b/src/Command/AnalyseApplication.php index d61529b0998..e78212f492c 100644 --- a/src/Command/AnalyseApplication.php +++ b/src/Command/AnalyseApplication.php @@ -244,12 +244,13 @@ private function runAnalyser( exportedNodes: [], reachedInternalErrorsCountLimit: false, peakMemoryUsageBytes: memory_get_peak_usage(true), + processedFiles: [], ); } if (!$debug) { $preFileCallback = null; - $postFileCallback = static function (int $step, array $processedFiles) use ($errorOutput): void { + $postFileCallback = static function (int $step) use ($errorOutput): void { $errorOutput->getStyle()->progressAdvance($step); }; @@ -264,7 +265,7 @@ private function runAnalyser( $postFileCallback = null; if ($stdOutput->isDebug()) { $previousMemory = memory_get_peak_usage(true); - $postFileCallback = static function (int $step, array $processedFiles) use ($stdOutput, &$previousMemory, &$startTime, &$linesOfCode): void { + $postFileCallback = static function (int $step, array $processedFiles = []) use ($stdOutput, &$previousMemory, &$startTime, &$linesOfCode): void { if ($startTime === null) { throw new ShouldNotHappenException(); } diff --git a/src/Command/AnalyserRunner.php b/src/Command/AnalyserRunner.php index 3447d6457a8..e0642493890 100644 --- a/src/Command/AnalyserRunner.php +++ b/src/Command/AnalyserRunner.php @@ -37,7 +37,7 @@ public function __construct( * @param string[] $files * @param string[] $allAnalysedFiles * @param Closure(string $file): void|null $preFileCallback - * @param Closure(int, list): void|null $postFileCallback + * @param Closure(int, list=): void|null $postFileCallback */ public function runAnalyser( array $files, @@ -68,6 +68,7 @@ public function runAnalyser( exportedNodes: [], reachedInternalErrorsCountLimit: false, peakMemoryUsageBytes: memory_get_peak_usage(true), + processedFiles: [], ); } 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/Parallel/ParallelAnalyser.php b/src/Parallel/ParallelAnalyser.php index d315a63e478..248b52a4a34 100644 --- a/src/Parallel/ParallelAnalyser.php +++ b/src/Parallel/ParallelAnalyser.php @@ -58,7 +58,7 @@ public function __construct( } /** - * @param Closure(int, list): void|null $postFileCallback + * @param Closure(int, list=): void|null $postFileCallback * @param (callable(list, list, string[]): void)|null $onFileAnalysisHandler * @return PromiseInterface */