| /* |
| * 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 org.codehaus.groovy.transform; |
| |
| import groovy.transform.NamedDelegate; |
| import groovy.transform.NamedParam; |
| import groovy.transform.NamedVariant; |
| import org.apache.groovy.ast.tools.AnnotatedNodeUtils; |
| import org.codehaus.groovy.ast.ASTNode; |
| import org.codehaus.groovy.ast.AnnotationNode; |
| import org.codehaus.groovy.ast.ClassNode; |
| import org.codehaus.groovy.ast.ConstructorNode; |
| import org.codehaus.groovy.ast.MethodNode; |
| import org.codehaus.groovy.ast.Parameter; |
| import org.codehaus.groovy.ast.PropertyNode; |
| import org.codehaus.groovy.ast.expr.ArgumentListExpression; |
| import org.codehaus.groovy.ast.expr.CastExpression; |
| import org.codehaus.groovy.ast.expr.Expression; |
| import org.codehaus.groovy.ast.expr.MethodCallExpression; |
| import org.codehaus.groovy.ast.expr.VariableExpression; |
| import org.codehaus.groovy.ast.stmt.AssertStatement; |
| import org.codehaus.groovy.ast.stmt.BlockStatement; |
| import org.codehaus.groovy.ast.stmt.ForStatement; |
| import org.codehaus.groovy.ast.tools.GenericsUtils; |
| import org.codehaus.groovy.control.CompilePhase; |
| import org.codehaus.groovy.control.SourceUnit; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import static org.apache.groovy.ast.tools.ClassNodeUtils.addGeneratedConstructor; |
| import static org.apache.groovy.ast.tools.ClassNodeUtils.addGeneratedMethod; |
| import static org.apache.groovy.ast.tools.ClassNodeUtils.isInnerClass; |
| import static org.apache.groovy.ast.tools.VisibilityUtils.getVisibility; |
| import static org.codehaus.groovy.ast.ClassHelper.MAP_TYPE; |
| import static org.codehaus.groovy.ast.ClassHelper.STRING_TYPE; |
| import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveType; |
| import static org.codehaus.groovy.ast.ClassHelper.make; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.args; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.asX; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.boolX; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.callX; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.castX; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.classX; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.constX; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.defaultValueX; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.elvisX; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.getAllProperties; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.ifS; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.isNullX; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.list2args; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.param; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.plusX; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.propX; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.ternaryX; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.throwS; |
| import static org.codehaus.groovy.ast.tools.GeneralUtils.varX; |
| |
| @GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS) |
| public class NamedVariantASTTransformation extends AbstractASTTransformation { |
| |
| private static final ClassNode NAMED_PARAM_TYPE = make(NamedParam.class); |
| private static final ClassNode NAMED_VARIANT_TYPE = make(NamedVariant.class); |
| private static final ClassNode NAMED_DELEGATE_TYPE = make(NamedDelegate.class); |
| private static final ClassNode ILLEGAL_ARGUMENT_TYPE = make(IllegalArgumentException.class); |
| |
| private static final String NAMED_VARIANT = "@" + NAMED_VARIANT_TYPE.getNameWithoutPackage(); |
| |
| @Override |
| public void visit(final ASTNode[] nodes, final SourceUnit source) { |
| init(nodes, source); |
| MethodNode mNode = (MethodNode) nodes[1]; |
| AnnotationNode anno = (AnnotationNode) nodes[0]; |
| if (!NAMED_VARIANT_TYPE.equals(anno.getClassNode())) return; |
| |
| Parameter[] mNodeParams = mNode.getParameters(); |
| if (mNodeParams.length == 0) { |
| addError("Error during " + NAMED_VARIANT + " processing. No-args method not supported.", mNode); |
| return; |
| } |
| |
| boolean autoDelegate = memberHasValue(anno, "autoDelegate", Boolean.TRUE); |
| boolean coerce = memberHasValue(anno, "coerce", Boolean.TRUE); |
| Parameter mapParam = param(GenericsUtils.nonGeneric(MAP_TYPE), "namedArgs"); |
| List<Parameter> genParams = new ArrayList<>(); |
| genParams.add(mapParam); |
| ClassNode cNode = mNode.getDeclaringClass(); |
| BlockStatement inner = new BlockStatement(); |
| ArgumentListExpression args = new ArgumentListExpression(); |
| List<String> propNames = new ArrayList<>(); |
| |
| // first pass, just check for annotations of interest |
| boolean annoFound = false; |
| for (Parameter mNodeParam : mNodeParams) { |
| if (AnnotatedNodeUtils.hasAnnotation(mNodeParam, NAMED_PARAM_TYPE) || AnnotatedNodeUtils.hasAnnotation(mNodeParam, NAMED_DELEGATE_TYPE)) { |
| annoFound = true; |
| break; |
| } |
| } |
| |
| if (!annoFound && autoDelegate) { // the first param is the delegate |
| processDelegateParam(mNode, mapParam, args, propNames, mNodeParams[0], coerce); |
| } else { |
| Map<Parameter, Expression> seen = new HashMap<>(); |
| for (Parameter mNodeParam : mNodeParams) { |
| if (!annoFound) { |
| if (!processImplicitNamedParam(this, mNode, mapParam, inner, args, propNames, mNodeParam, coerce, seen)) return; |
| } else if (AnnotatedNodeUtils.hasAnnotation(mNodeParam, NAMED_PARAM_TYPE)) { |
| if (!processExplicitNamedParam(mNode, mapParam, inner, args, propNames, mNodeParam, coerce, seen)) return; |
| } else if (AnnotatedNodeUtils.hasAnnotation(mNodeParam, NAMED_DELEGATE_TYPE)) { |
| if (!processDelegateParam(mNode, mapParam, args, propNames, mNodeParam, coerce)) return; |
| } else { |
| Expression arg = varX(mNodeParam); |
| Expression argOrDefault = mNodeParam.hasInitialExpression() ? elvisX(arg, mNodeParam.getDefaultValue()) : arg; |
| args.addExpression(asType(argOrDefault, mNodeParam.getType(), coerce)); |
| if (hasDuplicates(this, mNode, propNames, mNodeParam.getName())) return; |
| genParams.add(mNodeParam); |
| } |
| } |
| } |
| createMapVariant(this, mNode, anno, mapParam, genParams, cNode, inner, args, propNames); |
| } |
| |
| // for backwards compatibility |
| static boolean processImplicitNamedParam(final ErrorCollecting xform, final MethodNode mNode, final Parameter mapParam, final BlockStatement inner, final ArgumentListExpression args, final List<String> propNames, final Parameter fromParam, final boolean coerce) { |
| return processImplicitNamedParam(xform, mNode, mapParam, inner, args, propNames, fromParam, coerce, null); |
| } |
| |
| static boolean processImplicitNamedParam(final ErrorCollecting xform, final MethodNode mNode, final Parameter mapParam, final BlockStatement inner, final ArgumentListExpression args, final List<String> propNames, final Parameter fromParam, final boolean coerce, Map<Parameter, Expression> seen) { |
| String name = fromParam.getName(); |
| ClassNode type = fromParam.getType(); |
| boolean required = !fromParam.hasInitialExpression(); |
| if (hasDuplicates(xform, mNode, propNames, name)) return false; |
| |
| AnnotationNode namedParam = new AnnotationNode(NAMED_PARAM_TYPE); |
| namedParam.addMember("value", constX(name)); |
| namedParam.addMember("type", classX(type)); |
| namedParam.addMember("required", constX(required, true)); |
| mapParam.addAnnotation(namedParam); |
| |
| if (required) { |
| inner.addStatement(new AssertStatement(boolX(containsKey(mapParam, name)), |
| plusX(constX("Missing required named argument '" + name + "'. Keys found: "), callX(varX(mapParam), "keySet")))); |
| } |
| Expression defValue = getDefaultValue(fromParam.getInitialExpression(), seen); |
| Expression initExpr = namedParamValue(mapParam, name, type, coerce, defValue); |
| if (seen != null) { |
| seen.put(fromParam, initExpr); |
| } |
| args.addExpression(initExpr); |
| return true; |
| } |
| |
| private boolean processExplicitNamedParam(final MethodNode mNode, final Parameter mapParam, final BlockStatement inner, final ArgumentListExpression args, final List<String> propNames, final Parameter fromParam, final boolean coerce, Map<Parameter, Expression> seen) { |
| AnnotationNode namedParam = fromParam.getAnnotations(NAMED_PARAM_TYPE).get(0); |
| |
| String name = getMemberStringValue(namedParam, "value"); |
| if (name == null) { |
| name = fromParam.getName(); |
| namedParam.addMember("value", constX(name)); |
| } |
| if (hasDuplicates(this, mNode, propNames, name)) return false; |
| |
| ClassNode type = getMemberClassValue(namedParam, "type"); |
| if (type == null) { |
| type = fromParam.getType(); |
| namedParam.addMember("type", classX(type)); |
| } else { |
| // TODO: Check attribute type is assignable to declared param type? |
| } |
| |
| boolean required = memberHasValue(namedParam, "required", Boolean.TRUE); |
| if (required) { |
| if (fromParam.hasInitialExpression()) { |
| addError("Error during " + NAMED_VARIANT + " processing. A required parameter can't have an initial value.", fromParam); |
| return false; |
| } |
| inner.addStatement(new AssertStatement(boolX(containsKey(mapParam, name)), |
| plusX(constX("Missing required named argument '" + name + "'. Keys found: "), callX(varX(mapParam), "keySet")))); |
| } |
| Expression defValue = getDefaultValue(fromParam.getInitialExpression(), seen); |
| Expression initExpr = namedParamValue(mapParam, name, type, coerce, defValue); |
| seen.put(fromParam, initExpr); |
| args.addExpression(initExpr); |
| mapParam.addAnnotation(namedParam); |
| fromParam.getAnnotations().remove(namedParam); |
| return true; |
| } |
| |
| private boolean processDelegateParam(final MethodNode mNode, final Parameter mapParam, final ArgumentListExpression args, final List<String> propNames, final Parameter fromParam, final boolean coerce) { |
| if (isInnerClass(fromParam.getType()) && mNode.isStatic()) { |
| addError("Error during " + NAMED_VARIANT + " processing. Delegate type '" + fromParam.getType().getNameWithoutPackage() + "' is an inner class which is not supported.", mNode); |
| return false; |
| } |
| |
| Set<String> names = new HashSet<>(); |
| List<PropertyNode> props = getAllProperties(names, fromParam.getType(), true, false, false, true, false, true); |
| for (String name : names) { |
| if (hasDuplicates(this, mNode, propNames, name)) return false; |
| } |
| for (PropertyNode prop : props) { |
| // create annotation @NamedParam(value='name', type=PropertyType) |
| AnnotationNode namedParam = new AnnotationNode(NAMED_PARAM_TYPE); |
| namedParam.addMember("value", constX(prop.getName())); |
| namedParam.addMember("type", classX(prop.getType())); |
| mapParam.addAnnotation(namedParam); |
| } |
| |
| Expression[] subMapArgs = names.stream().map(name -> constX(name)).toArray(Expression[]::new); |
| Expression delegateMap = callX(varX(mapParam), "subMap", args(subMapArgs)); |
| args.addExpression(castX(fromParam.getType(), delegateMap)); |
| return true; |
| } |
| |
| private static boolean hasDuplicates(final ErrorCollecting xform, final MethodNode mNode, final List<String> propNames, final String next) { |
| if (propNames.contains(next)) { |
| xform.addError("Error during " + NAMED_VARIANT + " processing. Duplicate property '" + next + "' found.", mNode); |
| return true; |
| } |
| propNames.add(next); |
| return false; |
| } |
| |
| static void createMapVariant(final ErrorCollecting xform, final MethodNode mNode, final AnnotationNode anno, final Parameter mapParam, final List<Parameter> genParams, final ClassNode cNode, final BlockStatement inner, final ArgumentListExpression args, final List<String> propNames) { |
| Parameter namedArgKey = param(STRING_TYPE, "namedArgKey"); |
| if (!(mNode instanceof ConstructorNode)) { |
| inner.getStatements().add(0, ifS(isNullX(varX(mapParam)), |
| throwS(ctorX(ILLEGAL_ARGUMENT_TYPE, args(constX("Named parameter map cannot be null")))))); |
| } |
| inner.addStatement( |
| new ForStatement( |
| namedArgKey, |
| callX(varX(mapParam), "keySet"), |
| new AssertStatement(boolX(callX(list2args(propNames), "contains", varX(namedArgKey))), plusX(constX("Unrecognized namedArgKey: "), varX(namedArgKey))) |
| )); |
| |
| Parameter[] genParamsArray = genParams.toArray(Parameter.EMPTY_ARRAY); |
| // TODO account for default params giving multiple signatures |
| if (cNode.hasMethod(mNode.getName(), genParamsArray)) { |
| xform.addError("Error during " + NAMED_VARIANT + " processing. Class " + cNode.getNameWithoutPackage() + |
| " already has a named-arg " + (mNode instanceof ConstructorNode ? "constructor" : "method") + |
| " of type " + genParams, mNode); |
| return; |
| } |
| |
| BlockStatement body = new BlockStatement(); |
| int modifiers = getVisibility(anno, mNode, mNode.getClass(), mNode.getModifiers()); |
| if (mNode instanceof ConstructorNode) { |
| body.addStatement(stmt(ctorX(ClassNode.THIS, args))); |
| body.addStatement(inner); |
| addGeneratedConstructor(cNode, |
| modifiers, |
| genParamsArray, |
| mNode.getExceptions(), |
| body |
| ); |
| } else { |
| body.addStatement(inner); |
| body.addStatement(stmt(callThisX(mNode.getName(), args))); |
| addGeneratedMethod(cNode, |
| mNode.getName(), |
| modifiers, |
| mNode.getReturnType(), |
| genParamsArray, |
| mNode.getExceptions(), |
| body |
| ); |
| } |
| } |
| |
| private static Expression getDefaultValue(final Expression defaultValue, final Map<Parameter, Expression> seen) { |
| if (defaultValue != null && seen != null) { // GROOVY-10561, GROOVY-10889 |
| Expression v = defaultValue; |
| while (v instanceof CastExpression) { |
| v = ((CastExpression) v).getExpression(); |
| } |
| if (v instanceof VariableExpression) { // maybe it's a reference to a previous parameter |
| return seen.getOrDefault(((VariableExpression) v).getAccessedVariable(), defaultValue); |
| } |
| } |
| return defaultValue; |
| } |
| |
| private static Expression namedParamValue(final Parameter mapParam, final String name, final ClassNode type, final boolean coerce, Expression defaultValue) { |
| Expression value = propX(varX(mapParam), name); // TODO: "map.get(name)" |
| if (defaultValue == null && isPrimitiveType(type)) { |
| defaultValue = defaultValueX(type); |
| } |
| if (defaultValue != null) { |
| value = ternaryX(containsKey(mapParam, name), value, defaultValue); |
| } |
| return asType(value, type, coerce); |
| } |
| |
| private static Expression containsKey(final Parameter mapParam, final String name) { |
| MethodCallExpression call = callX(varX(mapParam), "containsKey", constX(name)); |
| call.setImplicitThis(false); // required for use before super ctor call |
| call.setMethodTarget(MAP_TYPE.getMethods("containsKey").get(0)); |
| return call; |
| } |
| |
| private static Expression asType(final Expression value, final ClassNode type, final boolean coerce) { |
| return coerce ? asX(type, value) : /*castX(*/value/*)*/; |
| } |
| } |