From bf1517ae769c607e585d4c966ad1ab1bc20e0606 Mon Sep 17 00:00:00 2001 From: Sylvain Fabre Date: Wed, 15 Apr 2026 09:42:46 +0200 Subject: [PATCH 1/3] Add origin field to baseline entries for trait errors, report unmatched rules when origin is analysed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an ignore rule has a specific path and an origin (the trait file where the error actually originates), PHPStan can now determine whether the origin file was part of the analysis. If both path and origin were analysed and the rule was still unmatched, it is now reported — fixing a gap where unmatched baseline entries were silently suppressed in onlyFiles mode even when PHPStan had full information. Baseline formatters (NEON and PHP) now emit an `origin` field for errors that come from trait files, so baseline entries carry the necessary context for this check. Co-Authored-By: Claude Sonnet 4.6 --- conf/parametersSchema.neon | 1 + phpstan-baseline.neon | 4 +- src/Analyser/Ignore/IgnoredErrorHelper.php | 9 +++ .../Ignore/IgnoredErrorHelperResult.php | 6 +- .../BaselineNeonErrorFormatter.php | 47 +++++++---- .../BaselinePhpErrorFormatter.php | 80 +++++++++++++------ tests/PHPStan/Analyser/AnalyserTest.php | 43 ++++++++++ 7 files changed, 146 insertions(+), 44 deletions(-) diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index bc79fe7c401..04e84c9e3ab 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -135,6 +135,7 @@ parametersSchema: ?paths: listOf(string()) ?count: int() ?reportUnmatched: bool() + ?origin: string() ]), ) ) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index bb51fac85b5..6e90eb5da42 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -159,13 +159,13 @@ parameters: - rawMessage: 'Call to static method escape() of internal class Nette\DI\Helpers from outside its root namespace Nette.' identifier: staticMethod.internalClass - count: 4 + count: 6 path: src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php - rawMessage: 'Call to static method escape() of internal class Nette\DI\Helpers from outside its root namespace Nette.' identifier: staticMethod.internalClass - count: 5 + count: 12 path: src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php - diff --git a/src/Analyser/Ignore/IgnoredErrorHelper.php b/src/Analyser/Ignore/IgnoredErrorHelper.php index d3394bcb0bb..c1f3b20f260 100644 --- a/src/Analyser/Ignore/IgnoredErrorHelper.php +++ b/src/Analyser/Ignore/IgnoredErrorHelper.php @@ -97,6 +97,9 @@ public function initialize(): IgnoredErrorHelperResult if (isset($ignoreError['identifier'])) { $key = sprintf("%s\n%s", $key, $ignoreError['identifier']); } + if (isset($ignoreError['origin'])) { + $key = sprintf("%s\n%s", $key, $ignoreError['origin']); + } if ($key === '') { throw new ShouldNotHappenException(); } @@ -115,6 +118,7 @@ public function initialize(): IgnoredErrorHelperResult 'message' => $ignoreError['message'] ?? null, 'rawMessage' => $ignoreError['rawMessage'] ?? null, 'path' => $ignoreError['path'], + 'origin' => $ignoreError['origin'] ?? null, 'identifier' => $ignoreError['identifier'] ?? null, 'count' => ($uniquedExpandedIgnoreErrors[$key]['count'] ?? 1) + ($ignoreError['count'] ?? 1), 'reportUnmatched' => $reportUnmatched, @@ -144,6 +148,11 @@ public function initialize(): IgnoredErrorHelperResult $ignoreError['path'] = $normalizedPath; $ignoreErrorsByFile[$normalizedPath][] = $ignoreErrorEntry; $ignoreError['realPath'] = $normalizedPath; + if (isset($ignoreError['origin']) && @is_file($ignoreError['origin'])) { + $normalizedOrigin = $this->fileHelper->normalizePath($ignoreError['origin']); + $ignoreError['origin'] = $normalizedOrigin; + $ignoreError['realOrigin'] = $normalizedOrigin; + } $expandedIgnoreErrors[$i] = $ignoreError; } else { $otherIgnoreErrors[] = $ignoreErrorEntry; diff --git a/src/Analyser/Ignore/IgnoredErrorHelperResult.php b/src/Analyser/Ignore/IgnoredErrorHelperResult.php index ea4c1295309..e29f693ce58 100644 --- a/src/Analyser/Ignore/IgnoredErrorHelperResult.php +++ b/src/Analyser/Ignore/IgnoredErrorHelperResult.php @@ -219,7 +219,11 @@ public function process( continue; } - if ($onlyFiles) { + if (isset($unmatchedIgnoredError['realOrigin'])) { + if (!array_key_exists($unmatchedIgnoredError['realOrigin'], $analysedFilesKeys)) { + continue; + } + } elseif ($onlyFiles) { continue; } diff --git a/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php b/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php index b5ea4914f16..f466a7f263b 100644 --- a/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php +++ b/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php @@ -38,41 +38,48 @@ public function formatErrors( if (!$fileSpecificError->canBeIgnored()) { continue; } - $fileErrors[$this->relativePathHelper->getRelativePath($fileSpecificError->getFilePath())][] = $fileSpecificError; + $traitFilePath = $fileSpecificError->getTraitFilePath(); + $fileErrors[$this->relativePathHelper->getRelativePath($fileSpecificError->getFilePath())][] = [ + 'error' => $fileSpecificError, + 'origin' => $traitFilePath !== null ? $this->relativePathHelper->getRelativePath($traitFilePath) : null, + ]; } ksort($fileErrors, SORT_STRING); $messageKey = $this->useRawMessage ? 'rawMessage' : 'message'; $errorsToOutput = []; - foreach ($fileErrors as $file => $errors) { - $fileErrorsByMessage = []; - foreach ($errors as $error) { + foreach ($fileErrors as $file => $fileErrorEntries) { + $fileErrorsByKey = []; + foreach ($fileErrorEntries as ['error' => $error, 'origin' => $origin]) { $errorMessage = $error->getMessage(); $identifier = $error->getIdentifier(); - if (!isset($fileErrorsByMessage[$errorMessage])) { - $fileErrorsByMessage[$errorMessage] = [ - 1, - $identifier !== null ? [$identifier => 1] : [], + $key = $errorMessage . "\0" . ($origin ?? ''); + if (!isset($fileErrorsByKey[$key])) { + $fileErrorsByKey[$key] = [ + 'message' => $errorMessage, + 'origin' => $origin, + 'count' => 1, + 'identifiers' => $identifier !== null ? [$identifier => 1] : [], ]; continue; } - $fileErrorsByMessage[$errorMessage][0]++; + $fileErrorsByKey[$key]['count']++; if ($identifier === null) { continue; } - if (!isset($fileErrorsByMessage[$errorMessage][1][$identifier])) { - $fileErrorsByMessage[$errorMessage][1][$identifier] = 1; + if (!isset($fileErrorsByKey[$key]['identifiers'][$identifier])) { + $fileErrorsByKey[$key]['identifiers'][$identifier] = 1; continue; } - $fileErrorsByMessage[$errorMessage][1][$identifier]++; + $fileErrorsByKey[$key]['identifiers'][$identifier]++; } - ksort($fileErrorsByMessage, SORT_STRING); + ksort($fileErrorsByKey, SORT_STRING); - foreach ($fileErrorsByMessage as $message => [$totalCount, $identifiers]) { + foreach ($fileErrorsByKey as ['message' => $message, 'origin' => $origin, 'count' => $totalCount, 'identifiers' => $identifiers]) { if (!$this->useRawMessage) { $message = '#^' . preg_quote($message, '#') . '$#'; } @@ -80,19 +87,27 @@ public function formatErrors( ksort($identifiers, SORT_STRING); if (count($identifiers) > 0) { foreach ($identifiers as $identifier => $identifierCount) { - $errorsToOutput[] = [ + $entry = [ $messageKey => Helpers::escape($message), 'identifier' => $identifier, 'count' => $identifierCount, 'path' => Helpers::escape($file), ]; + if ($origin !== null) { + $entry['origin'] = Helpers::escape($origin); + } + $errorsToOutput[] = $entry; } } else { - $errorsToOutput[] = [ + $entry = [ $messageKey => Helpers::escape($message), 'count' => $totalCount, 'path' => Helpers::escape($file), ]; + if ($origin !== null) { + $entry['origin'] = Helpers::escape($origin); + } + $errorsToOutput[] = $entry; } } } diff --git a/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php b/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php index ac4c1fc81fa..94277c95274 100644 --- a/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php +++ b/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php @@ -39,7 +39,11 @@ public function formatErrors( if (!$fileSpecificError->canBeIgnored()) { continue; } - $fileErrors['/' . $this->relativePathHelper->getRelativePath($fileSpecificError->getFilePath())][] = $fileSpecificError; + $traitFilePath = $fileSpecificError->getTraitFilePath(); + $fileErrors['/' . $this->relativePathHelper->getRelativePath($fileSpecificError->getFilePath())][] = [ + 'error' => $fileSpecificError, + 'origin' => $traitFilePath !== null ? '/' . $this->relativePathHelper->getRelativePath($traitFilePath) : null, + ]; } ksort($fileErrors, SORT_STRING); @@ -47,35 +51,38 @@ public function formatErrors( $php .= "\n\n"; $php .= '$ignoreErrors = [];'; $php .= "\n"; - foreach ($fileErrors as $file => $errors) { - $fileErrorsByMessage = []; - foreach ($errors as $error) { + foreach ($fileErrors as $file => $fileErrorEntries) { + $fileErrorsByKey = []; + foreach ($fileErrorEntries as ['error' => $error, 'origin' => $origin]) { $errorMessage = $error->getMessage(); $identifier = $error->getIdentifier(); - if (!isset($fileErrorsByMessage[$errorMessage])) { - $fileErrorsByMessage[$errorMessage] = [ - 1, - $identifier !== null ? [$identifier => 1] : [], + $key = $errorMessage . "\0" . ($origin ?? ''); + if (!isset($fileErrorsByKey[$key])) { + $fileErrorsByKey[$key] = [ + 'message' => $errorMessage, + 'origin' => $origin, + 'count' => 1, + 'identifiers' => $identifier !== null ? [$identifier => 1] : [], ]; continue; } - $fileErrorsByMessage[$errorMessage][0]++; + $fileErrorsByKey[$key]['count']++; if ($identifier === null) { continue; } - if (!isset($fileErrorsByMessage[$errorMessage][1][$identifier])) { - $fileErrorsByMessage[$errorMessage][1][$identifier] = 1; + if (!isset($fileErrorsByKey[$key]['identifiers'][$identifier])) { + $fileErrorsByKey[$key]['identifiers'][$identifier] = 1; continue; } - $fileErrorsByMessage[$errorMessage][1][$identifier]++; + $fileErrorsByKey[$key]['identifiers'][$identifier]++; } - ksort($fileErrorsByMessage, SORT_STRING); + ksort($fileErrorsByKey, SORT_STRING); - foreach ($fileErrorsByMessage as $message => [$totalCount, $identifiers]) { + foreach ($fileErrorsByKey as ['message' => $message, 'origin' => $origin, 'count' => $totalCount, 'identifiers' => $identifiers]) { if ($this->useRawMessage) { $messageKey = 'rawMessage'; } else { @@ -86,23 +93,46 @@ public function formatErrors( ksort($identifiers, SORT_STRING); if (count($identifiers) > 0) { foreach ($identifiers as $identifier => $identifierCount) { + if ($origin !== null) { + $php .= sprintf( + "\$ignoreErrors[] = [\n\t%s => %s,\n\t'identifier' => %s,\n\t'count' => %s,\n\t'path' => __DIR__ . %s,\n\t'origin' => __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), + var_export(Helpers::escape($origin), true), + ); + } else { + $php .= sprintf( + "\$ignoreErrors[] = [\n\t%s => %s,\n\t'identifier' => %s,\n\t'count' => %s,\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), + ); + } + } + } else { + if ($origin !== null) { + $php .= sprintf( + "\$ignoreErrors[] = [\n\t%s => %s,\n\t'count' => %s,\n\t'path' => __DIR__ . %s,\n\t'origin' => __DIR__ . %s,\n];\n", + var_export($messageKey, true), + var_export(Helpers::escape($message), true), + var_export($totalCount, true), + var_export(Helpers::escape($file), true), + var_export(Helpers::escape($origin), true), + ); + } else { $php .= sprintf( - "\$ignoreErrors[] = [\n\t%s => %s,\n\t'identifier' => %s,\n\t'count' => %s,\n\t'path' => __DIR__ . %s,\n];\n", + "\$ignoreErrors[] = [\n\t%s => %s,\n\t'count' => %s,\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($totalCount, true), var_export(Helpers::escape($file), true), ); } - } else { - $php .= sprintf( - "\$ignoreErrors[] = [\n\t%s => %s,\n\t'count' => %s,\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/Analyser/AnalyserTest.php b/tests/PHPStan/Analyser/AnalyserTest.php index 39da6a5be9e..c5e5bf0c4ad 100644 --- a/tests/PHPStan/Analyser/AnalyserTest.php +++ b/tests/PHPStan/Analyser/AnalyserTest.php @@ -29,8 +29,10 @@ use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\FileTypeMapper; use PHPUnit\Framework\Attributes\DataProvider; +use function array_filter; use function array_map; use function array_merge; +use function array_values; use function assert; use function count; use function is_string; @@ -609,6 +611,47 @@ public function testDoNotReportUnmatchedIgnoredErrorsFromPathWithCountIfPathWasN $this->assertNoErrors($result); } + public function testDoNotReportUnmatchedIgnoredErrorsFromPathWithoutOriginInPartialAnalysis(): void + { + $ignoreErrors = [ + [ + 'message' => '#Unknown error#', + 'path' => __DIR__ . '/data/traits-ignore/Foo.php', + ], + ]; + $result = $this->runAnalyser($ignoreErrors, true, [ + __DIR__ . '/data/traits-ignore/Foo.php', + ], true); + $this->assertNoErrors($result); + } + + public function testReturnUnmatchedIgnoredErrorFromPathWithOriginWhenOriginIsAnalysed(): void + { + $ignoreErrors = [ + [ + 'message' => '#Unknown error#', + 'path' => __DIR__ . '/data/traits-ignore/Foo.php', + 'origin' => __DIR__ . '/data/traits-ignore/FooTrait.php', + ], + ]; + $result = $this->runAnalyser($ignoreErrors, true, [ + __DIR__ . '/data/traits-ignore/Foo.php', + __DIR__ . '/data/traits-ignore/FooTrait.php', + ], true); + // One result is the real fail() error (not suppressed by #Unknown error#), + // the other is the unmatched ignore rule reported because origin was analysed. + $this->assertCount(2, $result); + $unmatchedErrors = array_values(array_filter( + $result, + static fn ($r) => $r instanceof Error && $r->getIdentifier() === 'ignore.unmatched', + )); + $this->assertCount(1, $unmatchedErrors); + $this->assertSame( + 'Ignored error pattern #Unknown error# in path ' . __DIR__ . '/data/traits-ignore/Foo.php was not matched in reported errors.', + $unmatchedErrors[0]->getMessage(), + ); + } + public function testIgnoreNextLine(): void { $result = $this->runAnalyser([], false, [ From 973e6b5a560b5ef9e809b1bf9828430c0e0bc42b Mon Sep 17 00:00:00 2001 From: Sylvain Fabre Date: Wed, 15 Apr 2026 15:14:19 +0200 Subject: [PATCH 2/3] Fix baseline formatter splitting errors by origin, causing count mismatches When an error message appears multiple times in the same file (e.g. once directly and once from a trait), grouping by message+origin created separate baseline entries that both tried to match the same errors, causing 'expected N, occurred M' count overflows. Group by message only (restoring original behaviour). Track per-group origins and emit 'origin' only when all errors in the group share the same non-null trait file path. Co-Authored-By: Claude Sonnet 4.6 --- .../BaselineNeonErrorFormatter.php | 42 +++++++++++-------- .../BaselinePhpErrorFormatter.php | 42 +++++++++++-------- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php b/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php index f466a7f263b..fa8f50c05c6 100644 --- a/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php +++ b/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php @@ -9,6 +9,8 @@ use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; use PHPStan\ShouldNotHappenException; +use function array_key_first; +use function array_unique; use function count; use function ksort; use function preg_quote; @@ -49,63 +51,67 @@ public function formatErrors( $messageKey = $this->useRawMessage ? 'rawMessage' : 'message'; $errorsToOutput = []; foreach ($fileErrors as $file => $fileErrorEntries) { - $fileErrorsByKey = []; + $fileErrorsByMessage = []; foreach ($fileErrorEntries as ['error' => $error, 'origin' => $origin]) { $errorMessage = $error->getMessage(); $identifier = $error->getIdentifier(); - $key = $errorMessage . "\0" . ($origin ?? ''); - if (!isset($fileErrorsByKey[$key])) { - $fileErrorsByKey[$key] = [ - 'message' => $errorMessage, - 'origin' => $origin, + if (!isset($fileErrorsByMessage[$errorMessage])) { + $fileErrorsByMessage[$errorMessage] = [ 'count' => 1, - 'identifiers' => $identifier !== null ? [$identifier => 1] : [], + 'origins' => [$origin], + 'identifiers' => $identifier !== null ? [$identifier => ['count' => 1, 'origins' => [$origin]]] : [], ]; continue; } - $fileErrorsByKey[$key]['count']++; + $fileErrorsByMessage[$errorMessage]['count']++; + $fileErrorsByMessage[$errorMessage]['origins'][] = $origin; if ($identifier === null) { continue; } - if (!isset($fileErrorsByKey[$key]['identifiers'][$identifier])) { - $fileErrorsByKey[$key]['identifiers'][$identifier] = 1; + if (!isset($fileErrorsByMessage[$errorMessage]['identifiers'][$identifier])) { + $fileErrorsByMessage[$errorMessage]['identifiers'][$identifier] = ['count' => 1, 'origins' => [$origin]]; continue; } - $fileErrorsByKey[$key]['identifiers'][$identifier]++; + $fileErrorsByMessage[$errorMessage]['identifiers'][$identifier]['count']++; + $fileErrorsByMessage[$errorMessage]['identifiers'][$identifier]['origins'][] = $origin; } - ksort($fileErrorsByKey, SORT_STRING); + ksort($fileErrorsByMessage, SORT_STRING); - foreach ($fileErrorsByKey as ['message' => $message, 'origin' => $origin, 'count' => $totalCount, 'identifiers' => $identifiers]) { + foreach ($fileErrorsByMessage as $message => ['count' => $totalCount, 'origins' => $messageOrigins, 'identifiers' => $identifiers]) { if (!$this->useRawMessage) { $message = '#^' . preg_quote($message, '#') . '$#'; } ksort($identifiers, SORT_STRING); if (count($identifiers) > 0) { - foreach ($identifiers as $identifier => $identifierCount) { + foreach ($identifiers as $identifier => ['count' => $identifierCount, 'origins' => $identifierOrigins]) { + $uniqueOrigins = array_unique($identifierOrigins); + $uniformOrigin = count($uniqueOrigins) === 1 && $uniqueOrigins[array_key_first($uniqueOrigins)] !== null ? $uniqueOrigins[array_key_first($uniqueOrigins)] : null; $entry = [ $messageKey => Helpers::escape($message), 'identifier' => $identifier, 'count' => $identifierCount, 'path' => Helpers::escape($file), ]; - if ($origin !== null) { - $entry['origin'] = Helpers::escape($origin); + if ($uniformOrigin !== null) { + $entry['origin'] = Helpers::escape($uniformOrigin); } $errorsToOutput[] = $entry; } } else { + $uniqueOrigins = array_unique($messageOrigins); + $uniformOrigin = count($uniqueOrigins) === 1 && $uniqueOrigins[array_key_first($uniqueOrigins)] !== null ? $uniqueOrigins[array_key_first($uniqueOrigins)] : null; $entry = [ $messageKey => Helpers::escape($message), 'count' => $totalCount, 'path' => Helpers::escape($file), ]; - if ($origin !== null) { - $entry['origin'] = Helpers::escape($origin); + if ($uniformOrigin !== null) { + $entry['origin'] = Helpers::escape($uniformOrigin); } $errorsToOutput[] = $entry; } diff --git a/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php b/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php index 94277c95274..36301b60c03 100644 --- a/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php +++ b/src/Command/ErrorFormatter/BaselinePhpErrorFormatter.php @@ -6,6 +6,8 @@ use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; +use function array_key_first; +use function array_unique; use function count; use function ksort; use function preg_quote; @@ -52,37 +54,37 @@ public function formatErrors( $php .= '$ignoreErrors = [];'; $php .= "\n"; foreach ($fileErrors as $file => $fileErrorEntries) { - $fileErrorsByKey = []; + $fileErrorsByMessage = []; foreach ($fileErrorEntries as ['error' => $error, 'origin' => $origin]) { $errorMessage = $error->getMessage(); $identifier = $error->getIdentifier(); - $key = $errorMessage . "\0" . ($origin ?? ''); - if (!isset($fileErrorsByKey[$key])) { - $fileErrorsByKey[$key] = [ - 'message' => $errorMessage, - 'origin' => $origin, + if (!isset($fileErrorsByMessage[$errorMessage])) { + $fileErrorsByMessage[$errorMessage] = [ 'count' => 1, - 'identifiers' => $identifier !== null ? [$identifier => 1] : [], + 'origins' => [$origin], + 'identifiers' => $identifier !== null ? [$identifier => ['count' => 1, 'origins' => [$origin]]] : [], ]; continue; } - $fileErrorsByKey[$key]['count']++; + $fileErrorsByMessage[$errorMessage]['count']++; + $fileErrorsByMessage[$errorMessage]['origins'][] = $origin; if ($identifier === null) { continue; } - if (!isset($fileErrorsByKey[$key]['identifiers'][$identifier])) { - $fileErrorsByKey[$key]['identifiers'][$identifier] = 1; + if (!isset($fileErrorsByMessage[$errorMessage]['identifiers'][$identifier])) { + $fileErrorsByMessage[$errorMessage]['identifiers'][$identifier] = ['count' => 1, 'origins' => [$origin]]; continue; } - $fileErrorsByKey[$key]['identifiers'][$identifier]++; + $fileErrorsByMessage[$errorMessage]['identifiers'][$identifier]['count']++; + $fileErrorsByMessage[$errorMessage]['identifiers'][$identifier]['origins'][] = $origin; } - ksort($fileErrorsByKey, SORT_STRING); + ksort($fileErrorsByMessage, SORT_STRING); - foreach ($fileErrorsByKey as ['message' => $message, 'origin' => $origin, 'count' => $totalCount, 'identifiers' => $identifiers]) { + foreach ($fileErrorsByMessage as $message => ['count' => $totalCount, 'origins' => $messageOrigins, 'identifiers' => $identifiers]) { if ($this->useRawMessage) { $messageKey = 'rawMessage'; } else { @@ -92,8 +94,10 @@ public function formatErrors( ksort($identifiers, SORT_STRING); if (count($identifiers) > 0) { - foreach ($identifiers as $identifier => $identifierCount) { - if ($origin !== null) { + foreach ($identifiers as $identifier => ['count' => $identifierCount, 'origins' => $identifierOrigins]) { + $uniqueOrigins = array_unique($identifierOrigins); + $uniformOrigin = count($uniqueOrigins) === 1 && $uniqueOrigins[array_key_first($uniqueOrigins)] !== null ? $uniqueOrigins[array_key_first($uniqueOrigins)] : null; + if ($uniformOrigin !== null) { $php .= sprintf( "\$ignoreErrors[] = [\n\t%s => %s,\n\t'identifier' => %s,\n\t'count' => %s,\n\t'path' => __DIR__ . %s,\n\t'origin' => __DIR__ . %s,\n];\n", var_export($messageKey, true), @@ -101,7 +105,7 @@ public function formatErrors( var_export(Helpers::escape($identifier), true), var_export($identifierCount, true), var_export(Helpers::escape($file), true), - var_export(Helpers::escape($origin), true), + var_export(Helpers::escape($uniformOrigin), true), ); } else { $php .= sprintf( @@ -115,14 +119,16 @@ public function formatErrors( } } } else { - if ($origin !== null) { + $uniqueOrigins = array_unique($messageOrigins); + $uniformOrigin = count($uniqueOrigins) === 1 && $uniqueOrigins[array_key_first($uniqueOrigins)] !== null ? $uniqueOrigins[array_key_first($uniqueOrigins)] : null; + if ($uniformOrigin !== null) { $php .= sprintf( "\$ignoreErrors[] = [\n\t%s => %s,\n\t'count' => %s,\n\t'path' => __DIR__ . %s,\n\t'origin' => __DIR__ . %s,\n];\n", var_export($messageKey, true), var_export(Helpers::escape($message), true), var_export($totalCount, true), var_export(Helpers::escape($file), true), - var_export(Helpers::escape($origin), true), + var_export(Helpers::escape($uniformOrigin), true), ); } else { $php .= sprintf( From 2d790a91282d1a207b8fbb9d85c072ae52522d55 Mon Sep 17 00:00:00 2001 From: Sylvain Fabre Date: Wed, 15 Apr 2026 21:26:28 +0200 Subject: [PATCH 3/3] Update phpstan-baseline.neon with origin fields for trait-originated errors Regenerate the baseline to include the new `origin` field emitted by the updated formatters for errors that exclusively originate from a single trait file. Co-Authored-By: Claude Sonnet 4.6 --- phpstan-baseline.neon | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 6e90eb5da42..6d90470a736 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -758,6 +758,7 @@ parameters: identifier: closure.unusedUse count: 1 path: src/Testing/PHPStanTestCase.php + origin: src/Testing/PHPStanTestCaseTrait.php - rawMessage: 'Doing instanceof PHPStan\Type\ConstantScalarType is error-prone and deprecated. Use Type::isConstantScalarValue() or Type::getConstantScalarTypes() or Type::getConstantScalarValues() instead.' @@ -896,6 +897,7 @@ parameters: identifier: phpstanApi.instanceofType count: 2 path: src/Type/BooleanType.php + origin: src/Type/JustNullableTypeTrait.php - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantBooleanType is error-prone and deprecated. Use Type::isTrue() or Type::isFalse() instead.' @@ -980,36 +982,42 @@ parameters: identifier: phpstanApi.instanceofType count: 1 path: src/Type/Constant/ConstantBooleanType.php + origin: src/Type/Traits/ConstantScalarTypeTrait.php - rawMessage: 'Doing instanceof PHPStan\Type\ConstantScalarType is error-prone and deprecated. Use Type::isConstantScalarValue() or Type::getConstantScalarTypes() or Type::getConstantScalarValues() instead.' identifier: phpstanApi.instanceofType count: 4 path: src/Type/Constant/ConstantBooleanType.php + origin: src/Type/Traits/ConstantScalarTypeTrait.php - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantBooleanType is error-prone and deprecated. Use Type::isTrue() or Type::isFalse() instead.' identifier: phpstanApi.instanceofType count: 3 path: src/Type/Constant/ConstantBooleanType.php + origin: src/Type/Traits/ConstantScalarTypeTrait.php - rawMessage: 'Doing instanceof PHPStan\Type\ConstantScalarType is error-prone and deprecated. Use Type::isConstantScalarValue() or Type::getConstantScalarTypes() or Type::getConstantScalarValues() instead.' identifier: phpstanApi.instanceofType count: 4 path: src/Type/Constant/ConstantFloatType.php + origin: src/Type/Traits/ConstantScalarTypeTrait.php - rawMessage: 'Doing instanceof PHPStan\Type\FloatType is error-prone and deprecated. Use Type::isFloat() instead.' identifier: phpstanApi.instanceofType count: 1 path: src/Type/Constant/ConstantFloatType.php + origin: src/Type/Traits/ConstantScalarTypeTrait.php - rawMessage: 'Doing instanceof PHPStan\Type\ConstantScalarType is error-prone and deprecated. Use Type::isConstantScalarValue() or Type::getConstantScalarTypes() or Type::getConstantScalarValues() instead.' identifier: phpstanApi.instanceofType count: 4 path: src/Type/Constant/ConstantIntegerType.php + origin: src/Type/Traits/ConstantScalarTypeTrait.php - rawMessage: 'Doing instanceof PHPStan\Type\IntegerType is error-prone and deprecated. Use Type::isInteger() instead.' @@ -1028,6 +1036,7 @@ parameters: identifier: phpstanApi.instanceofType count: 4 path: src/Type/Constant/ConstantStringType.php + origin: src/Type/Traits/ConstantScalarTypeTrait.php - rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.' @@ -1154,120 +1163,140 @@ parameters: identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateArrayType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateBenevolentUnionType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateBooleanType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateConstantArrayType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateConstantIntegerType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: 'Method PHPStan\Type\Generic\TemplateConstantIntegerType::toPhpDocNode() should return PHPStan\PhpDocParser\Ast\Type\ConstTypeNode but returns PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode.' identifier: return.type count: 1 path: src/Type/Generic/TemplateConstantIntegerType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateConstantStringType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateFloatType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateGenericObjectType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateIntegerType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateIntersectionType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateIterableType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateKeyOfType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 2 path: src/Type/Generic/TemplateMixedType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateNullType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateObjectShapeType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateObjectType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateObjectWithoutClassType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 2 path: src/Type/Generic/TemplateStrictMixedType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated. identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateStringType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: 'Doing instanceof PHPStan\Type\ArrayType is error-prone and deprecated. Use Type::isArray() or Type::getArrays() instead.' @@ -1358,6 +1387,7 @@ parameters: identifier: phpstanApi.instanceofType count: 3 path: src/Type/Generic/TemplateUnionType.php + origin: src/Type/Generic/TemplateTypeTrait.php - rawMessage: 'Doing instanceof PHPStan\Type\ConstantScalarType is error-prone and deprecated. Use Type::isConstantScalarValue() or Type::getConstantScalarTypes() or Type::getConstantScalarValues() instead.' @@ -1382,6 +1412,7 @@ parameters: identifier: phpstanApi.instanceofType count: 2 path: src/Type/IntegerType.php + origin: src/Type/JustNullableTypeTrait.php - rawMessage: 'Doing instanceof PHPStan\Type\ArrayType is error-prone and deprecated. Use Type::isArray() or Type::getArrays() instead.'