From 75ab3822482fdafb22fc520123a822ba47a6ee0c Mon Sep 17 00:00:00 2001 From: Alfred Bez <1001186+alfredbez@users.noreply.github.com> Date: Sun, 1 Nov 2026 10:32:08 +0000 Subject: [PATCH 1/3] Preserve ignore-comment errors in trait contexts across result cache runs When reportIgnoresWithoutComments is enabled, ignore.noComment and ignore.allLineErrors emitted for code inside traits were created with filePath set to the formatted file description instead of the analysed class file, and traitFilePath set to null. Because the result cache indexes errors by Error::getFilePath() and restores cached errors only for analysed files on warm runs, contextual ignore errors in traits were lost when restoring from cache. Pass the analysed file path and the trait file path to Error in FileAnalyser. --- .github/workflows/e2e-tests.yml | 19 +++++++++++++++++++ e2e/result-cache-trait-ignore/phpstan.neon | 6 ++++++ e2e/result-cache-trait-ignore/src/A.php | 8 ++++++++ e2e/result-cache-trait-ignore/src/B.php | 8 ++++++++ e2e/result-cache-trait-ignore/src/MyTrait.php | 14 ++++++++++++++ e2e/result-cache-trait-ignore/tmp/.gitignore | 2 ++ src/Analyser/FileAnalyser.php | 8 ++++---- src/Analyser/FileAnalyserCallback.php | 12 ++++++++++++ 8 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 e2e/result-cache-trait-ignore/phpstan.neon create mode 100644 e2e/result-cache-trait-ignore/src/A.php create mode 100644 e2e/result-cache-trait-ignore/src/B.php create mode 100644 e2e/result-cache-trait-ignore/src/MyTrait.php create mode 100644 e2e/result-cache-trait-ignore/tmp/.gitignore diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index ac31c5bd29a..0bc5db148e4 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -953,6 +953,25 @@ jobs: echo "$OUTPUT" ../bashunit assert contains 'Result cache not used because the metadata do not match: executedFilesHashes' "$OUTPUT" ../bashunit assert contains 'Foo.php:10:Method ResultCacheE2EBootstrap\Foo::doFoo() should return int but returns string.' "$OUTPUT" + - script: | + cd e2e/result-cache-trait-ignore + rm -rf tmp/cache tmp/resultCache.php + ../../bin/phpstan analyse --no-progress --error-format raw -vvv > tmp/cold.txt 2> tmp/cold-log.txt || test "$?" -eq 1 + ../../bin/phpstan analyse --no-progress --error-format raw -vvv > tmp/warm.txt 2> tmp/warm-log.txt || test "$?" -eq 1 + grep -F 'Result cache is saved.' tmp/cold-log.txt + grep -F 'Result cache restored. 0 files will be reanalysed.' tmp/warm-log.txt + grep -F 'MyTrait.php (in context of class Repro\A):10:Ignore with identifier method.notFound has no comment.' tmp/cold.txt + grep -F 'MyTrait.php (in context of class Repro\B):10:Ignore with identifier method.notFound has no comment.' tmp/cold.txt + grep -F 'MyTrait.php (in context of class Repro\A):12:Ignoring all errors on a line is not allowed.' tmp/cold.txt + grep -F 'MyTrait.php (in context of class Repro\B):12:Ignoring all errors on a line is not allowed.' tmp/cold.txt + diff -u tmp/cold.txt tmp/warm.txt + cp src/A.php tmp/A.php + grep -v 'use MyTrait;' tmp/A.php > src/A.php + ../../bin/phpstan analyse --no-progress --error-format raw -vvv > tmp/partial.txt 2> tmp/partial-log.txt || test "$?" -eq 1 + grep -F 'Result cache restored. 1 file will be reanalysed.' tmp/partial-log.txt + ! grep -F 'in context of class Repro\A' tmp/partial.txt + grep -F 'MyTrait.php (in context of class Repro\B):10:Ignore with identifier method.notFound has no comment.' tmp/partial.txt + cp tmp/A.php src/A.php - script: | cd e2e/result-cache-traits ../../bin/phpstan analyse diff --git a/e2e/result-cache-trait-ignore/phpstan.neon b/e2e/result-cache-trait-ignore/phpstan.neon new file mode 100644 index 00000000000..c6867eeee8d --- /dev/null +++ b/e2e/result-cache-trait-ignore/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + level: 5 + paths: + - src + tmpDir: tmp + reportIgnoresWithoutComments: true diff --git a/e2e/result-cache-trait-ignore/src/A.php b/e2e/result-cache-trait-ignore/src/A.php new file mode 100644 index 00000000000..c87537d1c23 --- /dev/null +++ b/e2e/result-cache-trait-ignore/src/A.php @@ -0,0 +1,8 @@ +undefinedMethod(); + // @phpstan-ignore-next-line + $this->anotherUndefinedMethod(); + } +} diff --git a/e2e/result-cache-trait-ignore/tmp/.gitignore b/e2e/result-cache-trait-ignore/tmp/.gitignore new file mode 100644 index 00000000000..125e34294bf --- /dev/null +++ b/e2e/result-cache-trait-ignore/tmp/.gitignore @@ -0,0 +1,2 @@ +* +!.* diff --git a/src/Analyser/FileAnalyser.php b/src/Analyser/FileAnalyser.php index ad0dad34c3f..19518c87f0f 100644 --- a/src/Analyser/FileAnalyser.php +++ b/src/Analyser/FileAnalyser.php @@ -155,8 +155,8 @@ public function analyseFile( $ignoredFile, $line, false, - $ignoredFile, - null, + $file, + $nodeCallback->getTraitFilePaths()[$ignoredFile] ?? null, 'Use @phpstan-ignore with an identifier and a comment (in parentheses).', ))->withIdentifier('ignore.allLineErrors'); unset($linesToIgnore[$ignoredFile][$line]); @@ -174,8 +174,8 @@ public function analyseFile( $ignoredFile, $line, false, - $ignoredFile, - null, + $file, + $nodeCallback->getTraitFilePaths()[$ignoredFile] ?? null, 'Explain why this ignore is necessary in parentheses after the identifier.', ))->withIdentifier('ignore.noComment'); unset($linesToIgnore[$ignoredFile][$line]); diff --git a/src/Analyser/FileAnalyserCallback.php b/src/Analyser/FileAnalyserCallback.php index 4dac2fa0ccf..202dcd9e495 100644 --- a/src/Analyser/FileAnalyserCallback.php +++ b/src/Analyser/FileAnalyserCallback.php @@ -48,6 +48,9 @@ final class FileAnalyserCallback /** @var LinesToIgnore */ private array $unmatchedLineIgnores; + /** @var array */ + private array $traitFilePaths = []; + /** * The registries answer per node class and cache that themselves; this keeps the answer for the * classes met in this file, so that the walk does not ask them again for every node of a class. @@ -119,6 +122,7 @@ public function __invoke(Node $node, Scope $scope): void $traitFileName = $node->getTraitReflection()->getFileName(); if ($traitFileName !== null) { + $this->traitFilePaths[$fileDescription] = $traitFileName; $this->processedFiles[] = $traitFileName; } } @@ -314,6 +318,14 @@ public function getUnmatchedLineIgnores(): array return $this->unmatchedLineIgnores; } + /** + * @return array + */ + public function getTraitFilePaths(): array + { + return $this->traitFilePaths; + } + /** * @return list */ From 9f88e537690c6e86c5cfb75bb74fbceefa29ba46 Mon Sep 17 00:00:00 2001 From: Alfred Bez <1001186+alfredbez@users.noreply.github.com> Date: Sun, 1 Nov 2026 10:32:09 +0000 Subject: [PATCH 2/3] Preserve trait errors in editor mode --- .github/workflows/e2e-tests.yml | 6 +++++- src/Analyser/Error.php | 9 +++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 0bc5db148e4..a983f821aa8 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -966,11 +966,15 @@ jobs: grep -F 'MyTrait.php (in context of class Repro\B):12:Ignoring all errors on a line is not allowed.' tmp/cold.txt diff -u tmp/cold.txt tmp/warm.txt cp src/A.php tmp/A.php + ../../bin/phpstan analyse --no-progress --error-format raw --tmp-file tmp/A.php --instead-of src/A.php > tmp/editor.txt 2> tmp/editor-log.txt || test "$?" -eq 1 + grep -F 'MyTrait.php (in context of class Repro\A):10:Ignore with identifier method.notFound has no comment.' tmp/editor.txt + grep -F 'MyTrait.php (in context of class Repro\A):12:Ignoring all errors on a line is not allowed.' tmp/editor.txt grep -v 'use MyTrait;' tmp/A.php > src/A.php ../../bin/phpstan analyse --no-progress --error-format raw -vvv > tmp/partial.txt 2> tmp/partial-log.txt || test "$?" -eq 1 grep -F 'Result cache restored. 1 file will be reanalysed.' tmp/partial-log.txt - ! grep -F 'in context of class Repro\A' tmp/partial.txt + if grep -F 'in context of class Repro\A' tmp/partial.txt; then exit 1; fi grep -F 'MyTrait.php (in context of class Repro\B):10:Ignore with identifier method.notFound has no comment.' tmp/partial.txt + grep -F 'MyTrait.php (in context of class Repro\B):12:Ignoring all errors on a line is not allowed.' tmp/partial.txt cp tmp/A.php src/A.php - script: | cd e2e/result-cache-traits diff --git a/src/Analyser/Error.php b/src/Analyser/Error.php index b7ba807a7fe..7a7befa78df 100644 --- a/src/Analyser/Error.php +++ b/src/Analyser/Error.php @@ -74,23 +74,20 @@ public function getFilePath(): string public function changeFilePath(string $newFilePath): self { - if ($this->traitFilePath !== null) { - throw new ShouldNotHappenException('Errors in traits not yet supported'); - } - return new self( $this->message, - $newFilePath, + $this->traitFilePath !== null ? $this->file : $newFilePath, $this->line, $this->canBeIgnored, $newFilePath, - null, + $this->traitFilePath, $this->tip, $this->nodeLine, $this->nodeType, $this->identifier, $this->metadata, $this->renameFixedFile($this->getFilePath(), $newFilePath), + $this->traitContexts, ); } From bf8114682681b126939db6340a11ddf4f77105f5 Mon Sep 17 00:00:00 2001 From: Alfred Bez <1001186+alfredbez@users.noreply.github.com> Date: Sun, 1 Nov 2026 10:32:09 +0000 Subject: [PATCH 3/3] test: use bashunit assertions in trait ignore result cache e2e test --- .github/workflows/e2e-tests.yml | 46 +++++++++++-------- .../removeTrait.patch | 8 ++++ 2 files changed, 35 insertions(+), 19 deletions(-) create mode 100644 e2e/result-cache-trait-ignore/removeTrait.patch diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index a983f821aa8..92eb36478ca 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -956,26 +956,34 @@ jobs: - script: | cd e2e/result-cache-trait-ignore rm -rf tmp/cache tmp/resultCache.php - ../../bin/phpstan analyse --no-progress --error-format raw -vvv > tmp/cold.txt 2> tmp/cold-log.txt || test "$?" -eq 1 - ../../bin/phpstan analyse --no-progress --error-format raw -vvv > tmp/warm.txt 2> tmp/warm-log.txt || test "$?" -eq 1 - grep -F 'Result cache is saved.' tmp/cold-log.txt - grep -F 'Result cache restored. 0 files will be reanalysed.' tmp/warm-log.txt - grep -F 'MyTrait.php (in context of class Repro\A):10:Ignore with identifier method.notFound has no comment.' tmp/cold.txt - grep -F 'MyTrait.php (in context of class Repro\B):10:Ignore with identifier method.notFound has no comment.' tmp/cold.txt - grep -F 'MyTrait.php (in context of class Repro\A):12:Ignoring all errors on a line is not allowed.' tmp/cold.txt - grep -F 'MyTrait.php (in context of class Repro\B):12:Ignoring all errors on a line is not allowed.' tmp/cold.txt - diff -u tmp/cold.txt tmp/warm.txt + OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vv --error-format raw") + echo "$OUTPUT" + ../bashunit assert contains 'Result cache is saved.' "$OUTPUT" + ../bashunit assert contains 'MyTrait.php (in context of class Repro\A):10:Ignore with identifier method.notFound has no comment.' "$OUTPUT" + ../bashunit assert contains 'MyTrait.php (in context of class Repro\B):10:Ignore with identifier method.notFound has no comment.' "$OUTPUT" + ../bashunit assert contains 'MyTrait.php (in context of class Repro\A):12:Ignoring all errors on a line is not allowed.' "$OUTPUT" + ../bashunit assert contains 'MyTrait.php (in context of class Repro\B):12:Ignoring all errors on a line is not allowed.' "$OUTPUT" + OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vv --error-format raw") + echo "$OUTPUT" + ../bashunit assert contains 'Result cache restored. 0 files will be reanalysed.' "$OUTPUT" + ../bashunit assert contains 'MyTrait.php (in context of class Repro\A):10:Ignore with identifier method.notFound has no comment.' "$OUTPUT" + ../bashunit assert contains 'MyTrait.php (in context of class Repro\B):10:Ignore with identifier method.notFound has no comment.' "$OUTPUT" + ../bashunit assert contains 'MyTrait.php (in context of class Repro\A):12:Ignoring all errors on a line is not allowed.' "$OUTPUT" + ../bashunit assert contains 'MyTrait.php (in context of class Repro\B):12:Ignoring all errors on a line is not allowed.' "$OUTPUT" cp src/A.php tmp/A.php - ../../bin/phpstan analyse --no-progress --error-format raw --tmp-file tmp/A.php --instead-of src/A.php > tmp/editor.txt 2> tmp/editor-log.txt || test "$?" -eq 1 - grep -F 'MyTrait.php (in context of class Repro\A):10:Ignore with identifier method.notFound has no comment.' tmp/editor.txt - grep -F 'MyTrait.php (in context of class Repro\A):12:Ignoring all errors on a line is not allowed.' tmp/editor.txt - grep -v 'use MyTrait;' tmp/A.php > src/A.php - ../../bin/phpstan analyse --no-progress --error-format raw -vvv > tmp/partial.txt 2> tmp/partial-log.txt || test "$?" -eq 1 - grep -F 'Result cache restored. 1 file will be reanalysed.' tmp/partial-log.txt - if grep -F 'in context of class Repro\A' tmp/partial.txt; then exit 1; fi - grep -F 'MyTrait.php (in context of class Repro\B):10:Ignore with identifier method.notFound has no comment.' tmp/partial.txt - grep -F 'MyTrait.php (in context of class Repro\B):12:Ignoring all errors on a line is not allowed.' tmp/partial.txt - cp tmp/A.php src/A.php + OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vv --error-format raw --tmp-file tmp/A.php --instead-of src/A.php") + echo "$OUTPUT" + ../bashunit assert contains 'MyTrait.php (in context of class Repro\A):10:Ignore with identifier method.notFound has no comment.' "$OUTPUT" + ../bashunit assert contains 'MyTrait.php (in context of class Repro\A):12:Ignoring all errors on a line is not allowed.' "$OUTPUT" + rm -f tmp/A.php + patch -b src/A.php < removeTrait.patch + OUTPUT=$(../bashunit assert exit_code "1" "../../bin/phpstan analyse -vv --error-format raw") + echo "$OUTPUT" + ../bashunit assert contains 'Result cache restored. 1 file will be reanalysed.' "$OUTPUT" + ../bashunit assert not_contains 'in context of class Repro\A' "$OUTPUT" + ../bashunit assert contains 'MyTrait.php (in context of class Repro\B):10:Ignore with identifier method.notFound has no comment.' "$OUTPUT" + ../bashunit assert contains 'MyTrait.php (in context of class Repro\B):12:Ignoring all errors on a line is not allowed.' "$OUTPUT" + mv src/A.php.orig src/A.php - script: | cd e2e/result-cache-traits ../../bin/phpstan analyse diff --git a/e2e/result-cache-trait-ignore/removeTrait.patch b/e2e/result-cache-trait-ignore/removeTrait.patch new file mode 100644 index 00000000000..2dadb32e619 --- /dev/null +++ b/e2e/result-cache-trait-ignore/removeTrait.patch @@ -0,0 +1,8 @@ +--- src/A.php ++++ src/A.php +@@ -4,5 +4,4 @@ + + class A + { +- use MyTrait; + }