diff --git a/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php b/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php index 9d453b5bf1..0af50dba47 100644 --- a/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php +++ b/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php @@ -6,8 +6,12 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; +use function array_keys; +use function count; +use function implode; use function ksort; use function preg_quote; +use function sort; use function sprintf; use function var_export; use const SORT_STRING; @@ -38,7 +42,7 @@ public function formatErrors( if (!$fileSpecificError->canBeIgnored()) { continue; } - $fileErrors['/' . $this->relativePathHelper->getRelativePath($fileSpecificError->getFilePath())][] = $fileSpecificError->getMessage(); + $fileErrors['/' . $this->relativePathHelper->getRelativePath($fileSpecificError->getFilePath())][] = $fileSpecificError; } ksort($fileErrors, SORT_STRING); @@ -46,21 +50,42 @@ public function formatErrors( $php .= "\n\n"; $php .= '$ignoreErrors = [];'; $php .= "\n"; - foreach ($fileErrors as $file => $errorMessages) { - $fileErrorsCounts = []; - foreach ($errorMessages as $errorMessage) { - if (!isset($fileErrorsCounts[$errorMessage])) { - $fileErrorsCounts[$errorMessage] = 1; + foreach ($fileErrors as $file => $errors) { + $fileErrorsByMessage = []; + foreach ($errors as $error) { + $errorMessage = $error->getMessage(); + if (!isset($fileErrorsByMessage[$errorMessage])) { + $fileErrorsByMessage[$errorMessage] = [ + 1, + $error->getIdentifier() !== null ? [$error->getIdentifier() => true] : [], + ]; continue; } - $fileErrorsCounts[$errorMessage]++; + $fileErrorsByMessage[$errorMessage][0]++; + + if ($error->getIdentifier() === null) { + continue; + } + $fileErrorsByMessage[$errorMessage][1][$error->getIdentifier()] = true; } - ksort($fileErrorsCounts, SORT_STRING); + ksort($fileErrorsByMessage, SORT_STRING); + + foreach ($fileErrorsByMessage as $message => [$count, $identifiersInKeys]) { + $identifiers = array_keys($identifiersInKeys); + sort($identifiers); + $identifiersComment = ''; + if (count($identifiers) > 0) { + if (count($identifiers) === 1) { + $identifiersComment = "\n\t// identifier: " . $identifiers[0]; + } else { + $identifiersComment = "\n\t// identifiers: " . implode(', ', $identifiers); + } + } - foreach ($fileErrorsCounts as $message => $count) { $php .= sprintf( - "\$ignoreErrors[] = [\n\t'message' => %s,\n\t'count' => %d,\n\t'path' => __DIR__ . %s,\n];\n", + "\$ignoreErrors[] = [%s\n\t'message' => %s,\n\t'count' => %d,\n\t'path' => __DIR__ . %s,\n];\n", + $identifiersComment, var_export(Helpers::escape('#^' . preg_quote($message, '#') . '$#'), true), var_export($count, true), var_export(Helpers::escape($file), true), diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php index d69bfadc36..5e2546f4b3 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php @@ -47,6 +47,105 @@ public function dataFormatErrors(): iterable return ['parameters' => ['ignoreErrors' => $ignoreErrors]]; +PHP, + ]; + + yield [ + [ + new Error( + 'Foo', + __DIR__ . '/Foo.php', + 5, + ), + new Error( + 'Foo', + __DIR__ . '/Foo.php', + 5, + ), + (new Error( + 'Foo with identifier', + __DIR__ . '/Foo.php', + 5, + ))->withIdentifier('argument.type'), + (new Error( + 'Foo with identifier', + __DIR__ . '/Foo.php', + 6, + ))->withIdentifier('argument.type'), + ], + <<<'PHP' + '#^Foo$#', + 'count' => 2, + 'path' => __DIR__ . '/Foo.php', +]; +$ignoreErrors[] = [ + // identifier: argument.type + 'message' => '#^Foo with identifier$#', + 'count' => 2, + 'path' => __DIR__ . '/Foo.php', +]; + +return ['parameters' => ['ignoreErrors' => $ignoreErrors]]; + +PHP, + ]; + + yield [ + [ + new Error( + 'Foo', + __DIR__ . '/Foo.php', + 5, + ), + new Error( + 'Foo', + __DIR__ . '/Foo.php', + 5, + ), + (new Error( + 'Foo with same message, different identifier', + __DIR__ . '/Foo.php', + 5, + ))->withIdentifier('argument.type'), + (new Error( + 'Foo with same message, different identifier', + __DIR__ . '/Foo.php', + 6, + ))->withIdentifier('argument.byRef'), + (new Error( + 'Foo with another message', + __DIR__ . '/Foo.php', + 5, + ))->withIdentifier('argument.type'), + ], + <<<'PHP' + '#^Foo$#', + 'count' => 2, + 'path' => __DIR__ . '/Foo.php', +]; +$ignoreErrors[] = [ + // identifier: argument.type + 'message' => '#^Foo with another message$#', + 'count' => 1, + 'path' => __DIR__ . '/Foo.php', +]; +$ignoreErrors[] = [ + // identifiers: argument.byRef, argument.type + 'message' => '#^Foo with same message, different identifier$#', + 'count' => 2, + 'path' => __DIR__ . '/Foo.php', +]; + +return ['parameters' => ['ignoreErrors' => $ignoreErrors]]; + PHP, ]; }