From 9aec04f04f2cc2842c36edd6bb88cb77366aee05 Mon Sep 17 00:00:00 2001 From: Gregor Harlan Date: Sat, 19 Jul 2025 00:03:38 +0200 Subject: [PATCH] Introduce `rawMessage` key in `ignoreErrors` --- conf/parametersSchema.neon | 1 + src/Analyser/Ignore/IgnoredError.php | 9 ++ src/Analyser/Ignore/IgnoredErrorHelper.php | 10 ++- .../Ignore/IgnoredErrorHelperResult.php | 8 +- src/DependencyInjection/ContainerFactory.php | 14 +++- tests/PHPStan/Analyser/AnalyserTest.php | 84 +++++++++++++++++++ .../InvalidIgnoredErrorExceptionTest.php | 10 ++- .../rawMessage-and-message.neon | 5 ++ .../rawMessage-and-messages.neon | 5 ++ 9 files changed, 134 insertions(+), 12 deletions(-) create mode 100644 tests/PHPStan/DependencyInjection/invalidIgnoreErrors/rawMessage-and-message.neon create mode 100644 tests/PHPStan/DependencyInjection/invalidIgnoreErrors/rawMessage-and-messages.neon diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index dd4c63b8ca..e2bc1981d9 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -114,6 +114,7 @@ parametersSchema: structure([ ?message: string() ?messages: listOf(string()) + ?rawMessage: string() ?identifier: string() ?identifiers: listOf(string()) ?path: string() diff --git a/src/Analyser/Ignore/IgnoredError.php b/src/Analyser/Ignore/IgnoredError.php index 8f44d3cb28..6aae1fba4c 100644 --- a/src/Analyser/Ignore/IgnoredError.php +++ b/src/Analyser/Ignore/IgnoredError.php @@ -30,6 +30,8 @@ public static function stringifyPattern($ignoredError): string $message = ''; if (isset($ignoredError['message'])) { $message = $ignoredError['message']; + } elseif (isset($ignoredError['rawMessage'])) { + $message = '"' . $ignoredError['rawMessage'] . '"'; } if (isset($ignoredError['identifier'])) { if ($message === '') { @@ -71,6 +73,7 @@ public static function shouldIgnore( FileHelper $fileHelper, Error $error, ?string $ignoredErrorPattern, + ?string $ignoredErrorMessage, ?string $identifier, ?string $path, ): bool @@ -91,6 +94,12 @@ public static function shouldIgnore( } } + if ($ignoredErrorMessage !== null) { + if ($error->getMessage() !== $ignoredErrorMessage) { + return false; + } + } + if ($path !== null) { $fileExcluder = new FileExcluder($fileHelper, [$path]); $isExcluded = $fileExcluder->isExcludedFromAnalysing($error->getFilePath()); diff --git a/src/Analyser/Ignore/IgnoredErrorHelper.php b/src/Analyser/Ignore/IgnoredErrorHelper.php index 29fb13d818..19e9428f26 100644 --- a/src/Analyser/Ignore/IgnoredErrorHelper.php +++ b/src/Analyser/Ignore/IgnoredErrorHelper.php @@ -40,7 +40,7 @@ public function initialize(): IgnoredErrorHelperResult $expandedIgnoreErrors = []; foreach ($this->ignoreErrors as $ignoreError) { if (is_array($ignoreError)) { - if (!isset($ignoreError['message']) && !isset($ignoreError['messages']) && !isset($ignoreError['identifier']) && !isset($ignoreError['identifiers'])) { + if (!isset($ignoreError['message']) && !isset($ignoreError['messages']) && !isset($ignoreError['rawMessage']) && !isset($ignoreError['identifier']) && !isset($ignoreError['identifiers'])) { $errors[] = sprintf( 'Ignored error %s is missing a message or an identifier.', Json::encode($ignoreError), @@ -71,7 +71,7 @@ public function initialize(): IgnoredErrorHelperResult $uniquedExpandedIgnoreErrors = []; foreach ($expandedIgnoreErrors as $ignoreError) { - if (!isset($ignoreError['message']) && !isset($ignoreError['identifier'])) { + if (!isset($ignoreError['message']) && !isset($ignoreError['rawMessage']) && !isset($ignoreError['identifier'])) { $uniquedExpandedIgnoreErrors[] = $ignoreError; continue; } @@ -84,6 +84,9 @@ public function initialize(): IgnoredErrorHelperResult if (isset($ignoreError['message'])) { $key = sprintf("%s\n%s", $key, $ignoreError['message']); } + if (isset($ignoreError['rawMessage'])) { + $key = sprintf("%s\n%s", $key, $ignoreError['rawMessage']); + } if (isset($ignoreError['identifier'])) { $key = sprintf("%s\n%s", $key, $ignoreError['identifier']); } @@ -98,6 +101,7 @@ public function initialize(): IgnoredErrorHelperResult $uniquedExpandedIgnoreErrors[$key] = [ 'message' => $ignoreError['message'] ?? null, + 'rawMessage' => $ignoreError['rawMessage'] ?? null, 'path' => $ignoreError['path'], 'identifier' => $ignoreError['identifier'] ?? null, 'count' => ($uniquedExpandedIgnoreErrors[$key]['count'] ?? 1) + ($ignoreError['count'] ?? 1), @@ -114,7 +118,7 @@ public function initialize(): IgnoredErrorHelperResult ]; try { if (is_array($ignoreError)) { - if (!isset($ignoreError['message']) && !isset($ignoreError['identifier'])) { + if (!isset($ignoreError['message']) && !isset($ignoreError['rawMessage']) && !isset($ignoreError['identifier'])) { $errors[] = sprintf( 'Ignored error %s is missing a message or an identifier.', Json::encode($ignoreError), diff --git a/src/Analyser/Ignore/IgnoredErrorHelperResult.php b/src/Analyser/Ignore/IgnoredErrorHelperResult.php index 548b1b536c..7ef13fa85b 100644 --- a/src/Analyser/Ignore/IgnoredErrorHelperResult.php +++ b/src/Analyser/Ignore/IgnoredErrorHelperResult.php @@ -58,13 +58,13 @@ public function process( $processIgnoreError = function (Error $error, int $i, $ignore) use (&$unmatchedIgnoredErrors, &$stringErrors): bool { $shouldBeIgnored = false; if (is_string($ignore)) { - $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore, null, null); + $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore, null, null, null); if ($shouldBeIgnored) { unset($unmatchedIgnoredErrors[$i]); } } else { if (isset($ignore['path'])) { - $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'] ?? null, $ignore['identifier'] ?? null, $ignore['path']); + $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'] ?? null, $ignore['rawMessage'] ?? null, $ignore['identifier'] ?? null, $ignore['path']); if ($shouldBeIgnored) { if (isset($ignore['count'])) { $realCount = $unmatchedIgnoredErrors[$i]['realCount'] ?? 0; @@ -85,7 +85,7 @@ public function process( } } elseif (isset($ignore['paths'])) { foreach ($ignore['paths'] as $j => $ignorePath) { - $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'] ?? null, $ignore['identifier'] ?? null, $ignorePath); + $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'] ?? null, $ignore['rawMessage'] ?? null, $ignore['identifier'] ?? null, $ignorePath); if (!$shouldBeIgnored) { continue; } @@ -102,7 +102,7 @@ public function process( break; } } else { - $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'] ?? null, $ignore['identifier'] ?? null, null); + $shouldBeIgnored = IgnoredError::shouldIgnore($this->fileHelper, $error, $ignore['message'] ?? null, $ignore['rawMessage'] ?? null, $ignore['identifier'] ?? null, null); if ($shouldBeIgnored) { unset($unmatchedIgnoredErrors[$i]); } diff --git a/src/DependencyInjection/ContainerFactory.php b/src/DependencyInjection/ContainerFactory.php index 2d3e70691d..379b1fb741 100644 --- a/src/DependencyInjection/ContainerFactory.php +++ b/src/DependencyInjection/ContainerFactory.php @@ -337,14 +337,20 @@ private function validateParameters(array $parameters, array $parametersSchema): continue; } - $atLeastOneOf = ['message', 'messages', 'identifier', 'identifiers', 'path', 'paths']; + $atLeastOneOf = ['message', 'messages', 'rawMessage', 'identifier', 'identifiers', 'path', 'paths']; if (array_intersect($atLeastOneOf, array_keys($ignoreError)) === []) { throw new InvalidIgnoredErrorException('An ignoreErrors entry must contain at least one of the following fields: ' . implode(', ', $atLeastOneOf) . '.'); } - foreach (['message', 'identifier', 'path'] as $field) { - if (array_key_exists($field, $ignoreError) && array_key_exists($field . 's', $ignoreError)) { - throw new InvalidIgnoredErrorException(sprintf('An ignoreErrors entry cannot contain both %s and %s fields.', $field, $field . 's')); + foreach ([ + ['message', 'messages'], + ['rawMessage', 'message'], + ['rawMessage', 'messages'], + ['identifier', 'identifiers'], + ['path', 'paths'], + ] as [$field1, $field2]) { + if (array_key_exists($field1, $ignoreError) && array_key_exists($field2, $ignoreError)) { + throw new InvalidIgnoredErrorException(sprintf('An ignoreErrors entry cannot contain both %s and %s fields.', $field1, $field2)); } } diff --git a/tests/PHPStan/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index c868784527..b1bef92217 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -79,6 +79,12 @@ public function testFileWithAnIgnoredErrorMessage(): void $this->assertEmpty($result); } + public function testFileWithAnIgnoredErrorRawMessage(): void + { + $result = $this->runAnalyser([['rawMessage' => 'Fail.']], true, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertEmpty($result); + } + public function testFileWithAnIgnoredErrorMessageAndWrongIdentifier(): void { $result = $this->runAnalyser([['message' => '#Fail\.#', 'identifier' => 'wrong.identifier']], true, __DIR__ . '/data/bootstrap-error.php', false); @@ -89,6 +95,16 @@ public function testFileWithAnIgnoredErrorMessageAndWrongIdentifier(): void $this->assertSame('Ignored error pattern #Fail\.# (wrong.identifier) was not matched in reported errors.', $result[1]); } + public function testFileWithAnIgnoredErrorRawMessageAndWrongIdentifier(): void + { + $result = $this->runAnalyser([['rawMessage' => 'Fail.', 'identifier' => 'wrong.identifier']], true, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertCount(2, $result); + assert($result[0] instanceof Error); + $this->assertSame('Fail.', $result[0]->getMessage()); + assert(is_string($result[1])); + $this->assertSame('Ignored error pattern "Fail." (wrong.identifier) was not matched in reported errors.', $result[1]); + } + public function testFileWithAnIgnoredWrongIdentifier(): void { $result = $this->runAnalyser([['identifier' => 'wrong.identifier']], true, __DIR__ . '/data/bootstrap-error.php', false); @@ -105,6 +121,12 @@ public function testFileWithAnIgnoredErrorMessageAndCorrectIdentifier(): void $this->assertEmpty($result); } + public function testFileWithAnIgnoredErrorRawMessageAndCorrectIdentifier(): void + { + $result = $this->runAnalyser([['rawMessage' => 'Fail.', 'identifier' => 'tests.alwaysFail']], true, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertEmpty($result); + } + public function testFileWithAnIgnoredErrorIdentifier(): void { $result = $this->runAnalyser([['identifier' => 'tests.alwaysFail']], true, __DIR__ . '/data/bootstrap-error.php', false); @@ -216,6 +238,31 @@ public static function dataIgnoreErrorByPathAndCount(): iterable ], ], ]; + + yield [ + [ + [ + 'rawMessage' => 'Fail.', + 'count' => 3, + 'path' => __DIR__ . '/data/two-fails.php', + ], + ], + ]; + + yield [ + [ + [ + 'rawMessage' => 'Fail.', + 'count' => 2, + 'path' => __DIR__ . '/data/two-fails.php', + ], + [ + 'rawMessage' => 'Fail.', + 'count' => 1, + 'path' => __DIR__ . '/data/two-fails.php', + ], + ], + ]; } /** @@ -352,6 +399,18 @@ public function testIgnoreErrorByPaths(): void $this->assertNoErrors($result); } + public function testIgnoreErrorRawByPaths(): void + { + $ignoreErrors = [ + [ + 'rawMessage' => 'Fail.', + 'paths' => [__DIR__ . '/data/bootstrap-error.php'], + ], + ]; + $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/bootstrap-error.php', false); + $this->assertNoErrors($result); + } + public function testIgnoreErrorMultiByPaths(): void { $ignoreErrors = [ @@ -622,6 +681,18 @@ public function testIgnoreErrorExplicitReportUnmatchedDisable(): void $this->assertNoErrors($result); } + public function testIgnoreErrorExplicitReportUnmatchedDisableRaw(): void + { + $ignoreErrors = [ + [ + 'rawMessage' => 'Fail.', + 'reportUnmatched' => false, + ], + ]; + $result = $this->runAnalyser($ignoreErrors, true, __DIR__ . '/data/bootstrap.php', false); + $this->assertNoErrors($result); + } + public function testIgnoreErrorExplicitReportUnmatchedDisableMulti(): void { $ignoreErrors = [ @@ -647,6 +718,19 @@ public function testIgnoreErrorExplicitReportUnmatchedEnable(): void $this->assertSame('Ignored error pattern #Fail# was not matched in reported errors.', $result[0]); } + public function testIgnoreErrorExplicitReportUnmatchedEnableRaw(): void + { + $ignoreErrors = [ + [ + 'rawMessage' => 'Fail.', + 'reportUnmatched' => true, + ], + ]; + $result = $this->runAnalyser($ignoreErrors, false, __DIR__ . '/data/bootstrap.php', false); + $this->assertCount(1, $result); + $this->assertSame('Ignored error pattern "Fail." was not matched in reported errors.', $result[0]); + } + public function testIgnoreErrorExplicitReportUnmatchedEnableMulti(): void { $ignoreErrors = [ diff --git a/tests/PHPStan/DependencyInjection/InvalidIgnoredErrorExceptionTest.php b/tests/PHPStan/DependencyInjection/InvalidIgnoredErrorExceptionTest.php index ed9af62b15..a37a7a73ee 100644 --- a/tests/PHPStan/DependencyInjection/InvalidIgnoredErrorExceptionTest.php +++ b/tests/PHPStan/DependencyInjection/InvalidIgnoredErrorExceptionTest.php @@ -19,6 +19,14 @@ public static function dataValidateIgnoreErrors(): iterable __DIR__ . '/invalidIgnoreErrors/message-and-messages.neon', 'An ignoreErrors entry cannot contain both message and messages fields.', ]; + yield [ + __DIR__ . '/invalidIgnoreErrors/rawMessage-and-message.neon', + 'An ignoreErrors entry cannot contain both rawMessage and message fields.', + ]; + yield [ + __DIR__ . '/invalidIgnoreErrors/rawMessage-and-messages.neon', + 'An ignoreErrors entry cannot contain both rawMessage and messages fields.', + ]; yield [ __DIR__ . '/invalidIgnoreErrors/identifier-and-identifiers.neon', 'An ignoreErrors entry cannot contain both identifier and identifiers fields.', @@ -29,7 +37,7 @@ public static function dataValidateIgnoreErrors(): iterable ]; yield [ __DIR__ . '/invalidIgnoreErrors/missing-main-key.neon', - 'An ignoreErrors entry must contain at least one of the following fields: message, messages, identifier, identifiers, path, paths.', + 'An ignoreErrors entry must contain at least one of the following fields: message, messages, rawMessage, identifier, identifiers, path, paths.', ]; yield [ __DIR__ . '/invalidIgnoreErrors/count-without-path.neon', diff --git a/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/rawMessage-and-message.neon b/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/rawMessage-and-message.neon new file mode 100644 index 0000000000..1aa155d135 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/rawMessage-and-message.neon @@ -0,0 +1,5 @@ +parameters: + ignoreErrors: + - + rawMessage: 'One' + message: '#Two#' diff --git a/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/rawMessage-and-messages.neon b/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/rawMessage-and-messages.neon new file mode 100644 index 0000000000..39f9515c4d --- /dev/null +++ b/tests/PHPStan/DependencyInjection/invalidIgnoreErrors/rawMessage-and-messages.neon @@ -0,0 +1,5 @@ +parameters: + ignoreErrors: + - + rawMessage: 'One' + messages: ['#Two#']