This is an automated email from the ASF dual-hosted git repository. emilles pushed a commit to branch GROOVY-11644 in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 8660ceb881d07937b7c222db4976f0e27803db78 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
