From 96167f12db5a4adf90040a5678260336194a5127 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Mon, 3 Aug 2026 07:45:17 +0000 Subject: [PATCH] Look up line ignores under the analysed file for trait errors reported in the context of a class - `AnalyserResultFinalizer` matched deferred (`CollectedDataNode`) errors against `$allLinesToIgnore[$error->getTraitFilePath() ?? $error->getFilePath()]`. That outer key is the *analysed* file, so for an error reported as `Trait.php (in context of class Foo)` the ignores of the class file were never consulted and `@phpstan-ignore` had no effect. - New `AnalyserResultFinalizer::resolveAnalysedFileWithLineIgnores()` picks the entry that actually contains the error's file description: the using-class file for in-context errors, the trait file when the context was removed by `Error::removeTraitContext()`. - Fixes all identifiers deferred through `FunctionCallConstantConditionRule` and `ConstantConditionInTraitRule` (`function.alreadyNarrowedType`, `function.impossibleType`, `identical.alwaysFalse`, ...) and all inline ignore forms (`@phpstan-ignore `, `@phpstan-ignore-line`, `@phpstan-ignore-next-line`). - Probed and also fixed by the same change: classes in separate files, nested traits, anonymous classes, aliased trait methods (`use T { m as n; }`), primed result cache. - Probed and found already correct: a trait used by a single class (`removeTraitContext()`, the case fixed by #5780 - kept as a regression test), baseline generation for in-context trait errors, and non-trait collector errors. --- .github/workflows/e2e-tests.yml | 18 ++++++ e2e/bug-14993/.gitignore | 1 + e2e/bug-14993/phpstan.neon | 4 ++ e2e/bug-14993/src/CheckedTrait.php | 18 ++++++ e2e/bug-14993/src/First.php | 17 ++++++ e2e/bug-14993/src/Second.php | 17 ++++++ e2e/bug-14993/with-baseline.neon | 3 + src/Analyser/AnalyserResultFinalizer.php | 32 +++++++++- .../AnalyserTraitsIntegrationTest.php | 61 +++++++++++++++++++ .../ignore/IgnoreErrorsAnonymousClasses.php | 33 ++++++++++ .../ignore/IgnoreErrorsAnonymousTrait.php | 20 ++++++ .../traits/ignore/IgnoreErrorsClasses.php | 31 ++++++++++ .../ignore/IgnoreErrorsNestedClasses.php | 29 +++++++++ .../ignore/IgnoreErrorsNestedInnerTrait.php | 26 ++++++++ .../traits/ignore/IgnoreErrorsNestedTrait.php | 10 +++ .../ignore/IgnoreErrorsSeparateFilesFirst.php | 17 ++++++ .../IgnoreErrorsSeparateFilesSecond.php | 17 ++++++ .../ignore/IgnoreErrorsSeparateFilesTrait.php | 26 ++++++++ .../traits/ignore/IgnoreErrorsSingleClass.php | 17 ++++++ .../traits/ignore/IgnoreErrorsSingleTrait.php | 26 ++++++++ .../traits/ignore/IgnoreErrorsTrait.php | 49 +++++++++++++++ 21 files changed, 471 insertions(+), 1 deletion(-) create mode 100644 e2e/bug-14993/.gitignore create mode 100644 e2e/bug-14993/phpstan.neon create mode 100644 e2e/bug-14993/src/CheckedTrait.php create mode 100644 e2e/bug-14993/src/First.php create mode 100644 e2e/bug-14993/src/Second.php create mode 100644 e2e/bug-14993/with-baseline.neon create mode 100644 tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsAnonymousClasses.php create mode 100644 tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsAnonymousTrait.php create mode 100644 tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsClasses.php create mode 100644 tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsNestedClasses.php create mode 100644 tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsNestedInnerTrait.php create mode 100644 tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsNestedTrait.php create mode 100644 tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsSeparateFilesFirst.php create mode 100644 tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsSeparateFilesSecond.php create mode 100644 tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsSeparateFilesTrait.php create mode 100644 tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsSingleClass.php create mode 100644 tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsSingleTrait.php create mode 100644 tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsTrait.php diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 7c2b32f9365..cd54dc5e46f 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -338,6 +338,24 @@ jobs: ../../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/bug-14993 + # https://github.com/phpstan/phpstan/issues/14993 + # An inline @phpstan-ignore must also suppress errors reported in the context of + # each class using the trait, 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 once per using class. + sed -i 's# // @phpstan-ignore function.alreadyNarrowedType##' src/CheckedTrait.php + ../../bin/phpstan clear-result-cache + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan --error-format=raw") + ../bashunit -a contains "CheckedTrait.php (in context of class Bug14993\\First):13:Call to function is_subclass_of() with 'Bug14993\\\\First' and 'Countable' will always evaluate to true." "$OUTPUT" + ../bashunit -a contains "CheckedTrait.php (in context of class Bug14993\\Second):13:Call to function is_subclass_of() with 'Bug14993\\\\Second' and 'Countable' will always evaluate to true." "$OUTPUT" + # A generated baseline suppresses both errors on re-run. + ../../bin/phpstan --generate-baseline=baseline.neon + ../../bin/phpstan clear-result-cache + ../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/e2e/bug-14993/.gitignore b/e2e/bug-14993/.gitignore new file mode 100644 index 00000000000..6690328b14d --- /dev/null +++ b/e2e/bug-14993/.gitignore @@ -0,0 +1 @@ +/baseline.neon diff --git a/e2e/bug-14993/phpstan.neon b/e2e/bug-14993/phpstan.neon new file mode 100644 index 00000000000..c308dcf5421 --- /dev/null +++ b/e2e/bug-14993/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: 8 + paths: + - src diff --git a/e2e/bug-14993/src/CheckedTrait.php b/e2e/bug-14993/src/CheckedTrait.php new file mode 100644 index 00000000000..6abae0b4286 --- /dev/null +++ b/e2e/bug-14993/src/CheckedTrait.php @@ -0,0 +1,18 @@ +getTraitFilePath() ?? $tempCollectorError->getFilePath(); + $file = $this->resolveAnalysedFileWithLineIgnores($tempCollectorError, $allLinesToIgnore); $linesToIgnore = $allLinesToIgnore[$file] ?? []; $unmatchedLineIgnores = $allUnmatchedLineIgnores[$file] ?? []; $localIgnoresProcessorResult = $this->localIgnoresProcessor->process( @@ -159,6 +163,32 @@ public function finalize(AnalyserResult $analyserResult, bool $onlyFiles, bool $ ), $collectorErrors, $locallyIgnoredCollectorErrors); } + /** + * Line ignores are keyed by the file whose analysis discovered them. For an error + * reported in a trait that's the file using the trait when the error keeps its trait + * context, and the trait file itself when the context was removed (Error::removeTraitContext()) + * or when the trait was analysed on its own. + * + * @param array $allLinesToIgnore + */ + private function resolveAnalysedFileWithLineIgnores(Error $error, array $allLinesToIgnore): string + { + $traitFilePath = $error->getTraitFilePath(); + if ($traitFilePath === null) { + return $error->getFilePath(); + } + + foreach ([$error->getFilePath(), $traitFilePath] as $analysedFile) { + if (!array_key_exists($error->getFile(), $allLinesToIgnore[$analysedFile] ?? [])) { + continue; + } + + return $analysedFile; + } + + return $traitFilePath; + } + private function mergeFilteredPhpErrors(AnalyserResult $analyserResult): AnalyserResult { return new AnalyserResult( diff --git a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php index eb5fbc4d67a..7bd77a4e259 100644 --- a/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserTraitsIntegrationTest.php @@ -201,6 +201,53 @@ public function testUnititializedReadonlyPropertyAccessedInTrait(): void $this->assertSame($expectedFile, $error->getFile()); } + public function testIgnoreErrorsReportedInContextOfEachClassUsingTheTrait(): void + { + $errors = $this->runAnalyseAndFinalize([ + __DIR__ . '/traits/ignore/IgnoreErrorsClasses.php', + __DIR__ . '/traits/ignore/IgnoreErrorsTrait.php', + ]); + $this->assertNoErrors($errors); + } + + public function testIgnoreErrorsReportedInContextOfClassesLivingInSeparateFiles(): void + { + $errors = $this->runAnalyseAndFinalize([ + __DIR__ . '/traits/ignore/IgnoreErrorsSeparateFilesFirst.php', + __DIR__ . '/traits/ignore/IgnoreErrorsSeparateFilesSecond.php', + __DIR__ . '/traits/ignore/IgnoreErrorsSeparateFilesTrait.php', + ]); + $this->assertNoErrors($errors); + } + + public function testIgnoreErrorsReportedDirectlyInTraitUsedByASingleClass(): void + { + $errors = $this->runAnalyseAndFinalize([ + __DIR__ . '/traits/ignore/IgnoreErrorsSingleClass.php', + __DIR__ . '/traits/ignore/IgnoreErrorsSingleTrait.php', + ]); + $this->assertNoErrors($errors); + } + + public function testIgnoreErrorsReportedInContextOfClassUsingNestedTrait(): void + { + $errors = $this->runAnalyseAndFinalize([ + __DIR__ . '/traits/ignore/IgnoreErrorsNestedClasses.php', + __DIR__ . '/traits/ignore/IgnoreErrorsNestedTrait.php', + __DIR__ . '/traits/ignore/IgnoreErrorsNestedInnerTrait.php', + ]); + $this->assertNoErrors($errors); + } + + public function testIgnoreErrorsReportedInContextOfAnonymousClassUsingTrait(): void + { + $errors = $this->runAnalyseAndFinalize([ + __DIR__ . '/traits/ignore/IgnoreErrorsAnonymousClasses.php', + __DIR__ . '/traits/ignore/IgnoreErrorsAnonymousTrait.php', + ]); + $this->assertNoErrors($errors); + } + /** * @param string[] $files * @return Error[] @@ -214,6 +261,20 @@ private function runAnalyse(array $files): array return $analyser->analyse($files)->getErrors(); } + /** + * @param string[] $files + * @return list + */ + private function runAnalyseAndFinalize(array $files): array + { + $files = array_map(fn (string $file): string => $this->getFileHelper()->normalizePath($file), $files); + /** @var Analyser $analyser */ + $analyser = self::getContainer()->getByType(Analyser::class); + $finalizer = self::getContainer()->getByType(AnalyserResultFinalizer::class); + + return $finalizer->finalize($analyser->analyse($files), false, true)->getErrors(); + } + public static function getAdditionalConfigFiles(): array { return array_unique( diff --git a/tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsAnonymousClasses.php b/tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsAnonymousClasses.php new file mode 100644 index 00000000000..134652763b2 --- /dev/null +++ b/tests/PHPStan/Analyser/traits/ignore/IgnoreErrorsAnonymousClasses.php @@ -0,0 +1,33 @@ +