Skip to content

Commit 38cdabb

Browse files
authored
TableErrorFormatter: Limit number of errors
1 parent c1c4816 commit 38cdabb

File tree

3 files changed

+254
-8
lines changed

3 files changed

+254
-8
lines changed

src/Command/ErrorFormatter/TableErrorFormatter.php

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,19 @@
55
use PHPStan\Analyser\Error;
66
use PHPStan\Command\AnalyseCommand;
77
use PHPStan\Command\AnalysisResult;
8+
use PHPStan\Command\CommandHelper;
89
use PHPStan\Command\Output;
910
use PHPStan\DependencyInjection\AutowiredParameter;
1011
use PHPStan\DependencyInjection\AutowiredService;
1112
use PHPStan\File\RelativePathHelper;
1213
use PHPStan\File\SimpleRelativePathHelper;
1314
use Symfony\Component\Console\Formatter\OutputFormatter;
1415
use function array_map;
16+
use function array_slice;
1517
use function count;
1618
use function explode;
1719
use function getenv;
20+
use function implode;
1821
use function in_array;
1922
use function is_string;
2023
use function ltrim;
@@ -27,6 +30,9 @@
2730
final class TableErrorFormatter implements ErrorFormatter
2831
{
2932

33+
public const ERRORS_LIMIT = 1000;
34+
private const FORCE_SHOW_ALL_ERRORS = 'PHPSTAN_TABLE_ERROR_FORMATTER_FORCE_SHOW_ALL_ERRORS';
35+
3036
public function __construct(
3137
private RelativePathHelper $relativePathHelper,
3238
#[AutowiredParameter(ref: '@simpleRelativePathHelper')]
@@ -38,8 +44,21 @@ public function __construct(
3844
private ?string $editorUrl,
3945
#[AutowiredParameter]
4046
private ?string $editorUrlTitle,
47+
#[AutowiredParameter]
48+
private string $usedLevel,
49+
private ?int $errorsBudget = null,
4150
)
4251
{
52+
if ($this->errorsBudget !== null) {
53+
return;
54+
}
55+
56+
$forceShowAll = getenv(self::FORCE_SHOW_ALL_ERRORS);
57+
if (!in_array($forceShowAll, [false, '0'], true)) {
58+
return;
59+
}
60+
61+
$this->errorsBudget = self::ERRORS_LIMIT;
4362
}
4463

4564
/** @api */
@@ -84,6 +103,8 @@ public function formatErrors(
84103
$fileErrors[$fileSpecificError->getFile()][] = $fileSpecificError;
85104
}
86105

106+
$errorsBudget = $this->errorsBudget;
107+
$printedErrors = 0;
87108
foreach ($fileErrors as $file => $errors) {
88109
$rows = [];
89110
foreach ($errors as $error) {
@@ -149,6 +170,14 @@ public function formatErrors(
149170
];
150171
}
151172

173+
$printedErrors += count($rows);
174+
if ($errorsBudget !== null && $printedErrors > $errorsBudget) {
175+
$rows = array_slice($rows, 0, $errorsBudget - ($printedErrors - count($rows)));
176+
177+
$style->table(['Line', $this->relativePathHelper->getRelativePath($file)], $rows);
178+
break;
179+
}
180+
152181
$style->table(['Line', $this->relativePathHelper->getRelativePath($file)], $rows);
153182
}
154183

@@ -161,15 +190,29 @@ public function formatErrors(
161190
$style->table(['', 'Warning'], array_map(static fn (string $warning): array => ['', OutputFormatter::escape($warning)], $analysisResult->getWarnings()));
162191
}
163192

164-
$finalMessage = sprintf($analysisResult->getTotalErrorsCount() === 1 ? 'Found %d error' : 'Found %d errors', $analysisResult->getTotalErrorsCount());
165-
if ($warningsCount > 0) {
166-
$finalMessage .= sprintf($warningsCount === 1 ? ' and %d warning' : ' and %d warnings', $warningsCount);
167-
}
193+
if ($errorsBudget !== null && $printedErrors > $errorsBudget) {
194+
$style->error(sprintf('Found %s+ errors', $errorsBudget));
168195

169-
if ($analysisResult->getTotalErrorsCount() > 0) {
170-
$style->error($finalMessage);
196+
$note = [];
197+
$note[] = sprintf('Result is limited to the first %d errors', $errorsBudget);
198+
if ($this->usedLevel !== CommandHelper::DEFAULT_LEVEL) {
199+
$note[] = '- Consider lowering the PHPStan level';
200+
}
201+
$note[] = sprintf('- Pass %s=1 environment variable to show all errors', self::FORCE_SHOW_ALL_ERRORS);
202+
$note[] = '- Consider using PHPStan Pro for more comfortable error browsing';
203+
$note[] = ' Learn more: https://phpstan.com';
204+
$style->note(implode("\n", $note));
171205
} else {
172-
$style->warning($finalMessage);
206+
$finalMessage = sprintf($analysisResult->getTotalErrorsCount() === 1 ? 'Found %d error' : 'Found %d errors', $analysisResult->getTotalErrorsCount());
207+
if ($warningsCount > 0) {
208+
$finalMessage .= sprintf($warningsCount === 1 ? ' and %d warning' : ' and %d warnings', $warningsCount);
209+
}
210+
211+
if ($analysisResult->getTotalErrorsCount() > 0) {
212+
$style->error($finalMessage);
213+
} else {
214+
$style->warning($finalMessage);
215+
}
173216
}
174217

175218
return $analysisResult->getTotalErrorsCount() > 0 ? 1 : 0;

tests/PHPStan/Command/AnalyseApplicationIntegrationTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ private function runPath(string $path, int $expectedStatusCode): string
7676
false,
7777
null,
7878
null,
79+
CommandHelper::DEFAULT_LEVEL,
7980
);
8081
$analysisResult = $analyserApplication->analyse(
8182
[$path],

tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php

Lines changed: 203 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Override;
66
use PHPStan\Analyser\Error;
77
use PHPStan\Command\AnalysisResult;
8+
use PHPStan\Command\CommandHelper;
89
use PHPStan\File\FuzzyRelativePathHelper;
910
use PHPStan\File\NullRelativePathHelper;
1011
use PHPStan\File\SimpleRelativePathHelper;
@@ -34,6 +35,7 @@ protected function tearDown(): void
3435
putenv('COLUMNS');
3536
putenv('TERM_PROGRAM');
3637
putenv('TERMINAL_EMULATOR' . ($this->terminalEmulator !== false ? '=' . $this->terminalEmulator : ''));
38+
putenv('PHPSTAN_TABLE_ERROR_FORMATTER_FORCE_SHOW_ALL_ERRORS');
3739
}
3840

3941
public static function dataFormatterOutputProvider(): iterable
@@ -293,6 +295,198 @@ public function testFormatErrors(
293295
$this->assertSame($expected, $this->getOutputContent(false, $verbose), sprintf('%s: output do not match', $message));
294296
}
295297

298+
public static function dataErrorLimit(): iterable
299+
{
300+
yield [
301+
'errorsBudget' => null,
302+
'usedLevel' => CommandHelper::DEFAULT_LEVEL,
303+
'showAllErrors' => false,
304+
'expected' => ' ------ -------------------------------
305+
Line Foo.php (in context of trait)
306+
------ -------------------------------
307+
12 Test
308+
13 Test
309+
14 Test
310+
15 Test
311+
------ -------------------------------
312+
313+
314+
[ERROR] Found 4 errors
315+
316+
',
317+
];
318+
yield [
319+
'errorsBudget' => 1,
320+
'usedLevel' => CommandHelper::DEFAULT_LEVEL,
321+
'showAllErrors' => false,
322+
'expected' => ' ------ -------------------------------
323+
Line Foo.php (in context of trait)
324+
------ -------------------------------
325+
12 Test
326+
------ -------------------------------
327+
328+
329+
[ERROR] Found 1+ errors
330+
331+
! [NOTE] Result is limited to the first 1 errors
332+
! - Pass PHPSTAN_TABLE_ERROR_FORMATTER_FORCE_SHOW_ALL_ERRORS=1
333+
! environment variable to show all errors
334+
! - Consider using PHPStan Pro for more comfortable error browsing
335+
! Learn more: https://phpstan.com
336+
337+
',
338+
];
339+
340+
yield [
341+
'errorsBudget' => 3,
342+
'usedLevel' => '8',
343+
'showAllErrors' => false,
344+
'expected' => ' ------ -------------------------------
345+
Line Foo.php (in context of trait)
346+
------ -------------------------------
347+
12 Test
348+
13 Test
349+
14 Test
350+
------ -------------------------------
351+
352+
353+
[ERROR] Found 3+ errors
354+
355+
! [NOTE] Result is limited to the first 3 errors
356+
! - Consider lowering the PHPStan level
357+
! - Pass PHPSTAN_TABLE_ERROR_FORMATTER_FORCE_SHOW_ALL_ERRORS=1
358+
! environment variable to show all errors
359+
! - Consider using PHPStan Pro for more comfortable error browsing
360+
! Learn more: https://phpstan.com
361+
362+
',
363+
];
364+
365+
yield [
366+
'errorsBudget' => 3,
367+
'usedLevel' => CommandHelper::DEFAULT_LEVEL,
368+
'showAllErrors' => false,
369+
'expected' => ' ------ -------------------------------
370+
Line Foo.php (in context of trait)
371+
------ -------------------------------
372+
12 Test
373+
13 Test
374+
14 Test
375+
------ -------------------------------
376+
377+
378+
[ERROR] Found 3+ errors
379+
380+
! [NOTE] Result is limited to the first 3 errors
381+
! - Pass PHPSTAN_TABLE_ERROR_FORMATTER_FORCE_SHOW_ALL_ERRORS=1
382+
! environment variable to show all errors
383+
! - Consider using PHPStan Pro for more comfortable error browsing
384+
! Learn more: https://phpstan.com
385+
',
386+
];
387+
388+
yield [
389+
'errorsBudget' => 4,
390+
'usedLevel' => CommandHelper::DEFAULT_LEVEL,
391+
'showAllErrors' => false,
392+
'expected' => ' ------ -------------------------------
393+
Line Foo.php (in context of trait)
394+
------ -------------------------------
395+
12 Test
396+
13 Test
397+
14 Test
398+
15 Test
399+
------ -------------------------------
400+
401+
402+
[ERROR] Found 4 errors
403+
404+
',
405+
];
406+
yield [
407+
'errorsBudget' => 5,
408+
'usedLevel' => CommandHelper::DEFAULT_LEVEL,
409+
'showAllErrors' => false,
410+
'expected' => ' ------ -------------------------------
411+
Line Foo.php (in context of trait)
412+
------ -------------------------------
413+
12 Test
414+
13 Test
415+
14 Test
416+
15 Test
417+
------ -------------------------------
418+
419+
420+
[ERROR] Found 4 errors
421+
422+
',
423+
];
424+
425+
yield [
426+
'errorsBudget' => null,
427+
'usedLevel' => '8',
428+
'showAllErrors' => false,
429+
'expected' => '
430+
431+
[ERROR] Found 1000+ errors
432+
433+
',
434+
'generateErrorsCount' => TableErrorFormatter::ERRORS_LIMIT + 5,
435+
];
436+
437+
yield [
438+
'errorsBudget' => null,
439+
'usedLevel' => '8',
440+
'showAllErrors' => true,
441+
'expected' => '
442+
443+
[ERROR] Found 1005 errors
444+
445+
',
446+
'generateErrorsCount' => TableErrorFormatter::ERRORS_LIMIT + 5,
447+
];
448+
}
449+
450+
#[DataProvider('dataErrorLimit')]
451+
public function testErrorLimit(
452+
?int $errorsBudget,
453+
string $usedLevel,
454+
bool $showAllErrors,
455+
string $expected,
456+
int $generateErrorsCount = 4,
457+
): void
458+
{
459+
// windows has minor formatting differences (line breaks)
460+
$this->skipIfNotOnUnix();
461+
462+
putenv('COLUMNS=80');
463+
if ($showAllErrors) {
464+
if ($errorsBudget !== null) {
465+
$this->fail('showAllErrors cannot be true when errorsBudget is set');
466+
}
467+
putenv('PHPSTAN_TABLE_ERROR_FORMATTER_FORCE_SHOW_ALL_ERRORS=1');
468+
$errorsBudget = null;
469+
} else {
470+
putenv('PHPSTAN_TABLE_ERROR_FORMATTER_FORCE_SHOW_ALL_ERRORS');
471+
}
472+
473+
$formatter = $this->createErrorFormatter(
474+
null,
475+
null,
476+
$usedLevel,
477+
$errorsBudget,
478+
);
479+
$errors = [];
480+
$line = 12;
481+
for ($i = 0; $i < $generateErrorsCount; $i++) {
482+
$errors[] = new Error('Test', 'Foo.php (in context of trait)', $line, filePath: 'Foo.php', traitFilePath: 'Bar.php');
483+
$line++;
484+
}
485+
$formatter->formatErrors(new AnalysisResult($errors, [], [], [], [], false, null, true, 0, false, []), $this->getOutput());
486+
487+
$this->assertStringContainsString($expected, $this->getOutputContent());
488+
}
489+
296490
public function testEditorUrlWithTrait(): void
297491
{
298492
$formatter = $this->createErrorFormatter('editor://%file%/%line%');
@@ -441,14 +635,20 @@ public function testJetBrainsTerminalRelativePath(): void
441635
false,
442636
null,
443637
null,
638+
CommandHelper::DEFAULT_LEVEL,
444639
);
445640
$error = new Error('Test', 'Foo.php', 12, filePath: self::DIRECTORY_PATH . '/rel/Foo.php');
446641
$formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true, 0, false, []), $this->getOutput(true));
447642

448643
$this->assertStringContainsString('at rel/Foo.php:12', $this->getOutputContent(true));
449644
}
450645

451-
private function createErrorFormatter(?string $editorUrl, ?string $editorUrlTitle = null): TableErrorFormatter
646+
private function createErrorFormatter(
647+
?string $editorUrl,
648+
?string $editorUrlTitle = null,
649+
string $usedLevel = CommandHelper::DEFAULT_LEVEL,
650+
?int $errorsBudget = null,
651+
): TableErrorFormatter
452652
{
453653
$relativePathHelper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), self::DIRECTORY_PATH, [], '/');
454654

@@ -462,6 +662,8 @@ private function createErrorFormatter(?string $editorUrl, ?string $editorUrlTitl
462662
false,
463663
$editorUrl,
464664
$editorUrlTitle,
665+
$usedLevel,
666+
$errorsBudget,
465667
);
466668
}
467669

0 commit comments

Comments
 (0)