You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/08/23 21:28:30 UTC
(groovy) branch master updated: GROOVY-12290: Private field access from another class compiles under @TypeChecked but fails under @CompileStatic
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new 0f217d25d3 GROOVY-12290: Private field access from another class compiles under @TypeChecked but fails under @CompileStatic
0f217d25d3 is described below
commit 0f217d25d3ae3caeadd88b404699ac384e0c37e1
Author: Paul King <pa...@asert.com.au>
AuthorDate: Sun Aug 23 16:39:56 2026 +1000
GROOVY-12290: Private field access from another class compiles under @TypeChecked but fails under @CompileStatic
---
.../transform/stc/StaticTypeCheckingVisitor.java | 60 +++++++++++++++++++---
src/test/groovy/bugs/Groovy7165.groovy | 3 +-
.../transform/stc/ConstructorsSTCTest.groovy | 9 ++--
.../stc/FieldsAndPropertiesSTCTest.groovy | 48 +++++++++++++++++
.../asm/sc/StaticCompileConstructorsTest.groovy | 12 ++---
5 files changed, 113 insertions(+), 19 deletions(-)
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
index f942a365f2..5f2e7861e8 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
@@ -49,6 +49,7 @@ import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.MultipleAssignmentMetadata;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.PropertyNode;
+import org.codehaus.groovy.ast.RecordComponentNode;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.expr.AnnotationConstantExpression;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
@@ -653,6 +654,20 @@ public class StaticTypeCheckingVisitor extends ClassCodeVisitorSupport {
}
}
+ /**
+ * Determines if the two classes belong to the same nest, so a private-access
+ * bridge can be generated between them. Identity comparison is deliberate:
+ * nest-mates only arise within a single source unit, where the outer-class
+ * chain shares {@code ClassNode} instances; name-based {@code equals} could
+ * conflate same-named classes from different origins. Accessibility decisions
+ * ({@link #isFieldAccessible}) must use the same predicate as bridge marking
+ * ({@code checkOrMarkPrivateAccess}), or an access could be admitted for
+ * which no bridge is generated.
+ */
+ private static boolean inSameNest(final ClassNode a, final ClassNode b) {
+ return getNestHost(a) == getNestHost(b);
+ }
+
/**
* Checks for private field access from closure or nestmate.
*/
@@ -660,7 +675,7 @@ public class StaticTypeCheckingVisitor extends ClassCodeVisitorSupport {
if (fn != null && fn.isPrivate() && !fn.isSynthetic()) {
ClassNode declaringClass = fn.getDeclaringClass();
ClassNode enclosingClass = typeCheckingContext.getEnclosingClassNode();
- if (declaringClass == enclosingClass ? typeCheckingContext.getEnclosingClosure() != null : getNestHost(declaringClass) == getNestHost(enclosingClass)) {
+ if (declaringClass == enclosingClass ? typeCheckingContext.getEnclosingClosure() != null : inSameNest(declaringClass, enclosingClass)) {
source.putNodeMetaData(lhsOfAssignment ? PV_FIELDS_MUTATION : PV_FIELDS_ACCESS, fn);
}
}
@@ -673,7 +688,7 @@ public class StaticTypeCheckingVisitor extends ClassCodeVisitorSupport {
ClassNode declaringClass = mn.getDeclaringClass();
ClassNode enclosingClass = typeCheckingContext.getEnclosingClassNode();
if (declaringClass != enclosingClass || typeCheckingContext.getEnclosingClosure() != null) {
- if (mn.isPrivate() && getNestHost(declaringClass) == getNestHost(enclosingClass)) {
+ if (mn.isPrivate() && inSameNest(declaringClass, enclosingClass)) {
if (source instanceof MethodReferenceExpression // GROOVY-11301: access bridge for lambda
&& Float.parseFloat(getSourceUnit().getConfiguration().getTargetBytecode()) < 15)
declaringClass.getNodeMetaData(PV_METHODS_ACCESS, k -> new LinkedHashSet<MethodNode>()).add(mn);
@@ -2241,6 +2256,16 @@ out: if ((samParameterTypes.length == 1 && isOrImplements(samParameterTypes[0
getter = getGetterMethod(current, getterName, checkUp);
getter = allowStaticAccessToMember(getter, staticOnly);
}
+ if (getter == null && current.isRecord()) {
+ // GROOVY-12225: a precompiled record's component accessor is named for the
+ // component (e.g. "x()"), so there is no get/is getter and no PropertyNode
+ for (RecordComponentNode rcn : current.getRecordComponents()) {
+ if (rcn.getName().equals(propertyName)) {
+ getter = allowStaticAccessToMember(current.getDeclaredMethod(propertyName, Parameter.EMPTY_ARRAY), staticOnly);
+ break;
+ }
+ }
+ }
if (getter != null && ((publicOnly && (!getter.isPublic() || propertyName.equals("class") || propertyName.equals("empty")))
// GROOVY-11319:
|| !hasAccessToMember(typeCheckingContext.getEnclosingClassNode(), getter.getDeclaringClass(), getter.getModifiers()))) {
@@ -2284,7 +2309,8 @@ out: if ((samParameterTypes.length == 1 && isOrImplements(samParameterTypes[0
}
pexp.removeNodeMetaData(READONLY_PROPERTY);
return true;
- } else if (getter != null && (field == null || field.isFinal()) && setters.isEmpty()) {
+ } else if (getter != null && setters.isEmpty() && (field == null || field.isFinal()
+ || !isFieldAccessible(field, receiverType, pexp, receiver.getData()))) { // GROOVY-12290: inaccessible field cannot make the property writable
pexp.putNodeMetaData(READONLY_PROPERTY, Boolean.TRUE); // GROOVY-9127
}
}
@@ -2526,10 +2552,32 @@ out: if ((samParameterTypes.length == 1 && isOrImplements(samParameterTypes[0
return method instanceof ExtensionMethodNode ? ((ExtensionMethodNode) method).isStaticExtension() : method.isStatic();
}
- private boolean storeField(final FieldNode field, final PropertyExpression expressionToStoreOn, final ClassNode receiver, final ClassCodeVisitorSupport visitor, final String delegationData, final boolean lhsOfAssignment) {
- boolean superField = isSuperExpression(expressionToStoreOn.getObjectExpression());
- boolean accessible = (!superField && receiver.equals(field.getDeclaringClass()) && !field.getDeclaringClass().isAbstract()) // GROOVY-7300, GROOVY-11358
+ /**
+ * Determines if the field can back the given property (or attribute)
+ * expression: accessible by Java rules, or admitted by the receiver-type
+ * leniency (GROOVY-7300, GROOVY-11358) that models dynamic Groovy's
+ * permissive private access. Since GROOVY-12290 that leniency no longer
+ * admits plain property syntax to a private field of a foreign nest — a
+ * direct field access that static compilation cannot honour (no access
+ * bridge exists). The deliberate dynamic escape hatches remain: attribute
+ * access (.@), and closure bodies (GROOVY-9195) — including delegate-resolved
+ * access — whose property dispatch stays dynamic-capable under static compilation.
+ */
+ private boolean isFieldAccessible(final FieldNode field, final ClassNode receiver, final PropertyExpression expression, final String delegationData) {
+ boolean superField = isSuperExpression(expression.getObjectExpression());
+ boolean exactReceiver = (!superField && receiver.equals(field.getDeclaringClass()) && !field.getDeclaringClass().isAbstract()); // GROOVY-7300, GROOVY-11358
+ if (exactReceiver && field.isPrivate() && delegationData == null
+ && typeCheckingContext.getEnclosingClosure() == null
+ && !(expression instanceof AttributeExpression)
+ && !inSameNest(field.getDeclaringClass(), typeCheckingContext.getEnclosingClassNode())) {
+ exactReceiver = false; // GROOVY-12290
+ }
+ return exactReceiver
|| hasAccessToMember(typeCheckingContext.getEnclosingClassNode(), field.getDeclaringClass(), field.getModifiers());
+ }
+
+ private boolean storeField(final FieldNode field, final PropertyExpression expressionToStoreOn, final ClassNode receiver, final ClassCodeVisitorSupport visitor, final String delegationData, final boolean lhsOfAssignment) {
+ boolean accessible = isFieldAccessible(field, receiver, expressionToStoreOn, delegationData);
if (!accessible) {
if (expressionToStoreOn instanceof AttributeExpression) {
addStaticTypeError("Cannot access field: " + field.getName() + " of class: " + prettyPrintTypeName(field.getDeclaringClass()), expressionToStoreOn.getProperty());
diff --git a/src/test/groovy/bugs/Groovy7165.groovy b/src/test/groovy/bugs/Groovy7165.groovy
index 2b50224f35..5a49b46a4a 100644
--- a/src/test/groovy/bugs/Groovy7165.groovy
+++ b/src/test/groovy/bugs/Groovy7165.groovy
@@ -90,6 +90,7 @@ final class Groovy7165 {
}
assert false : 'compilation should fail'
'''
- assert err.message =~ /Access to A#CONST is forbidden/
+ // GROOVY-12290: reported by the type checker, no longer late by the static writer
+ assert err.message =~ /No such property: CONST for Class or static property for class: A/
}
}
diff --git a/src/test/groovy/groovy/transform/stc/ConstructorsSTCTest.groovy b/src/test/groovy/groovy/transform/stc/ConstructorsSTCTest.groovy
index 1cbef1626c..06a6747b66 100644
--- a/src/test/groovy/groovy/transform/stc/ConstructorsSTCTest.groovy
+++ b/src/test/groovy/groovy/transform/stc/ConstructorsSTCTest.groovy
@@ -568,12 +568,15 @@ import static groovy.test.GroovyAssert.shouldFail
'Cannot set read-only property: foo','@ line 5, column 19'
}
- // GROOVY-11956
+ // GROOVY-11956, GROOVY-12290: the protected setter is out of scope and the
+ // private field is in a foreign nest, so the property is read-only in both
+ // type-checked and statically-compiled code
@Test
void testMapStyleConstructorWithInaccessibleSetter() {
- assertScript """import ${Pojo11956.canonicalName}
+ shouldFailWithMessages """import ${Pojo11956.canonicalName}
new Pojo11956(foo: 'bar')
- """
+ """,
+ 'Cannot set read-only property: foo'
}
// GROOVY-10787
diff --git a/src/test/groovy/groovy/transform/stc/FieldsAndPropertiesSTCTest.groovy b/src/test/groovy/groovy/transform/stc/FieldsAndPropertiesSTCTest.groovy
index 753cc335f5..26dcbf13e6 100644
--- a/src/test/groovy/groovy/transform/stc/FieldsAndPropertiesSTCTest.groovy
+++ b/src/test/groovy/groovy/transform/stc/FieldsAndPropertiesSTCTest.groovy
@@ -1887,6 +1887,54 @@ class FieldsAndPropertiesSTCTest extends StaticTypeCheckingTestCase {
'Cannot access field: f of class: A', 'No such property: f for class: A'
}
+ // GROOVY-12290
+ @Test
+ void testPrivateFieldOfForeignNestViaPropertySyntax() {
+ shouldFailWithMessages '''
+ class P { private static final Map M = [a: 1] }
+ def m() { P.M }
+ ''',
+ 'No such property: M for Class or static property for class: P'
+
+ shouldFailWithMessages '''
+ class P { private final Map f = [a: 1] }
+ def m() { new P().f }
+ ''',
+ 'No such property: f for class: P'
+
+ shouldFailWithMessages '''
+ class P { private static Map W = [:] }
+ def m() { P.W = [b: 2] }
+ ''',
+ 'No such property: W for Class or static property for class: P'
+ }
+
+ // GROOVY-12290: the deliberate escape hatches into dynamic private access
+ // keep working: attribute syntax and delegate-resolved access in closures
+ @Test
+ void testPrivateFieldOfForeignNestViaAttributeOrDelegate() {
+ assertScript '''
+ class P { private final Map f = [a: 1] }
+ assert new P().@f == [a: 1]
+ '''
+ assertScript '''
+ class P { private final Map f = [a: 1] }
+ assert new P().with { f } == [a: 1]
+ '''
+ }
+
+ // GROOVY-12290: nest-mates go through access bridges, not the receiver-type leniency
+ @Test
+ void testPrivateFieldOfNestMateViaPropertySyntax() {
+ assertScript '''
+ class Outer {
+ private static final Map M = [a: 1]
+ static class Inner { def m() { M } }
+ }
+ assert new Outer.Inner().m() == [a: 1]
+ '''
+ }
+
// GROOVY-5737
@Test
void testGeneratedFieldAccessInClosure() {
diff --git a/src/test/groovy/org/codehaus/groovy/classgen/asm/sc/StaticCompileConstructorsTest.groovy b/src/test/groovy/org/codehaus/groovy/classgen/asm/sc/StaticCompileConstructorsTest.groovy
index 02756215ee..b937f9e7cc 100644
--- a/src/test/groovy/org/codehaus/groovy/classgen/asm/sc/StaticCompileConstructorsTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/classgen/asm/sc/StaticCompileConstructorsTest.groovy
@@ -19,7 +19,6 @@
package org.codehaus.groovy.classgen.asm.sc
import groovy.transform.stc.ConstructorsSTCTest
-import groovy.transform.stc.Pojo11956
import org.junit.jupiter.api.Test
/**
@@ -128,12 +127,7 @@ final class StaticCompileConstructorsTest extends ConstructorsSTCTest implements
assert !asserts.contains('INVOKESTATIC org/codehaus/groovy/runtime/ScriptBytecodeAdapter.setGroovyObjectProperty')
}
- // GROOVY-11956
- @Override @Test
- void testMapStyleConstructorWithInaccessibleSetter() {
- shouldFailWithMessages """import ${Pojo11956.canonicalName}
- new Pojo11956(foo: 'bar')
- """,
- "Cannot access method: setFoo(java.lang.String) of class: ${Pojo11956.canonicalName} @ line 2, column 27"
- }
+ // GROOVY-11956, GROOVY-12290: testMapStyleConstructorWithInaccessibleSetter is
+ // inherited: the read-only property error is now reported by the type checker,
+ // identically for @TypeChecked and @CompileStatic
}