From 6646411b788635d94fc8e95b9db497a0925a97cf Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 29 May 2026 09:26:12 +0200 Subject: [PATCH 1/5] Fix file path of trait errors reported directly in the trait When an error inside a trait is reported once directly in the trait (Error::removeTraitContext()), the displayed file is moved to the trait file but filePath was left pointing at the using-class file. This broke inline @phpstan-ignore matching (looked up by filePath) and the editorUrl (getTraitFilePath() ?? getFilePath()), both resolving to the wrong file. Make filePath follow the file onto the trait, matching changeFilePath(). Fixes phpstan/phpstan#14718 Co-Authored-By: Claude Code --- src/Analyser/Error.php | 2 +- tests/PHPStan/Analyser/ErrorTest.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Analyser/Error.php b/src/Analyser/Error.php index 107db06f08a..23d379b808a 100644 --- a/src/Analyser/Error.php +++ b/src/Analyser/Error.php @@ -117,7 +117,7 @@ public function removeTraitContext(): self $this->traitFilePath, $this->line, $this->canBeIgnored, - $this->filePath, + $this->traitFilePath, null, $this->tip, $this->nodeLine, diff --git a/tests/PHPStan/Analyser/ErrorTest.php b/tests/PHPStan/Analyser/ErrorTest.php index e59abafff36..f6aa16b8799 100644 --- a/tests/PHPStan/Analyser/ErrorTest.php +++ b/tests/PHPStan/Analyser/ErrorTest.php @@ -16,6 +16,20 @@ public function testError(): void $this->assertSame(10, $error->getLine()); } + public function testRemoveTraitContextUsesTraitFileAsFilePath(): void + { + $error = new Error('Message', 'trait.php (in context of class C)', 11, true, 'user.php', 'trait.php'); + $this->assertSame('user.php', $error->getFilePath()); + $this->assertSame('trait.php', $error->getTraitFilePath()); + + $withoutTraitContext = $error->removeTraitContext(); + $this->assertSame('trait.php', $withoutTraitContext->getFile()); + // filePath must follow the file onto the trait, otherwise editor URLs and + // inline @phpstan-ignore lookups point at the using-class file (#14718). + $this->assertSame('trait.php', $withoutTraitContext->getFilePath()); + $this->assertNull($withoutTraitContext->getTraitFilePath()); + } + public static function dataValidIdentifier(): iterable { yield ['a']; From 431e0e110f6ff817da27cc4eaa758649bba82887 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 29 May 2026 12:18:56 +0200 Subject: [PATCH 2/5] Add e2e test for trait error file path (#14718) Covers the behaviours from the issue against the fix: - inline @phpstan-ignore on a trait-deduplicated error suppresses it, consistently with an empty and a primed result cache - with the ignore removed, the error is reported in the trait file and a generated baseline records the trait file path, not the using-class file Co-Authored-By: Claude Code --- .github/workflows/e2e-tests.yml | 16 ++++++++++++++++ e2e/bug-14718/phpstan.neon | 4 ++++ e2e/bug-14718/src/Bar.php | 10 ++++++++++ e2e/bug-14718/src/Foo.php | 10 ++++++++++ e2e/bug-14718/src/FooTrait.php | 15 +++++++++++++++ 5 files changed, 55 insertions(+) create mode 100644 e2e/bug-14718/phpstan.neon create mode 100644 e2e/bug-14718/src/Bar.php create mode 100644 e2e/bug-14718/src/Foo.php create mode 100644 e2e/bug-14718/src/FooTrait.php diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index f5989b740b8..cc795e63757 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -297,6 +297,22 @@ jobs: ../bashunit -a contains 'FooTrait.php:10:Strict comparison using === between int<0, max> and false will always evaluate to false.' "$OUTPUT" ../bashunit -a contains 'FooTrait.php (in context of class E2EInTrait\Bar):18:Strict comparison using === between E2EInTrait\Bar and null will always evaluate to false.' "$OUTPUT" ../bashunit -a contains 'FooTrait.php (in context of class E2EInTrait\Foo):18:Strict comparison using === between E2EInTrait\Foo and null will always evaluate to false.' "$OUTPUT" + - script: | + cd e2e/bug-14718 + # https://github.com/phpstan/phpstan/issues/14718 + # an inline @phpstan-ignore on an error deduplicated into the trait must suppress it, + # consistently with both an empty and a primed result cache + ../../bin/phpstan clear-result-cache + ../bashunit -a exit_code "0" "../../bin/phpstan --error-format=raw" + ../bashunit -a exit_code "0" "../../bin/phpstan --error-format=raw" + # with the ignore removed, the error is reported in the trait file and a generated + # baseline records the trait file path, not the using-class file + sed -i 's# // @phpstan-ignore identical.alwaysFalse##' src/FooTrait.php + ../../bin/phpstan clear-result-cache + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan --error-format=raw") + ../bashunit -a contains 'FooTrait.php:10:Strict comparison using === between int<0, max> and false will always evaluate to false.' "$OUTPUT" + ../../bin/phpstan --generate-baseline=baseline.neon + ../bashunit -a contains 'path: src/FooTrait.php' "$(cat baseline.neon)" - script: | cd e2e/result-cache-meta-extension composer install diff --git a/e2e/bug-14718/phpstan.neon b/e2e/bug-14718/phpstan.neon new file mode 100644 index 00000000000..c308dcf5421 --- /dev/null +++ b/e2e/bug-14718/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: 8 + paths: + - src diff --git a/e2e/bug-14718/src/Bar.php b/e2e/bug-14718/src/Bar.php new file mode 100644 index 00000000000..11a8023e727 --- /dev/null +++ b/e2e/bug-14718/src/Bar.php @@ -0,0 +1,10 @@ + Date: Fri, 29 May 2026 15:19:07 +0200 Subject: [PATCH 3/5] Avoid literal ignore annotation in test comment The phrase tripped PHPStan's own ignore.parseError rule during self-analysis. Reword the comment. Co-Authored-By: Claude Code --- tests/PHPStan/Analyser/ErrorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/ErrorTest.php b/tests/PHPStan/Analyser/ErrorTest.php index f6aa16b8799..6f88d692f59 100644 --- a/tests/PHPStan/Analyser/ErrorTest.php +++ b/tests/PHPStan/Analyser/ErrorTest.php @@ -25,7 +25,7 @@ public function testRemoveTraitContextUsesTraitFileAsFilePath(): void $withoutTraitContext = $error->removeTraitContext(); $this->assertSame('trait.php', $withoutTraitContext->getFile()); // filePath must follow the file onto the trait, otherwise editor URLs and - // inline @phpstan-ignore lookups point at the using-class file (#14718). + // inline ignore-comment lookups point at the using-class file (#14718). $this->assertSame('trait.php', $withoutTraitContext->getFilePath()); $this->assertNull($withoutTraitContext->getTraitFilePath()); } From ebbd290a45532b736fa358a3fb4f1b30f3071a1f Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 29 May 2026 15:38:51 +0200 Subject: [PATCH 4/5] Update self-analysis baseline for relocated trait error The identical.alwaysFalse error in MbFunctionsReturnTypeExtensionTrait (used by StrSplitFunctionReturnTypeExtension) is now attributed to the trait file instead of the using class, so move its baseline entry. Co-Authored-By: Claude Code --- build/baseline-8.0.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/baseline-8.0.neon b/build/baseline-8.0.neon index e249b3ce3e6..29b3aedabd3 100644 --- a/build/baseline-8.0.neon +++ b/build/baseline-8.0.neon @@ -22,4 +22,4 @@ parameters: message: '#^Strict comparison using \=\=\= between list\ and false will always evaluate to false\.$#' identifier: identical.alwaysFalse count: 1 - path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php + path: ../src/Type/Php/MbFunctionsReturnTypeExtensionTrait.php From 788bc61ea8d472bbebd59d5c584be2ac9c017b9d Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 1 Jun 2026 08:59:21 +0200 Subject: [PATCH 5/5] Keep traitFilePath on dedup'd trait errors instead of rewriting filePath The previous approach overwrote filePath with the trait path in removeTraitContext(), which broke ignoreErrors/baseline entries keyed on the using-class path - a BC break (both paths used to match). Instead, removeTraitContext() now keeps traitFilePath set, leaving filePath as the using-class file. As a result: - the editor URL resolves to the trait via getTraitFilePath() - IgnoredErrorHelperResult keeps matching BOTH the using-class path and the trait path, so no existing ignore/baseline breaks The inline @phpstan-ignore lookup for these collector errors is fixed at its actual processing site (AnalyserResultFinalizer, where collector errors are finalized) by selecting the ignore bucket with getTraitFilePath() ?? getFilePath(). Reverts the self-analysis baseline relocation, and reworks the e2e to assert both ignore paths and a clean baseline round-trip. Co-Authored-By: Claude Code --- .github/workflows/e2e-tests.yml | 19 ++++++++++++++----- build/baseline-8.0.neon | 2 +- e2e/bug-14718/.gitignore | 1 + e2e/bug-14718/ignore-class-path.neon | 7 +++++++ e2e/bug-14718/ignore-trait-path.neon | 7 +++++++ e2e/bug-14718/with-baseline.neon | 3 +++ src/Analyser/AnalyserResultFinalizer.php | 2 +- src/Analyser/Error.php | 2 +- tests/PHPStan/Analyser/ErrorTest.php | 13 ++++++++----- 9 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 e2e/bug-14718/.gitignore create mode 100644 e2e/bug-14718/ignore-class-path.neon create mode 100644 e2e/bug-14718/ignore-trait-path.neon create mode 100644 e2e/bug-14718/with-baseline.neon diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index cc795e63757..09ddd3bede0 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -300,19 +300,28 @@ jobs: - script: | cd e2e/bug-14718 # https://github.com/phpstan/phpstan/issues/14718 - # an inline @phpstan-ignore on an error deduplicated into the trait must suppress it, - # consistently with both an empty and a primed result cache + # An inline @phpstan-ignore on an error deduplicated directly into the trait must + # suppress it, consistently with both an empty and a primed result cache. ../../bin/phpstan clear-result-cache ../bashunit -a exit_code "0" "../../bin/phpstan --error-format=raw" ../bashunit -a exit_code "0" "../../bin/phpstan --error-format=raw" - # with the ignore removed, the error is reported in the trait file and a generated - # baseline records the trait file path, not the using-class file + # With the ignore removed, the error is reported directly in the trait file. sed -i 's# // @phpstan-ignore identical.alwaysFalse##' src/FooTrait.php ../../bin/phpstan clear-result-cache OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan --error-format=raw") ../bashunit -a contains 'FooTrait.php:10:Strict comparison using === between int<0, max> and false will always evaluate to false.' "$OUTPUT" + # An ignoreErrors path may target the error by the trait file or by the using-class + # file - both keep working (no BC break). + ../../bin/phpstan clear-result-cache + ../bashunit -a exit_code "0" "../../bin/phpstan analyse --error-format=raw -c ignore-trait-path.neon" + ../../bin/phpstan clear-result-cache + ../bashunit -a exit_code "0" "../../bin/phpstan analyse --error-format=raw -c ignore-class-path.neon" + # A generated baseline suppresses the error on re-run, with both an empty and a + # primed result cache (the issue reported baseline generation as impossible). ../../bin/phpstan --generate-baseline=baseline.neon - ../bashunit -a contains 'path: src/FooTrait.php' "$(cat baseline.neon)" + ../../bin/phpstan clear-result-cache + ../bashunit -a exit_code "0" "../../bin/phpstan analyse --error-format=raw -c with-baseline.neon" + ../bashunit -a exit_code "0" "../../bin/phpstan analyse --error-format=raw -c with-baseline.neon" - script: | cd e2e/result-cache-meta-extension composer install diff --git a/build/baseline-8.0.neon b/build/baseline-8.0.neon index 29b3aedabd3..e249b3ce3e6 100644 --- a/build/baseline-8.0.neon +++ b/build/baseline-8.0.neon @@ -22,4 +22,4 @@ parameters: message: '#^Strict comparison using \=\=\= between list\ and false will always evaluate to false\.$#' identifier: identical.alwaysFalse count: 1 - path: ../src/Type/Php/MbFunctionsReturnTypeExtensionTrait.php + path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php diff --git a/e2e/bug-14718/.gitignore b/e2e/bug-14718/.gitignore new file mode 100644 index 00000000000..6690328b14d --- /dev/null +++ b/e2e/bug-14718/.gitignore @@ -0,0 +1 @@ +/baseline.neon diff --git a/e2e/bug-14718/ignore-class-path.neon b/e2e/bug-14718/ignore-class-path.neon new file mode 100644 index 00000000000..f7c33389dcb --- /dev/null +++ b/e2e/bug-14718/ignore-class-path.neon @@ -0,0 +1,7 @@ +includes: + - phpstan.neon +parameters: + ignoreErrors: + - + identifier: identical.alwaysFalse + path: src/Foo.php diff --git a/e2e/bug-14718/ignore-trait-path.neon b/e2e/bug-14718/ignore-trait-path.neon new file mode 100644 index 00000000000..c988c58e5ed --- /dev/null +++ b/e2e/bug-14718/ignore-trait-path.neon @@ -0,0 +1,7 @@ +includes: + - phpstan.neon +parameters: + ignoreErrors: + - + identifier: identical.alwaysFalse + path: src/FooTrait.php diff --git a/e2e/bug-14718/with-baseline.neon b/e2e/bug-14718/with-baseline.neon new file mode 100644 index 00000000000..1d2acfcc717 --- /dev/null +++ b/e2e/bug-14718/with-baseline.neon @@ -0,0 +1,3 @@ +includes: + - phpstan.neon + - baseline.neon diff --git a/src/Analyser/AnalyserResultFinalizer.php b/src/Analyser/AnalyserResultFinalizer.php index 627c946e691..34efa63e7b8 100644 --- a/src/Analyser/AnalyserResultFinalizer.php +++ b/src/Analyser/AnalyserResultFinalizer.php @@ -114,7 +114,7 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $ $collectorErrors = []; $locallyIgnoredCollectorErrors = []; foreach ($tempCollectorErrors as $tempCollectorError) { - $file = $tempCollectorError->getFilePath(); + $file = $tempCollectorError->getTraitFilePath() ?? $tempCollectorError->getFilePath(); $linesToIgnore = $allLinesToIgnore[$file] ?? []; $unmatchedLineIgnores = $allUnmatchedLineIgnores[$file] ?? []; $localIgnoresProcessorResult = $this->localIgnoresProcessor->process( diff --git a/src/Analyser/Error.php b/src/Analyser/Error.php index 23d379b808a..526594ed2fe 100644 --- a/src/Analyser/Error.php +++ b/src/Analyser/Error.php @@ -117,8 +117,8 @@ public function removeTraitContext(): self $this->traitFilePath, $this->line, $this->canBeIgnored, + $this->filePath, $this->traitFilePath, - null, $this->tip, $this->nodeLine, $this->nodeType, diff --git a/tests/PHPStan/Analyser/ErrorTest.php b/tests/PHPStan/Analyser/ErrorTest.php index 6f88d692f59..d4408a741d1 100644 --- a/tests/PHPStan/Analyser/ErrorTest.php +++ b/tests/PHPStan/Analyser/ErrorTest.php @@ -16,18 +16,21 @@ public function testError(): void $this->assertSame(10, $error->getLine()); } - public function testRemoveTraitContextUsesTraitFileAsFilePath(): void + public function testRemoveTraitContextKeepsTraitFilePath(): void { $error = new Error('Message', 'trait.php (in context of class C)', 11, true, 'user.php', 'trait.php'); $this->assertSame('user.php', $error->getFilePath()); $this->assertSame('trait.php', $error->getTraitFilePath()); $withoutTraitContext = $error->removeTraitContext(); + // The error is now reported directly in the trait: the displayed file is + // the trait, and traitFilePath is kept so the editor URL and the + // trait-file ignore lookups resolve to the trait (#14718). filePath stays + // the using-class file, so an ignoreErrors path keyed on either the trait + // or the using-class file keeps matching (no BC break). $this->assertSame('trait.php', $withoutTraitContext->getFile()); - // filePath must follow the file onto the trait, otherwise editor URLs and - // inline ignore-comment lookups point at the using-class file (#14718). - $this->assertSame('trait.php', $withoutTraitContext->getFilePath()); - $this->assertNull($withoutTraitContext->getTraitFilePath()); + $this->assertSame('user.php', $withoutTraitContext->getFilePath()); + $this->assertSame('trait.php', $withoutTraitContext->getTraitFilePath()); } public static function dataValidIdentifier(): iterable