Skip to content

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
apache:masterfrom
codeconsole:stc-private-field-attribute
Draft

codeconsole wants to merge 11 commits into
apache:masterfrom
codeconsole:stc-private-field-attribute

Conversation

@codeconsole

@codeconsole codeconsole commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

In a class nested in Outer, the type checker accepted sub.@f, where f is a private field of Outer and sub is typed as a subclass of Outer. Dynamic Groovy and @TypeChecked threw MissingFieldException, because the meta-class of a subclass offers no private field of its superclass, and javac rejects it ("f has private access in Outer"); @CompileStatic read the field. With f declared in a nested class Box and Sub 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().@f and a receiver typed as a deeper subclass.

@TypeChecked
class Outer {
    private String f = 'outer'
    static class Sub extends Outer {
        String read(Sub sub) { sub.@f }  // threw MissingFieldException; now "Cannot access field: f of class: Outer"
    }
}

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.@f inside Sub is rejected too once GROOVY-12514 (#3049) makes Sub.this compile 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).@f reads it. 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. 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 only this.@name and super.@name reach such a field.

Tests

FieldsAndPropertiesSTCTest (also run by FieldsAndPropertiesStaticCompileTest): 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 the groovy, gls, bugs, org.codehaus.groovy.classgen, transform, runtime, reflection, vmplugin, control and ast packages and the stub generator tests pass, apart from bugs.Groovy12363, which fails on master too when it runs after bugs.Groovy12142 in the same JVM (#3096 fixes it). In that run the new test also failed, on a Sub.this.@f case that needs GROOVY-12514; with that case removed, FieldsAndPropertiesSTCTest and FieldsAndPropertiesStaticCompileTest pass. groovy-contracts, groovy-typecheckers, groovy-ginq, groovy-macro-library, groovy-json, groovy-xml and groovy-templates pass their tests, and every subproject compiles its main and test sources. The Checkstyle and CodeNarc gates pass.

…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-commenter

codecov-commenter commented Oct 26, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.10526% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 73.6870%. Comparing base (3869743) to head (a2c19c8).

Files with missing lines Patch % Lines
...sc/StaticTypesMethodReferenceExpressionWriter.java 81.8182% 0 Missing and 2 partials ⚠️
...roovy/transform/stc/StaticTypeCheckingVisitor.java 95.5556% 0 Missing and 2 partials ⚠️
...groovy/classgen/asm/sc/StaticInvocationWriter.java 93.7500% 0 Missing and 1 partial ⚠️
...roovy/transform/stc/StaticTypeCheckingSupport.java 75.0000% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@                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     
Files with missing lines Coverage Δ
...groovy/classgen/asm/sc/StaticInvocationWriter.java 75.8621% <93.7500%> (+0.4196%) ⬆️
...roovy/transform/stc/StaticTypeCheckingSupport.java 81.8104% <75.0000%> (-0.0389%) ⬇️
...sc/StaticTypesMethodReferenceExpressionWriter.java 89.6175% <81.8182%> (+0.5419%) ⬆️
...roovy/transform/stc/StaticTypeCheckingVisitor.java 87.5670% <95.5556%> (+0.1007%) ⬆️

... and 12 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…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
@testlens-app

testlens-app Bot commented Oct 30, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: a2c19c8
▶️ Tests: 124447 executed
⚪️ Checks: 26/26 completed


Learn more about TestLens at testlens.app/docs.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants