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

emilles pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 2c9b454923ab825f819bd3d381bf21e9c6a405ae
Author: Eric Milles <[email protected]>
AuthorDate: Tue Jul 15 12:20:11 2025 -0500

    GROOVY-11644: check for `namedVariant` constructor duplication
---
 .../transform/RecordTypeASTTransformation.java     |  5 ++++
 .../TupleConstructorASTTransformation.java         | 30 +++++++++++++++++++++-
 .../org/codehaus/groovy/classgen/RecordTest.groovy | 22 ++++++++++++++++
 3 files changed, 56 insertions(+), 1 deletion(-)

diff --git 
a/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java 
b/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java
index 2a344463ce..0b80bc8e6e 100644
--- 
a/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java
+++ 
b/src/main/java/org/codehaus/groovy/transform/RecordTypeASTTransformation.java
@@ -39,6 +39,7 @@ import org.codehaus.groovy.ast.PropertyNode;
 import org.codehaus.groovy.ast.RecordComponentNode;
 import org.codehaus.groovy.ast.expr.ArgumentListExpression;
 import org.codehaus.groovy.ast.expr.ClassExpression;
+import org.codehaus.groovy.ast.expr.ConstantExpression;
 import org.codehaus.groovy.ast.expr.Expression;
 import org.codehaus.groovy.ast.expr.MapEntryExpression;
 import org.codehaus.groovy.ast.expr.PropertyExpression;
@@ -283,6 +284,10 @@ public class RecordTypeASTTransformation extends 
AbstractASTTransformation imple
 
         if (hasAnnotation(cNode, TupleConstructorASTTransformation.MY_TYPE)) {
             AnnotationNode tupleConstructor = 
cNode.getAnnotations(TupleConstructorASTTransformation.MY_TYPE).get(0);
+            if (pList.size() == 1 && pList.get(0).getType().equals(MAP_TYPE)
+                    && memberHasValue(tupleConstructor, "namedVariant", 
Boolean.TRUE)) {
+                tupleConstructor.setMember("namedVariant", 
ConstantExpression.PRIM_FALSE); // GROOVY-11644: conflicts
+            }
             if (unsupportedTupleAttribute(tupleConstructor, "excludes")) 
return;
             if (unsupportedTupleAttribute(tupleConstructor, "includes")) 
return;
             if (unsupportedTupleAttribute(tupleConstructor, 
"includeProperties")) return;
diff --git 
a/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
 
b/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
index e3b226167e..25a81f9fb0 100644
--- 
a/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
+++ 
b/src/main/java/org/codehaus/groovy/transform/TupleConstructorASTTransformation.java
@@ -60,10 +60,12 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Stream;
 
 import static groovy.transform.DefaultsMode.OFF;
 import static groovy.transform.DefaultsMode.ON;
 import static java.util.stream.Collectors.joining;
+import static java.util.stream.IntStream.rangeClosed;
 import static 
org.apache.groovy.ast.tools.ClassNodeUtils.addGeneratedConstructor;
 import static 
org.apache.groovy.ast.tools.ClassNodeUtils.hasExplicitConstructor;
 import static org.apache.groovy.ast.tools.ConstructorNodeUtils.checkPropNamesS;
@@ -284,8 +286,15 @@ public class TupleConstructorASTTransformation extends 
AbstractASTTransformation
                 tupleCtor.putNodeMetaData("_SKIPPABLE_ANNOTATIONS", 
Boolean.TRUE);
             }
             if (namedVariant) {
+                var pType = ClassHelper.MAP_TYPE.getPlainNodeReference();
+                // GROOVY-11644: check if named-param constructor would clash
+                if (cNode.getDeclaredConstructor(params(param(pType, "map"))) 
!= null
+                        || variants(signature).anyMatch(types -> types.length 
== 1 && types[0].equals(pType))) {
+                    xform.addError(String.format("%s(namedVariant=true) 
specifies duplicate constructor: %s(%s)",
+                            xform.getAnnotationName(), 
cNode.getNameWithoutPackage(), ClassNodeUtils.formatTypeName(pType)), 
anno.getLineNumber() > 0 ? anno : cNode);
+                }
                 BlockStatement inner = new BlockStatement();
-                Parameter mapParam = 
param(ClassHelper.MAP_TYPE.getPlainNodeReference(), NAMED_ARGS);
+                Parameter mapParam = param(pType, NAMED_ARGS);
                 ArgumentListExpression args = new ArgumentListExpression();
                 List<String> propNames = new ArrayList<>();
                 Map<Parameter, Expression> seen = new HashMap<>();
@@ -398,4 +407,23 @@ public class TupleConstructorASTTransformation extends 
AbstractASTTransformation
         }
         return null;
     }
+
+    private static Stream<ClassNode[]> variants(final Parameter[] parameters) {
+        int n = (int) 
Stream.of(parameters).filter(Parameter::hasInitialExpression).count();
+
+        return rangeClosed(0, n).mapToObj(i -> {
+            // drop parameters with value from right to left
+            ClassNode[] signature = new ClassNode[parameters.length - i];
+            int j = 1, index = 0;
+            for (Parameter parameter : parameters) {
+                if (j > n - i && parameter.hasInitialExpression()) {
+                    // skip parameter with default argument
+                } else {
+                    signature[index++] = parameter.getType();
+                }
+                if (parameter.hasInitialExpression()) j += 1;
+            }
+            return signature;
+        });
+    }
 }
diff --git a/src/test/groovy/org/codehaus/groovy/classgen/RecordTest.groovy 
b/src/test/groovy/org/codehaus/groovy/classgen/RecordTest.groovy
index 13a7dfb48e..eaa7ca49e2 100644
--- a/src/test/groovy/org/codehaus/groovy/classgen/RecordTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/classgen/RecordTest.groovy
@@ -591,6 +591,28 @@ final class RecordTest {
                 new Person(name:'John Doe')
             }
         '''
+
+        // GROOVY-11644
+        assertScript shell, '''
+            record Person(@NamedParam(value='name', required=true) Map map) {
+                Person {
+                    assert map.name instanceof String
+                        && map.name.trim().size() > 1
+                }
+                String getName() {
+                    return map.get('name')
+                }
+            }
+
+            def person = new Person(name:'Frank Grimes', hair:'brown')
+            assert person.name == 'Frank Grimes'
+            shouldFail {
+                new Person([:])
+            }
+            shouldFail {
+                new Person()
+            }
+        '''
     }
 
     @Test

Reply via email to