diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index b5f69aee86..adc7fbb3d7 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -11,3 +11,4 @@ parameters: newStaticInAbstractClassStaticMethod: true checkExtensionsForComparisonOperators: true reportTooWideBool: true + rawMessageInBaseline: true diff --git a/conf/config.neon b/conf/config.neon index 5e703d4c19..6056ba948c 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -35,6 +35,7 @@ parameters: newStaticInAbstractClassStaticMethod: false checkExtensionsForComparisonOperators: false reportTooWideBool: false + rawMessageInBaseline: false fileExtensions: - php checkAdvancedIsset: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index e2bc1981d9..1781ffad33 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -38,6 +38,7 @@ parametersSchema: newStaticInAbstractClassStaticMethod: bool() checkExtensionsForComparisonOperators: bool() reportTooWideBool: bool() + rawMessageInBaseline: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index 926aa66408..f14d289baf 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -733,12 +733,13 @@ private function generateBaseline(string $generateBaselineFile, InceptionResult $baselineOutput = new SymfonyOutput($streamOutput, new SymfonyStyle($errorConsoleStyle)); $baselineFileDirectory = dirname($generateBaselineFile); $baselinePathHelper = new ParentDirectoryRelativePathHelper($baselineFileDirectory); + $rawMessageInBaseline = $inceptionResult->getContainer()->getParameter('featureToggles')['rawMessageInBaseline']; if ($baselineExtension === 'php') { - $baselineErrorFormatter = new BaselinePhpErrorFormatter($baselinePathHelper); + $baselineErrorFormatter = new BaselinePhpErrorFormatter($baselinePathHelper, $rawMessageInBaseline); $baselineErrorFormatter->formatErrors($analysisResult, $baselineOutput); } else { - $baselineErrorFormatter = new BaselineNeonErrorFormatter($baselinePathHelper); + $baselineErrorFormatter = new BaselineNeonErrorFormatter($baselinePathHelper, $rawMessageInBaseline); $existingBaselineContent = is_file($generateBaselineFile) ? FileReader::read($generateBaselineFile) : ''; $baselineErrorFormatter->formatErrors($analysisResult, $baselineOutput, $existingBaselineContent); } diff --git a/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php b/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php index ac02e1f9e1..b5ea4914f1 100644 --- a/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php +++ b/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php @@ -18,7 +18,7 @@ final class BaselineNeonErrorFormatter { - public function __construct(private RelativePathHelper $relativePathHelper) + public function __construct(private RelativePathHelper $relativePathHelper, private bool $useRawMessage) { } @@ -42,6 +42,7 @@ public function formatErrors( } ksort($fileErrors, SORT_STRING); + $messageKey = $this->useRawMessage ? 'rawMessage' : 'message'; $errorsToOutput = []; foreach ($fileErrors as $file => $errors) { $fileErrorsByMessage = []; @@ -72,11 +73,15 @@ public function formatErrors( ksort($fileErrorsByMessage, SORT_STRING); foreach ($fileErrorsByMessage as $message => [$totalCount, $identifiers]) { + if (!$this->useRawMessage) { + $message = '#^' . preg_quote($message, '#') . '$#'; + } + ksort($identifiers, SORT_STRING); if (count($identifiers) > 0) { foreach ($identifiers as $identifier => $identifierCount) { $errorsToOutput[] = [ - 'message' => Helpers::escape('#^' . preg_quote($message, '#') . '$#'), + $messageKey => Helpers::escape($message), 'identifier' => $identifier, 'count' => $identifierCount, 'path' => Helpers::escape($file), @@ -84,7 +89,7 @@ public function formatErrors( } } else { $errorsToOutput[] = [ - 'message' => Helpers::escape('#^' . preg_quote($message, '#') . '$#'), + $messageKey => Helpers::escape($message), 'count' => $totalCount, 'path' => Helpers::escape($file), ]; @@ -98,7 +103,7 @@ public function formatErrors( } /** - * @param array $ignoreErrors + * @param array> $ignoreErrors */ private function getNeon(array $ignoreErrors, string $existingBaselineContent): string { diff --git a/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php b/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php index 65cafffb9f..892a3e1eb2 100644 --- a/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php +++ b/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php @@ -16,7 +16,7 @@ final class BaselinePhpErrorFormatter { - public function __construct(private RelativePathHelper $relativePathHelper) + public function __construct(private RelativePathHelper $relativePathHelper, private bool $useRawMessage) { } @@ -76,12 +76,20 @@ public function formatErrors( ksort($fileErrorsByMessage, SORT_STRING); foreach ($fileErrorsByMessage as $message => [$totalCount, $identifiers]) { + if ($this->useRawMessage) { + $messageKey = 'rawMessage'; + } else { + $messageKey = 'message'; + $message = '#^' . preg_quote($message, '#') . '$#'; + } + ksort($identifiers, SORT_STRING); if (count($identifiers) > 0) { foreach ($identifiers as $identifier => $identifierCount) { $php .= sprintf( - "\$ignoreErrors[] = [\n\t'message' => %s,\n\t'identifier' => %s,\n\t'count' => %d,\n\t'path' => __DIR__ . %s,\n];\n", - var_export(Helpers::escape('#^' . preg_quote($message, '#') . '$#'), true), + "\$ignoreErrors[] = [\n\t%s => %s,\n\t'identifier' => %s,\n\t'count' => %d,\n\t'path' => __DIR__ . %s,\n];\n", + var_export($messageKey, true), + var_export(Helpers::escape($message), true), var_export(Helpers::escape($identifier), true), var_export($identifierCount, true), var_export(Helpers::escape($file), true), @@ -89,8 +97,9 @@ public function formatErrors( } } else { $php .= sprintf( - "\$ignoreErrors[] = [\n\t'message' => %s,\n\t'count' => %d,\n\t'path' => __DIR__ . %s,\n];\n", - var_export(Helpers::escape('#^' . preg_quote($message, '#') . '$#'), true), + "\$ignoreErrors[] = [\n\t%s => %s,\n\t'count' => %d,\n\t'path' => __DIR__ . %s,\n];\n", + var_export($messageKey, true), + var_export(Helpers::escape($message), true), var_export($totalCount, true), var_export(Helpers::escape($file), true), ); diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php index 48b4a54e55..1f9f4bfd27 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php @@ -36,6 +36,7 @@ public static function dataFormatterOutputProvider(): iterable 0, 0, 0, + false, [], ]; @@ -44,6 +45,7 @@ public static function dataFormatterOutputProvider(): iterable 1, 1, 0, + false, [ [ 'message' => '#^Foo$#', @@ -58,6 +60,7 @@ public static function dataFormatterOutputProvider(): iterable 1, 4, 0, + false, [ [ 'message' => "#^Bar\nBar2$#", @@ -87,6 +90,7 @@ public static function dataFormatterOutputProvider(): iterable 1, 4, 2, + false, [ [ 'message' => "#^Bar\nBar2$#", @@ -110,6 +114,36 @@ public static function dataFormatterOutputProvider(): iterable ], ], ]; + + yield [ + 'Multiple file, multiple generic errors (raw messages)', + 1, + 4, + 2, + true, + [ + [ + 'rawMessage' => "Bar\nBar2", + 'count' => 1, + 'path' => 'folder with unicode 😃/file name with "spaces" and unicode 😃.php', + ], + [ + 'rawMessage' => 'Foo', + 'count' => 1, + 'path' => 'folder with unicode 😃/file name with "spaces" and unicode 😃.php', + ], + [ + 'rawMessage' => "Bar\nBar2", + 'count' => 1, + 'path' => 'foo.php', + ], + [ + 'rawMessage' => 'Foo', + 'count' => 1, + 'path' => 'foo.php', + ], + ], + ]; } /** @@ -121,10 +155,11 @@ public function testFormatErrors( int $exitCode, int $numFileErrors, int $numGenericErrors, + bool $useRawMessage, array $expected, ): void { - $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH)); + $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH), $useRawMessage); $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), @@ -137,7 +172,7 @@ public function testFormatErrors( public function testFormatErrorMessagesRegexEscape(): void { - $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH)); + $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH), false); $result = new AnalysisResult( [new Error('Escape Regex with file # ~ \' ()', 'Testfile')], @@ -176,11 +211,51 @@ public function testFormatErrorMessagesRegexEscape(): void ); } - public function testEscapeDiNeon(): void + /** + * @return iterable}> + */ + public static function dataEscapeDiNeon(): iterable + { + yield [ + new Error('Test %value%', 'Testfile'), + false, + [ + 'message' => '#^Test %%value%%$#', + 'count' => 1, + 'path' => 'Testfile', + ], + ]; + + yield [ + new Error('Test %value%', 'Testfile'), + true, + [ + 'rawMessage' => 'Test %%value%%', + 'count' => 1, + 'path' => 'Testfile', + ], + ]; + + yield [ + new Error('@Foo', 'Testfile'), + true, + [ + 'rawMessage' => '@@Foo', + 'count' => 1, + 'path' => 'Testfile', + ], + ]; + } + + /** + * @param array $expected + */ + #[DataProvider('dataEscapeDiNeon')] + public function testEscapeDiNeon(Error $error, bool $useRawMessage, array $expected): void { - $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH)); + $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH), $useRawMessage); $result = new AnalysisResult( - [new Error('Test %value%', 'Testfile')], + [$error], [], [], [], @@ -203,11 +278,7 @@ public function testEscapeDiNeon(): void Neon::encode([ 'parameters' => [ 'ignoreErrors' => [ - [ - 'message' => '#^Test %%value%%$#', - 'count' => 1, - 'path' => 'Testfile', - ], + $expected, ], ], ], Neon::BLOCK), @@ -245,7 +316,7 @@ public static function outputOrderingProvider(): Generator #[DataProvider('outputOrderingProvider')] public function testOutputOrdering(array $errors): void { - $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH)); + $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH), false); $result = new AnalysisResult( $errors, [], @@ -404,7 +475,7 @@ public function testEndOfFileNewlines( int $expectedNewlinesCount, ): void { - $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH)); + $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH), false); $result = new AnalysisResult( $errors, [], @@ -468,6 +539,7 @@ public static function dataFormatErrorsWithIdentifiers(): iterable 6, ))->withIdentifier('argument.type'), ], + false, [ 'parameters' => [ 'ignoreErrors' => [ @@ -515,6 +587,7 @@ public static function dataFormatErrorsWithIdentifiers(): iterable 5, ))->withIdentifier('argument.type'), ], + false, [ 'parameters' => [ 'ignoreErrors' => [ @@ -545,6 +618,49 @@ public static function dataFormatErrorsWithIdentifiers(): iterable ], ], ]; + + 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'), + ], + true, + [ + 'parameters' => [ + 'ignoreErrors' => [ + [ + 'rawMessage' => 'Foo', + 'count' => 2, + 'path' => 'Foo.php', + ], + [ + 'rawMessage' => 'Foo with identifier', + 'identifier' => 'argument.type', + 'count' => 2, + 'path' => 'Foo.php', + ], + ], + ], + ], + ]; } /** @@ -552,9 +668,9 @@ public static function dataFormatErrorsWithIdentifiers(): iterable * @param mixed[] $expectedOutput */ #[DataProvider('dataFormatErrorsWithIdentifiers')] - public function testFormatErrorsWithIdentifiers(array $errors, array $expectedOutput): void + public function testFormatErrorsWithIdentifiers(array $errors, bool $useRawMessage, array $expectedOutput): void { - $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(__DIR__)); + $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(__DIR__), $useRawMessage); $formatter->formatErrors( new AnalysisResult( $errors, diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php index 868154dd43..2f8d95428d 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselinePhpErrorFormatterTest.php @@ -31,6 +31,7 @@ public static function dataFormatErrors(): iterable 5, ), ], + false, "withIdentifier('argument.type'), ], + false, "withIdentifier('argument.type'), ], + false, " __DIR__ . '/Foo.php', ]; +return ['parameters' => ['ignoreErrors' => \$ignoreErrors]]; +", + ]; + + yield [ + [ + new Error( + 'Foo A\\B\\C|null', + __DIR__ . '/Foo.php', + 5, + ), + new Error( + 'Foo A\\B\\C|null', + __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'), + ], + true, + " 'Foo A\\\\B\\\\C|null', + 'count' => 2, + 'path' => __DIR__ . '/Foo.php', +]; +\$ignoreErrors[] = [ + 'rawMessage' => 'Foo with another message', + 'identifier' => 'argument.type', + 'count' => 1, + 'path' => __DIR__ . '/Foo.php', +]; +\$ignoreErrors[] = [ + 'rawMessage' => 'Foo with same message, different identifier', + 'identifier' => 'argument.byRef', + 'count' => 1, + 'path' => __DIR__ . '/Foo.php', +]; +\$ignoreErrors[] = [ + 'rawMessage' => 'Foo with same message, different identifier', + 'identifier' => 'argument.type', + 'count' => 1, + 'path' => __DIR__ . '/Foo.php', +]; + return ['parameters' => ['ignoreErrors' => \$ignoreErrors]]; ", ]; @@ -155,9 +218,9 @@ public static function dataFormatErrors(): iterable * @param list $errors */ #[DataProvider('dataFormatErrors')] - public function testFormatErrors(array $errors, string $expectedOutput): void + public function testFormatErrors(array $errors, bool $useRawMessage, string $expectedOutput): void { - $formatter = new BaselinePhpErrorFormatter(new ParentDirectoryRelativePathHelper(__DIR__)); + $formatter = new BaselinePhpErrorFormatter(new ParentDirectoryRelativePathHelper(__DIR__), $useRawMessage); $formatter->formatErrors( new AnalysisResult( $errors,