blob: 6a15eb0035ae434c9e20eb62c413ba989310094a [file] [log] [blame]
/*
* 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/*)*/;
}
}