Reanalyse trait-consuming files after a body-only trait change even when the trait shares its file with another symbol - #6033
Merged
ondrejmirtes merged 1 commit intoJul 10, 2026
Conversation
…hen the trait shares its file with another symbol - In ResultCacheManager, the body-only-change branch (exported nodes unchanged) only re-queued the trait-consuming files when *every* exported node in the file was a trait; it did `continue 2` and skipped them as soon as it saw a non-trait node (e.g. a class declared next to the trait). - Replace the "all exported nodes are traits" check with "the file contains at least one trait": when a trait's method body changes but its signature stays the same, the classes using that trait are now always reanalysed, regardless of any non-trait symbol living in the same file. - A body-only change of the non-trait symbols in that file still correctly does not force their dependents to be reanalysed. - Add e2e/result-cache-trait-alongside-class covering the reported scenario: a trait alongside a class in one file, a warm cache, then a trait-body change that introduces an error in a consuming class.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When a trait's method body changed (but its signature stayed the same) and the reused result cache predated that change, PHPStan failed to reanalyse the classes consuming the trait — as long as the trait's file also declared another symbol (a class, enum, etc.) next to it. As a result, real errors introduced by the change were silently missed on cached runs (and
--generate-baselineregenerated an empty baseline /ignore.unmatchedfired on CI), while a fresh cache reported them correctly.The fix makes the trait body-change cache invalidation trigger whenever the changed file contains a trait, instead of only when the file contains nothing but traits.
Changes
src/Analyser/ResultCache/ResultCacheManager.php: in the branch handling a file whose hash changed but whose exported nodes are identical (a body-only change), stop bailing out viacontinue 2on the first non-trait exported node. Instead, detect whether the file contains at least oneExportedTraitNode; if it does, re-queue that file'susedTraitDependentFiles(the classes using the trait). Files with no trait node keep the previous behaviour (no dependent reanalysis).e2e/result-cache-trait-alongside-class/: new e2e reproduction — a trait plus a marker class in one file, a class using the trait, a warm cache, then a patch that changes only the trait method body to return the wrong type. The consuming class must report thereturn.typeerror on the cached run..github/workflows/e2e-tests.yml: run the new e2e scenario.Root cause
Trait method bodies are analysed in the context of every consuming class, so a body-only change to a trait must reanalyse those consumers — regular dependency tracking only covers signature-level changes.
ResultCacheManagerhandled this, but guarded it with an "all exported nodes in this file are traits" loop that didcontinue 2(skipping the trait-consumer re-queueing entirely) the moment it encountered any non-trait node. A trait declared next to a class in the same file therefore never had its consumers reanalysed on a body-only change. The guard is now "the file contains a trait", which is the condition that actually determines whether trait consumers need reanalysing; a body-only change to the co-located non-trait symbols still (correctly) does not force their dependents to be reanalysed.Analogous cases probed
exportedNodesChangedreturnstrue/false): verified these already reanalyse consumers through the regulardependentFilesdependency graph, so no parallel fix was needed there.Test
e2e/result-cache-trait-alongside-classreproduces the exact reported conditions (trait alongside a class in one file, warm cache predating the change, unchanged consuming classes) and asserts the consuming class reports the introduced error on the cached run. I confirmed it fails before the fix (exit 0, no error) and passes after (exit 1,return.typeerror reported).Fixes phpstan/phpstan#14943