GROOVY-12166: STC: scope statement-level instanceof narrowing to the … - #2796
Open
paulk-asert wants to merge 1 commit into
Open
GROOVY-12166: STC: scope statement-level instanceof narrowing to the …#2796paulk-asert wants to merge 1 commit into
paulk-asert wants to merge 1 commit into
Conversation
…member being visited Temporary type information from instanceof is keyed by the target variable — for a field access, the shared FieldNode. if/loops/ternaries and expression statements bracket their own frames, but a narrowing recorded at statement level (an explicit `return field instanceof Sub`, or an `assert field instanceof Sub`, whose propagation to subsequent statements is intentional) lands in the enclosing frame, which was the frame pushed for the whole class visit. Keyed by the shared FieldNode, it then applied to every member visited afterwards: the checker stamped the narrowed type on unrelated field reads and the static compiler emitted a spurious checkcast, throwing ClassCastException at runtime whenever the field held a different subtype. Members now push their own frame (methods and constructors, property and field initializers, object initializer blocks), so statement-level narrowing still flows within a member but can never survive into another; narrowing has no meaning past the member boundary. Intra-member flow typing (assert, if-branch) is unchanged. The checker-side leak predates Groovy 5 but was masked by codegen: StaticTypesTypeChooser resolved types from the declared target first until GROOVY-11375 (5.0.0-alpha-9) gave the expression-stamped inferred type priority, exposing the stale narrowing as a checkcast. That change is correct; the fix belongs here.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2796 +/- ##
==================================================
+ Coverage 70.1064% 70.1211% +0.0147%
- Complexity 35774 35796 +22
==================================================
Files 1561 1562 +1
Lines 132373 132408 +35
Branches 24332 24345 +13
==================================================
+ Hits 92802 92846 +44
+ Misses 31170 31160 -10
- Partials 8401 8402 +1
🚀 New features to boost your workflow:
|
✅ All tests passed ✅🏷️ Commit: 4a6c175 Learn more about TestLens at testlens.app. |
blackdrag
approved these changes
Aug 17, 2026
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.
…member being visited
Temporary type information from instanceof is keyed by the target variable — for a field access, the shared FieldNode. if/loops/ternaries and expression statements bracket their own frames, but a narrowing recorded at statement level (an explicit
return field instanceof Sub, or anassert field instanceof Sub, whose propagation to subsequent statements is intentional) lands in the enclosing frame, which was the frame pushed for the whole class visit. Keyed by the shared FieldNode, it then applied to every member visited afterwards: the checker stamped the narrowed type on unrelated field reads and the static compiler emitted a spurious checkcast, throwing ClassCastException at runtime whenever the field held a different subtype.Members now push their own frame (methods and constructors, property and field initializers, object initializer blocks), so statement-level narrowing still flows within a member but can never survive into another; narrowing has no meaning past the member boundary. Intra-member flow typing (assert, if-branch) is unchanged.
The checker-side leak predates Groovy 5 but was masked by codegen: StaticTypesTypeChooser resolved types from the declared target first until GROOVY-11375 (5.0.0-alpha-9) gave the expression-stamped inferred type priority, exposing the stale narrowing as a checkcast. That change is correct; the fix belongs here.