You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by em...@apache.org on 2026/04/24 00:01:34 UTC

(groovy) branch GROOVY_5_0_X updated: GROOVY-11956: STC: read-only property error for map constructor elements

This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch GROOVY_5_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/GROOVY_5_0_X by this push:
     new 8b8687aedf GROOVY-11956: STC: read-only property error for map constructor elements
8b8687aedf is described below

commit 8b8687aedffb3a6c6c79ebd510fc79262724390b
Author: Eric Milles <er...@thomsonreuters.com>
AuthorDate: Thu Apr 23 18:00:46 2026 -0500

    GROOVY-11956: STC: read-only property error for map constructor elements
    
    5_0_X backport
---
 .../asm/sc/StaticPropertyAccessHelper.java         | 42 +++++++++++++---------
 .../transformers/ConstructorCallTransformer.java   | 16 +++++----
 .../transform/stc/StaticTypeCheckingVisitor.java   | 10 +++---
 .../transform/stc/ConstructorsSTCTest.groovy       | 36 ++++++++++++-------
 .../groovy/groovy/transform/stc/Pojo11956.java     | 25 +++++++++++++
 .../asm/sc/StaticCompileConstructorsTest.groovy    | 10 ++++++
 6 files changed, 100 insertions(+), 39 deletions(-)

diff --git a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticPropertyAccessHelper.java b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticPropertyAccessHelper.java
index 6cffa34b55..841c6a8294 100644
--- a/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticPropertyAccessHelper.java
+++ b/src/main/java/org/codehaus/groovy/classgen/asm/sc/StaticPropertyAccessHelper.java
@@ -20,6 +20,7 @@ package org.codehaus.groovy.classgen.asm.sc;
 
 import org.codehaus.groovy.ast.GroovyCodeVisitor;
 import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.ast.expr.ArgumentListExpression;
 import org.codehaus.groovy.ast.expr.Expression;
 import org.codehaus.groovy.ast.expr.ExpressionTransformer;
 import org.codehaus.groovy.ast.expr.MethodCallExpression;
@@ -45,27 +46,35 @@ public abstract class StaticPropertyAccessHelper {
             final boolean spreadSafe,
             final boolean returnValue,
             final Expression sourceExpression) {
+        MethodCallExpression call;
+        Expression result;
         if (returnValue) {
-            TemporaryVariableExpression tmp = new TemporaryVariableExpression(valueExpression);
-            PoppingMethodCallExpression call = new PoppingMethodCallExpression(receiver, setterMethod, tmp);
-            call.setSafe(safe);
-            call.setSpreadSafe(spreadSafe);
-            call.setImplicitThis(implicitThis);
-            call.setSourcePosition(sourceExpression);
-            PoppingListOfExpressionsExpression list = new PoppingListOfExpressionsExpression(tmp, call);
-            list.setSourcePosition(sourceExpression);
-            return list;
+            var temp = new TemporaryVariableExpression(valueExpression);
+            temp.setSourcePosition(valueExpression);
+
+            var pmce = new PoppingMethodCallExpression(receiver, setterMethod, temp);
+            result = new PoppingListOfExpressionsExpression(temp, pmce);
+            call = pmce;
         } else {
-            MethodCallExpression call = new MethodCallExpression(receiver, setterMethod.getName(), valueExpression);
-            call.setSafe(safe);
-            call.setSpreadSafe(spreadSafe);
-            call.setImplicitThis(implicitThis);
+            var args = new ArgumentListExpression(valueExpression);
+            args.setSourcePosition(valueExpression);
+
+            call = new MethodCallExpression(receiver, setterMethod.getName(), args);
             call.setMethodTarget(setterMethod);
-            call.setSourcePosition(sourceExpression);
-            return call;
+            result = call;
         }
+        call.setSafe(safe);
+        call.setSpreadSafe(spreadSafe);
+        call.setImplicitThis(implicitThis);
+        call.setSourcePosition(sourceExpression);
+        call.setLastLineNumber(valueExpression.getLastLineNumber());
+        call.setLastColumnNumber(valueExpression.getLastColumnNumber());
+
+        return result;
     }
 
+    //--------------------------------------------------------------------------
+
     private static class PoppingListOfExpressionsExpression extends ListOfExpressionsExpression {
 
         private final TemporaryVariableExpression tmp;
@@ -97,7 +106,8 @@ public abstract class StaticPropertyAccessHelper {
         private final TemporaryVariableExpression tmp;
 
         public PoppingMethodCallExpression(final Expression receiver, final MethodNode setterMethod, final TemporaryVariableExpression tmp) {
-            super(receiver, setterMethod.getName(), tmp);
+            super(receiver, setterMethod.getName(), new ArgumentListExpression(tmp));
+            getArguments().setSourcePosition(tmp);
             setMethodTarget(setterMethod);
             this.tmp = tmp;
         }
diff --git a/src/main/java/org/codehaus/groovy/transform/sc/transformers/ConstructorCallTransformer.java b/src/main/java/org/codehaus/groovy/transform/sc/transformers/ConstructorCallTransformer.java
index 6998195031..dcb4a455dc 100644
--- a/src/main/java/org/codehaus/groovy/transform/sc/transformers/ConstructorCallTransformer.java
+++ b/src/main/java/org/codehaus/groovy/transform/sc/transformers/ConstructorCallTransformer.java
@@ -106,8 +106,8 @@ public class ConstructorCallTransformer {
             this.originalCall = originalCall;
             this.copyNodeMetaData(originalCall);
             this.setSourcePosition(originalCall);
-            Expression originalArgs = originalCall.getArguments();
-            this.innerClassCall = (2 == ((TupleExpression) originalArgs).getExpressions().size());
+            var originalArgs = (TupleExpression) originalCall.getArguments();
+            this.innerClassCall = (2 == originalArgs.getExpressions().size());
         }
 
         @Override
@@ -145,20 +145,22 @@ public class ConstructorCallTransformer {
             String signature = "()V";
             if (innerClassCall && ctorType.getOuterClass() != null) {
                 acg.visitVariableExpression(varX("this")); // GROOVY-11122
-                Parameter[] params = {new Parameter(ctorType.getOuterClass(), "$p$")};
+                Parameter[] params = {new Parameter(ctorType.getOuterClass(),"$p$")};
                 signature = BytecodeHelper.getMethodDescriptor(ClassHelper.VOID_TYPE, params);
             }
             mv.visitMethodInsn(INVOKESPECIAL, ctorTypeName, "<init>", signature, false);
             mv.visitVarInsn(ASTORE, tmpObj);
 
+            Expression objExpression = bytecodeX(ctorType, v -> v.visitVarInsn(ALOAD, tmpObj));
+
             // process property initializers
             for (MapEntryExpression entryExpression : map.getMapEntryExpressions()) {
                 Expression keyExpression = entryExpression.getKeyExpression();
+                Expression varExpression = propX(objExpression, keyExpression);
                 Expression valExpression = entryExpression.getValueExpression();
-                Expression varExpression = propX(
-                        bytecodeX(ctorType, v -> v.visitVarInsn(ALOAD, tmpObj)),
-                        keyExpression
-                );
+
+                objExpression.setSourcePosition(keyExpression); // GROOVY-11956
+
                 varExpression.putNodeMetaData(DIRECT_METHOD_CALL_TARGET,
                         keyExpression.getNodeMetaData(DIRECT_METHOD_CALL_TARGET));
 
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 9cc82464c9..8750e5d249 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingVisitor.java
@@ -1533,11 +1533,13 @@ out:    if ((samParameterTypes.length == 1 && isOrImplements(samParameterTypes[0
                 String propName = keyExpression.getText();
                 Set<ClassNode> propertyTypes = new HashSet<>();
                 Expression valueExpression = entryExpression.getValueExpression();
+                PropertyExpression dummyExpression = propX(varX("", receiverType), propName);
+                dummyExpression.setSourcePosition(keyExpression);
                 typeCheckingContext.pushEnclosingBinaryExpression(assignX(keyExpression, valueExpression, entryExpression));
-                if (!existsProperty(propX(varX("_", receiverType), propName), false, new PropertyLookup(receiverType, propertyTypes::add))) {
+                if (!existsProperty(dummyExpression, /*read*/false, new PropertyLookup(receiverType, propertyTypes::add))) {
                     typeCheckingContext.popEnclosingBinaryExpression();
-                    addStaticTypeError("No such property: " + propName + " for class: " + prettyPrintTypeName(receiverType), receiver);
-                } else {
+                    addStaticTypeError("No such property: " + propName + " for class: " + prettyPrintTypeName(receiverType), keyExpression);
+                } else if (receiverType.isRecord() || !addedReadOnlyPropertyError(dummyExpression)) { // GROOVY-11956
                     SetterInfo setterInfo = removeSetterInfo(keyExpression);
                     if (setterInfo != null) { // GROOVY-11451: select setter
                         ensureValidSetter(entryExpression, keyExpression, valueExpression, setterInfo);
@@ -1545,7 +1547,7 @@ out:    if ((samParameterTypes.length == 1 && isOrImplements(samParameterTypes[0
                     ClassNode valueType = getType(valueExpression);
                     BinaryExpression kv = typeCheckingContext.popEnclosingBinaryExpression();
                     if (propertyTypes.stream().noneMatch(targetType -> checkCompatibleAssignmentTypes(targetType, getResultType(targetType, ASSIGN, valueType, kv), valueExpression))) {
-                        ClassNode targetType = propertyTypes.size() == 1 ? propertyTypes.iterator().next() : new UnionTypeClassNode(propertyTypes.toArray(ClassNode.EMPTY_ARRAY));
+                        ClassNode targetType = propertyTypes.isEmpty() ? VOID_TYPE : (propertyTypes.size() == 1 ? propertyTypes.iterator().next() : new UnionTypeClassNode(propertyTypes.toArray(ClassNode[]::new)));
                         if (!extension.handleIncompatibleAssignment(targetType, valueType, entryExpression)) {
                             addAssignmentError(targetType, valueType, entryExpression);
                         }
diff --git a/src/test/groovy/groovy/transform/stc/ConstructorsSTCTest.groovy b/src/test/groovy/groovy/transform/stc/ConstructorsSTCTest.groovy
index 80c0bc5b53..55e1ee31f7 100644
--- a/src/test/groovy/groovy/transform/stc/ConstructorsSTCTest.groovy
+++ b/src/test/groovy/groovy/transform/stc/ConstructorsSTCTest.groovy
@@ -432,7 +432,7 @@ class ConstructorsSTCTest extends StaticTypeCheckingTestCase {
         '''
     }
 
-    void testMapConstructorShouldFail() {
+    void testMapConstructorIncompatibleType() {
         shouldFailWithMessages '''
             class Foo {
                 ByteArrayOutputStream out
@@ -459,19 +459,12 @@ class ConstructorsSTCTest extends StaticTypeCheckingTestCase {
     void testConstructorsWithVarargsAndArrayParameters() {
         assertScript '''
             class MultipleConstructors {
-
-                public MultipleConstructors(String s, short[] arr) {}
-                public MultipleConstructors(String s, int... arr) {}
-                public MultipleConstructors(short[] arr) {}
-            }
-
-            class Clz {
-                  void run() {
-                        new MultipleConstructors('d',1)
-                }
+                MultipleConstructors(String s, short[] arr) {}
+                MultipleConstructors(String s, int...  arr) {}
+                MultipleConstructors(          short[] arr) {}
             }
 
-            new Clz().run()
+            new MultipleConstructors('d', 1)
         '''
     }
 
@@ -485,6 +478,7 @@ class ConstructorsSTCTest extends StaticTypeCheckingTestCase {
                 }
                 String toString() { var }
             }
+
             def b = new MyBean(foo: 'Test')
             assert b.toString() == 'Test'
         '''
@@ -523,6 +517,24 @@ class ConstructorsSTCTest extends StaticTypeCheckingTestCase {
         '''
     }
 
+    // GROOVY-11956
+    void testMapStyleConstructorWithReadOnlyProperty() {
+        shouldFailWithMessages ''' // line 1
+            class C {
+                String getFoo() {}
+            }
+            new C(foo: 'bar')
+        ''',
+        'Cannot set read-only property: foo','@ line 5, column 19'
+    }
+
+    // GROOVY-11956
+    void testMapStyleConstructorWithInaccessibleSetter() {
+        assertScript """import ${Pojo11956.canonicalName}
+            new Pojo11956(foo: 'bar')
+        """
+    }
+
     // GROOVY-10787
     void testMapStyleConstructorWithParameterizedProperty() {
         assertScript '''
diff --git a/src/test/groovy/groovy/transform/stc/Pojo11956.java b/src/test/groovy/groovy/transform/stc/Pojo11956.java
new file mode 100644
index 0000000000..1d6de18b5d
--- /dev/null
+++ b/src/test/groovy/groovy/transform/stc/Pojo11956.java
@@ -0,0 +1,25 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements. See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership. The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License. You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied. See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package groovy.transform.stc;
+
+public class Pojo11956 {
+    private String foo = "bar";
+    public  String getFoo() { return this.foo; }
+    protected void setFoo(String foo) { this.foo = foo; }
+}
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 c9d8050965..297975f7c0 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,6 +19,7 @@
 package org.codehaus.groovy.classgen.asm.sc
 
 import groovy.transform.stc.ConstructorsSTCTest
+import groovy.transform.stc.Pojo11956
 
 /**
  * Unit tests for static compilation: constructors.
@@ -107,4 +108,13 @@ class StaticCompileConstructorsTest extends ConstructorsSTCTest implements Stati
         assert  asserts.contains('INVOKEVIRTUAL C.setP (Ljava/util/regex/Pattern;)V')
         assert !asserts.contains('INVOKESTATIC org/codehaus/groovy/runtime/ScriptBytecodeAdapter.setGroovyObjectProperty')
     }
+
+    // GROOVY-11956
+    @Override
+    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"
+    }
 }