Repository navigation
GROOVY-12523: STC: reject x.@name for a private field of a superclass when the type of x is a subclass - #3050
Draft
codeconsole wants to merge 11 commits into
Draft
codeconsole wants to merge 11 commits into
codeconsole wants to merge 11 commits into
Conversation
…a subclass receiver A private method is not inherited (JLS 8.2), but the type checker found it through a subclass whenever the caller could see the method: in the outer class of Box, new Sub().im(), where im() is private in Box and Sub extends Box; in a class nested in Box, the same call; and in a static subclass nested in Box, im() or this.im(). @TypeChecked compiled such a call and it then failed with MissingMethodException, because dynamic Groovy does not find a private method of a superclass either. @CompileStatic called it directly. javac rejects it. Method references and method pointers (Sub::im, new Sub()::im, Sub::sm, new Sub().&im) behaved the same way, and method pointers failed at run time even under @CompileStatic. findMethod now drops a private method that the receiver type only inherits, before it selects an overload, so these calls fail with "Cannot find matching method" and a public overload of the subclass is selected as javac selects it. Method references and pointers drop it too, in the type checker and in the method reference writer (which is where @CompileStatic reports an unresolved method reference). These calls keep compiling, because dynamic Groovy runs them: - calls in the declaring class itself, including its closures and lambdas: inside Box, new Sub().im() works in all three modes; - private static methods through a subclass (Sub.sm(), sm() in a subclass). These keep compiling because the receiver's type declares the method: a Box parameter or method result, a cast to Box, super.im(), and a variable declared as Box even when flow typing narrows it to Sub (Box b = new Sub(); b.im()). An enum constant body or an anonymous subclass nested in Box still finds the method on its enclosing class. Method references and pointers get no such exceptions, because dynamic Groovy fails on them even in the declaring class: Sub::im and Sub::sm are rejected inside Box as well. Under @CompileStatic this stops compiling: new Sub().im() and the method references above in the outer class of Box or in a class nested in Box, also through a def variable, a with { } delegate or an instanceof check on an Object; Sub::im, new Sub()::im and Sub::sm inside Box; and im(), this.im() and new S().im() in a static subclass S nested in Box, as in class Shape { private String helper() { 'helper' } static class Circle extends Shape { String area() { helper() } } } where javac reports that helper() cannot be referenced from a static context. With a public Sub.m(Object) and a private Box.m(String), new Sub().m('x') now selects Sub.m(Object), as javac, dynamic Groovy and @TypeChecked do.
…class of the same nest Under @TypeChecked and @CompileStatic, a private method or constructor of a nested class could be used only from its declaring class, from the classes it encloses and from its enclosing classes. Java allows any class nested in the same top-level class (JLS 6.6.1), and the JVM allows it between nestmates (JEP 181), which Groovy's minimum target of 17 always provides. Code generation already treats the nest as one unit (AsmClassGenerator.isMemberDirectlyAccessible), and the type checker already does for fields (GROOVY-12290). Only filterMethodsByVisibility still used the older rule, so a sibling, cousin or nephew class was rejected with "Cannot find matching method/constructor", and a method reference such as Box::sm passed @TypeChecked but failed in the @CompileStatic writer with "Failed to find class method". filterMethodsByVisibility now admits a private method or constructor when the caller and the declaring class have the same nest host, using the same predicate as code generation. The calls compile to direct INVOKESTATIC, INVOKEVIRTUAL and INVOKESPECIAL instructions, also from a closure, a lambda, an anonymous class or a trait of the nest. A class of another nest is still rejected. A private method of a superclass stays out of reach through a subclass from the new callers as well (GROOVY-12510); its tests now also cover a sibling of the declaring class. Under @CompileStatic this changes which overload some calls select, because the private candidate used to be filtered out. The new choice is the one javac, dynamic Groovy and @TypeChecked already make: private Box(String) over Box(Object) for new Box('x'), a private m(String) of the receiver's class over an inherited m(Object), a private method of a with { } delegate over an owner method of the same name, and private m(int) over m(Object) for m(1).
…me nest through property syntax Property syntax checked a getter or setter with hasAccessToMember, which knows only "the declaring class and the classes it encloses", so box.x, box.y = v and Box.z failed with "No such property" when getX, setY or getZ is private and the caller is a sibling nested class, or even the outer class of Box. Dynamic Groovy calls the private accessor in all of these places, and javac allows the equivalent method calls (JLS 6.6.1). isAccessibleAccessor now admits a private accessor from any class of its nest, but only through its own class: private members are not inherited (JLS 8.2), so new Sub().x still fails when getX is private in the superclass, and new Sub().f still fails for a private field. hasAccessToMember itself is unchanged; giving it a nest clause would also admit private fields through a subclass receiver. Under @CompileStatic, a nestmate that reads or writes a private field through property syntax now calls the private getter or setter of the same name when there is one, instead of the field. Dynamic Groovy and @TypeChecked already call the accessor there.
…r class of the same nest Since GROOVY-12310 (6.0.0), the type checker rejects a field whose qualifying type is out of reach, through hasAccessToClass. That check did not know about nests, so a public field or enum constant of a private nested type, such as A.Hidden.x or A.E.ONE, failed with "Cannot access field" when used from a sibling of A or even from the outer class. Groovy 5.0.8 compiles and runs the same code, and javac accepts it (JLS 6.6.1). Methods and constructors of the same private type were never affected. hasAccessToClass now treats every type of a nest as accessible from every other type of the same nest. A private nested type of another top-level class is still rejected, and so is the package-private nested type of another package that GROOVY-12310 is about.
…ymous class in a static context An anonymous class created in a static method, a static initializer, a static field initializer or a closure inside one of them has no enclosing instance. The type checker still looked up instance methods of the outer class for an unqualified call inside it, so @TypeChecked compiled pm() there and it then failed with MissingMethodException, and @CompileStatic generated code that failed with "VerifyError: Bad type on operand stack" (or a ClassCastException when a closure was involved). javac reports that a non-static method cannot be referenced from a static context. An instance field in the same place was already rejected. The type checker now treats such an anonymous class like a static nested class when it looks for a method or property of an outer class, so the call fails with "Cannot find matching method". The static context is read from the variable scope that VariableScopeVisitor gives the anonymous class, whose parent is the scope where the class is created. The explicit form, Box.this.pm(), failed with VerifyError in every mode, in such an anonymous class and also in a static nested class, which never has an enclosing instance. ResolveVisitor now reports "The usage of 'Class.this' and 'Class.super' is not allowed in '...', which has no enclosing instance of '...'" when the path from the current class to the named outer class crosses a static nested class or an anonymous class created in a static context.
…the outer class A class nested in Box that also extends Box does not inherit the private members of Box (JLS 8.2), so an unqualified im() or f in it refers to the enclosing Box instance, as if written Box.this.im(). javac, dynamic Groovy and @TypeChecked use the enclosing instance. @CompileStatic used the subclass instance instead: an inner class Sub extends Box, or an anonymous subclass created in an instance method of Box, called im() and read f on itself. The same happened in their closures and lambdas, for a private getter used as a property, and for this.im(). Separately, this.om() in a plain inner class, where om() is a method of the outer class, failed with GroovyCastException. GROOVY-12510 makes the type checker find such a private method on the enclosing class only. The call writer now also calls it on the enclosing instance when the subclass has one, and calls this.name() on the enclosing instance when the method belongs to an outer class. An enum constant body has no enclosing instance; it still calls the private methods of its enum on itself, as every mode does. For property syntax, the type checker now skips a private field or accessor that the receiver type only inherits, unless the code is in the declaring class, and finds the outer class's member as Outer.this.name. this.@name in such a subclass now reads and writes the subclass's own field, as in dynamic Groovy; @CompileStatic used the enclosing instance's field there. Without an enclosing instance (a static nested subclass, an enum constant body), the name now fails with "No such property", as javac reports a non-static field referenced from a static context and dynamic Groovy throws MissingPropertyException; for a field this replaces a VerifyError under @CompileStatic. Under @CompileStatic this stops compiling, where it used the subclass instance's member: this.f in an inner subclass of Box, where f is private in Box; new Sub().f and new Sub().x in a class nested in Box, where f and getX() are private in Box; and a private getter of the superclass used as a property in a static nested subclass or an enum constant body. @TypeChecked compiled these; at run time this.f read the enclosing instance's field and the others threw MissingPropertyException. A private field in a static nested subclass or an enum constant body failed at run time before in every mode, with VerifyError or MissingPropertyException.
…e when flow typing narrows the variable A private method of Box is found through a variable declared as Box even when flow typing narrows it to a subclass (Box b = new Sub(); b.im()), because the declared type declares the method. A private field or accessor of Box was not: b.f, b.f = v, b.f += v, b.@f and b.x (with a private getX()) failed with "No such property: f for class: Sub" or "Cannot access field", also inside a closure or lambda and when a Box parameter was narrowed by instanceof. Through a parameter declared as Box, which flow typing does not narrow, the same access compiled. javac accepts it, because the static type of b is Box. Property lookup now treats a private field or accessor declared by the variable's declared type like one of the receiver type itself, for the inherited-member rule and for the field access check. A receiver whose type is only the subclass, such as new Sub().f or a def variable, is still rejected, because a private member is not inherited. @TypeChecked accesses such a property dynamically, so it throws MissingPropertyException at run time when the variable holds a Sub, as it already did for the parameter.
…ween nested classes The "Inner class" section said only that an inner class can use the private members of its enclosing class. State that, as in Java, all classes nested in the same top-level class can use each other's private members, that private members are not inherited, so they cannot be used through a subclass receiver except in the declaring class, and that an unqualified name of such a member in a nested subclass refers to the enclosing instance.
… through a subclass The rule that a private field of the superclass is not found through a subclass, so that a name in a nested subclass refers to the enclosing instance, also dropped private static fields: Sub.sf, Sub.sf = v and new Sub().sf in a static class Sub nested in Outer, or Sub.sf in a sibling, failed with "No such property" under @TypeChecked and @CompileStatic, where 6.0.0 and dynamic Groovy run them. The rule now leaves static fields alone, as GROOVY-12510 leaves private static methods.
… when the type of x is a subclass A private field is not a member of a subclass (JLS 8.2), and the meta-class of a subclass offers no private field of its superclass, so sub.@f, where f is private in Outer and sub is typed as a subclass of Outer, throws MissingFieldException in dynamic Groovy and under @TypeChecked, in every version. The type checker accepted it in a class nested in Outer, through the rule that a nested class sees the private members of its outer class; with f in a nested class Box and Sub extending Box, the same access was already rejected. It now reports "Cannot access field: f of class: Outer", as javac reports that f has private access, also for a write, a compound assignment, a closure, a lambda, new Sub().@f and a receiver typed as a deeper subclass. this.@f and super.@f, which read the field directly in every mode, keep working, as do sub.@f in Outer itself and a receiver declared as Outer or cast to it. Private static fields are left alone, as private static methods are. Under @CompileStatic, which read the field directly, such code stops compiling; ((Outer) sub).@f reads it. Groovy 4.0.33 failed at run time in every mode.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #3050 +/- ##
==================================================
+ Coverage 73.6728% 73.6870% +0.0142%
- Complexity 42587 42668 +81
==================================================
Files 1678 1678
Lines 146913 146973 +60
Branches 27792 27819 +27
==================================================
+ Hits 108235 108300 +65
+ Misses 28526 28522 -4
+ Partials 10152 10151 -1
🚀 New features to boost your workflow:
|
codeconsole
marked this pull request as draft
October 26, 2026 16:03
…ttribute # Conflicts: # src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticInvocationWriter.java # src/main/java/org/codehaus/groovy/control/ResolveVisitor.java # src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java # src/test/groovy/gls/innerClass/InnerClassTest.groovy # src/test/groovy/groovy/transform/stc/AnonymousInnerClassSTCTest.groovy # src/test/groovy/groovy/transform/stc/FieldsAndPropertiesSTCTest.groovy
✅ All tests passed ✅🏷️ Commit: a2c19c8 Learn more about TestLens at testlens.app/docs. |
This branch has not been deployed
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.
In a class nested in
Outer, the type checker acceptedsub.@f, wherefis a private field ofOuterandsubis typed as a subclass ofOuter. Dynamic Groovy and@TypeCheckedthrewMissingFieldException, because the meta-class of a subclass offers no private field of its superclass, and javac rejects it ("f has private access in Outer");@CompileStaticread the field. Withfdeclared in a nested classBoxandSub extends Box, or with a top-level subclass, the same access was already rejected. Both modes now report "Cannot access field: f of class: Outer", also for writes, compound assignments, closures, lambdas,new Sub().@fand a receiver typed as a deeper subclass.This builds on GROOVY-12510, GROOVY-12511 and GROOVY-12513, which are not merged yet: the rule reuses the check for a private member reached through a subclass (GROOVY-12510, GROOVY-12511) and the exception for a variable declared with the field's class (GROOVY-12513). Until they are merged, this pull request stays a draft and also shows the commits it builds on; the one to review is "GROOVY-12523: STC: reject x.@name for a private field of a superclass when the type of x is a subclass".
Sub.this.@finsideSubis rejected too once GROOVY-12514 (#3049) makesSub.thiscompile there; whichever of the two is merged second adds that case to the tests.Behaviour change
Under
@CompileStatic, which read the field directly, such code stops compiling;((Outer) sub).@freads it.this.@fandsuper.@f, which read the field directly in every mode, keep working, as dosub.@finOuteritself and a receiver declared asOuter. Private static fields are left alone, as private static methods are. 5.0.8, 6.0.0 and master compile such code; 4.0.33 failed at run time in every mode. The object orientation guide now says that within a subclass onlythis.@nameandsuper.@namereach such a field.Tests
FieldsAndPropertiesSTCTest(also run byFieldsAndPropertiesStaticCompileTest): thirteen rejected forms and a script with the forms that keep working; the statically compiled test also reads the field through a cast. They fail without the change. 15,011 tests in thegroovy,gls,bugs,org.codehaus.groovy.classgen,transform,runtime,reflection,vmplugin,controlandastpackages and the stub generator tests pass, apart frombugs.Groovy12363, which fails on master too when it runs afterbugs.Groovy12142in the same JVM (#3096 fixes it). In that run the new test also failed, on aSub.this.@fcase that needs GROOVY-12514; with that case removed,FieldsAndPropertiesSTCTestandFieldsAndPropertiesStaticCompileTestpass.groovy-contracts,groovy-typecheckers,groovy-ginq,groovy-macro-library,groovy-json,groovy-xmlandgroovy-templatespass their tests, and every subproject compiles its main and test sources. The Checkstyle and CodeNarc gates pass.