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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 50 additions & 7 deletions src/Command/ErrorFormatter/TableErrorFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@
use PHPStan\Analyser\Error;
use PHPStan\Command\AnalyseCommand;
use PHPStan\Command\AnalysisResult;
use PHPStan\Command\CommandHelper;
use PHPStan\Command\Output;
use PHPStan\DependencyInjection\AutowiredParameter;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\File\RelativePathHelper;
use PHPStan\File\SimpleRelativePathHelper;
use Symfony\Component\Console\Formatter\OutputFormatter;
use function array_map;
use function array_slice;
use function count;
use function explode;
use function getenv;
use function implode;
use function in_array;
use function is_string;
use function ltrim;
Expand All @@ -27,6 +30,9 @@
final class TableErrorFormatter implements ErrorFormatter
{

public const ERRORS_LIMIT = 1000;
private const FORCE_SHOW_ALL_ERRORS = 'PHPSTAN_TABLE_ERROR_FORMATTER_FORCE_SHOW_ALL_ERRORS';

public function __construct(
private RelativePathHelper $relativePathHelper,
#[AutowiredParameter(ref: '@simpleRelativePathHelper')]
Expand All @@ -38,8 +44,21 @@ public function __construct(
private ?string $editorUrl,
#[AutowiredParameter]
private ?string $editorUrlTitle,
#[AutowiredParameter]
private string $usedLevel,
private ?int $errorsBudget = null,
)
{
if ($this->errorsBudget !== null) {
return;
}

$forceShowAll = getenv(self::FORCE_SHOW_ALL_ERRORS);
if (!in_array($forceShowAll, [false, '0'], true)) {
return;
}

$this->errorsBudget = self::ERRORS_LIMIT;
}

/** @api */
Expand Down Expand Up @@ -84,6 +103,8 @@ public function formatErrors(
$fileErrors[$fileSpecificError->getFile()][] = $fileSpecificError;
}

$errorsBudget = $this->errorsBudget;
$printedErrors = 0;
foreach ($fileErrors as $file => $errors) {
$rows = [];
foreach ($errors as $error) {
Expand Down Expand Up @@ -149,6 +170,14 @@ public function formatErrors(
];
}

$printedErrors += count($rows);
if ($errorsBudget !== null && $printedErrors > $errorsBudget) {
$rows = array_slice($rows, 0, $errorsBudget - ($printedErrors - count($rows)));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last thing - I'm not sure whether this line works correctly, a lot is going on there.

For example it's easy to make a mistake in $errorsBudget - ($printedErrors - count($rows)) based on whether $printedErrors already includes current count($rows) or if the += operation happened after this computation.

I'm thinking this should really have some unit tests.

My suggestion - constructor of TableErrorFormatter should have a new optional nullable parameter. If it's null, then the autodetection based on the env variable will happen. If it's not null then that will become the error limit.

The unit test will then construct TableErrorFormatter with a specific value, can be 5. And we should test the output for < 5, exactly 5 and > 5.


$style->table(['Line', $this->relativePathHelper->getRelativePath($file)], $rows);
break;
}

$style->table(['Line', $this->relativePathHelper->getRelativePath($file)], $rows);
}

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

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

if ($analysisResult->getTotalErrorsCount() > 0) {
$style->error($finalMessage);
$note = [];
$note[] = sprintf('Result is limited to the first %d errors', $errorsBudget);
if ($this->usedLevel !== CommandHelper::DEFAULT_LEVEL) {
$note[] = '- Consider lowering the PHPStan level';
}
$note[] = sprintf('- Pass %s=1 environment variable to show all errors', self::FORCE_SHOW_ALL_ERRORS);
$note[] = '- Consider using PHPStan Pro for more comfortable error browsing';
$note[] = ' Learn more: https://phpstan.com';
$style->note(implode("\n", $note));
} else {
$style->warning($finalMessage);
$finalMessage = sprintf($analysisResult->getTotalErrorsCount() === 1 ? 'Found %d error' : 'Found %d errors', $analysisResult->getTotalErrorsCount());
if ($warningsCount > 0) {
$finalMessage .= sprintf($warningsCount === 1 ? ' and %d warning' : ' and %d warnings', $warningsCount);
}

if ($analysisResult->getTotalErrorsCount() > 0) {
$style->error($finalMessage);
} else {
$style->warning($finalMessage);
}
}

return $analysisResult->getTotalErrorsCount() > 0 ? 1 : 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ private function runPath(string $path, int $expectedStatusCode): string
false,
null,
null,
CommandHelper::DEFAULT_LEVEL,
);
$analysisResult = $analyserApplication->analyse(
[$path],
Expand Down
204 changes: 203 additions & 1 deletion tests/PHPStan/Command/ErrorFormatter/TableErrorFormatterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Override;
use PHPStan\Analyser\Error;
use PHPStan\Command\AnalysisResult;
use PHPStan\Command\CommandHelper;
use PHPStan\File\FuzzyRelativePathHelper;
use PHPStan\File\NullRelativePathHelper;
use PHPStan\File\SimpleRelativePathHelper;
Expand Down Expand Up @@ -34,6 +35,7 @@ protected function tearDown(): void
putenv('COLUMNS');
putenv('TERM_PROGRAM');
putenv('TERMINAL_EMULATOR' . ($this->terminalEmulator !== false ? '=' . $this->terminalEmulator : ''));
putenv('PHPSTAN_TABLE_ERROR_FORMATTER_FORCE_SHOW_ALL_ERRORS');
}

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

public static function dataErrorLimit(): iterable
{
yield [
'errorsBudget' => null,
'usedLevel' => CommandHelper::DEFAULT_LEVEL,
'showAllErrors' => false,
'expected' => ' ------ -------------------------------
Line Foo.php (in context of trait)
------ -------------------------------
12 Test
13 Test
14 Test
15 Test
------ -------------------------------


[ERROR] Found 4 errors

',
];
yield [
'errorsBudget' => 1,
'usedLevel' => CommandHelper::DEFAULT_LEVEL,
'showAllErrors' => false,
'expected' => ' ------ -------------------------------
Line Foo.php (in context of trait)
------ -------------------------------
12 Test
------ -------------------------------


[ERROR] Found 1+ errors

! [NOTE] Result is limited to the first 1 errors
! - Pass PHPSTAN_TABLE_ERROR_FORMATTER_FORCE_SHOW_ALL_ERRORS=1
! environment variable to show all errors
! - Consider using PHPStan Pro for more comfortable error browsing
! Learn more: https://phpstan.com

',
];

yield [
'errorsBudget' => 3,
'usedLevel' => '8',
'showAllErrors' => false,
'expected' => ' ------ -------------------------------
Line Foo.php (in context of trait)
------ -------------------------------
12 Test
13 Test
14 Test
------ -------------------------------


[ERROR] Found 3+ errors

! [NOTE] Result is limited to the first 3 errors
! - Consider lowering the PHPStan level
! - Pass PHPSTAN_TABLE_ERROR_FORMATTER_FORCE_SHOW_ALL_ERRORS=1
! environment variable to show all errors
! - Consider using PHPStan Pro for more comfortable error browsing
! Learn more: https://phpstan.com

',
];

yield [
'errorsBudget' => 3,
'usedLevel' => CommandHelper::DEFAULT_LEVEL,
'showAllErrors' => false,
'expected' => ' ------ -------------------------------
Line Foo.php (in context of trait)
------ -------------------------------
12 Test
13 Test
14 Test
------ -------------------------------


[ERROR] Found 3+ errors

! [NOTE] Result is limited to the first 3 errors
! - Pass PHPSTAN_TABLE_ERROR_FORMATTER_FORCE_SHOW_ALL_ERRORS=1
! environment variable to show all errors
! - Consider using PHPStan Pro for more comfortable error browsing
! Learn more: https://phpstan.com
',
];

yield [
'errorsBudget' => 4,
'usedLevel' => CommandHelper::DEFAULT_LEVEL,
'showAllErrors' => false,
'expected' => ' ------ -------------------------------
Line Foo.php (in context of trait)
------ -------------------------------
12 Test
13 Test
14 Test
15 Test
------ -------------------------------


[ERROR] Found 4 errors

',
];
yield [
'errorsBudget' => 5,
'usedLevel' => CommandHelper::DEFAULT_LEVEL,
'showAllErrors' => false,
'expected' => ' ------ -------------------------------
Line Foo.php (in context of trait)
------ -------------------------------
12 Test
13 Test
14 Test
15 Test
------ -------------------------------


[ERROR] Found 4 errors

',
];

yield [
'errorsBudget' => null,
'usedLevel' => '8',
'showAllErrors' => false,
'expected' => '

[ERROR] Found 1000+ errors

',
'generateErrorsCount' => TableErrorFormatter::ERRORS_LIMIT + 5,
];

yield [
'errorsBudget' => null,
'usedLevel' => '8',
'showAllErrors' => true,
'expected' => '

[ERROR] Found 1005 errors

',
'generateErrorsCount' => TableErrorFormatter::ERRORS_LIMIT + 5,
];
}

#[DataProvider('dataErrorLimit')]
public function testErrorLimit(
?int $errorsBudget,
string $usedLevel,
bool $showAllErrors,
string $expected,
int $generateErrorsCount = 4,
): void
{
// windows has minor formatting differences (line breaks)
$this->skipIfNotOnUnix();

putenv('COLUMNS=80');
if ($showAllErrors) {
if ($errorsBudget !== null) {
$this->fail('showAllErrors cannot be true when errorsBudget is set');
}
putenv('PHPSTAN_TABLE_ERROR_FORMATTER_FORCE_SHOW_ALL_ERRORS=1');
$errorsBudget = null;
} else {
putenv('PHPSTAN_TABLE_ERROR_FORMATTER_FORCE_SHOW_ALL_ERRORS');
}

$formatter = $this->createErrorFormatter(
null,
null,
$usedLevel,
$errorsBudget,
);
$errors = [];
$line = 12;
for ($i = 0; $i < $generateErrorsCount; $i++) {
$errors[] = new Error('Test', 'Foo.php (in context of trait)', $line, filePath: 'Foo.php', traitFilePath: 'Bar.php');
$line++;
}
$formatter->formatErrors(new AnalysisResult($errors, [], [], [], [], false, null, true, 0, false, []), $this->getOutput());

$this->assertStringContainsString($expected, $this->getOutputContent());
}

public function testEditorUrlWithTrait(): void
{
$formatter = $this->createErrorFormatter('editor://%file%/%line%');
Expand Down Expand Up @@ -441,14 +635,20 @@ public function testJetBrainsTerminalRelativePath(): void
false,
null,
null,
CommandHelper::DEFAULT_LEVEL,
);
$error = new Error('Test', 'Foo.php', 12, filePath: self::DIRECTORY_PATH . '/rel/Foo.php');
$formatter->formatErrors(new AnalysisResult([$error], [], [], [], [], false, null, true, 0, false, []), $this->getOutput(true));

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

private function createErrorFormatter(?string $editorUrl, ?string $editorUrlTitle = null): TableErrorFormatter
private function createErrorFormatter(
?string $editorUrl,
?string $editorUrlTitle = null,
string $usedLevel = CommandHelper::DEFAULT_LEVEL,
?int $errorsBudget = null,
): TableErrorFormatter
{
$relativePathHelper = new FuzzyRelativePathHelper(new NullRelativePathHelper(), self::DIRECTORY_PATH, [], '/');

Expand All @@ -462,6 +662,8 @@ private function createErrorFormatter(?string $editorUrl, ?string $editorUrlTitl
false,
$editorUrl,
$editorUrlTitle,
$usedLevel,
$errorsBudget,
);
}

Expand Down
Loading