diff --git a/build.gradle b/build.gradle index 218f1376..b0a691c6 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,6 @@ import org.apache.tools.ant.filters.ReplaceTokens buildscript { repositories { mavenCentral() - jcenter() maven { url "https://plugins.gradle.org/m2/" } @@ -33,10 +32,6 @@ group 'com.blackbuild.klum.ast' description 'A transformation for creating convenient configuration model DSLs.' -ext { - commonVersion = '0.6.0' -} - license { mapping("java", "SLASHSTAR_STYLE") mapping("groovy", "SLASHSTAR_STYLE") diff --git a/klum-ast-common/build.gradle b/klum-ast-common/build.gradle new file mode 100644 index 00000000..46314d0c --- /dev/null +++ b/klum-ast-common/build.gradle @@ -0,0 +1,3 @@ +apply plugin: 'java-library' + +description "Common Classes (temp reinclusion from Klum-common)" diff --git a/klum-ast-common/src/main/java/com/blackbuild/klum/common/CommonAstHelper.java b/klum-ast-common/src/main/java/com/blackbuild/klum/common/CommonAstHelper.java new file mode 100644 index 00000000..6c3eafb0 --- /dev/null +++ b/klum-ast-common/src/main/java/com/blackbuild/klum/common/CommonAstHelper.java @@ -0,0 +1,413 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2015-2017 Stephan Pauxberger + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.blackbuild.klum.common; + +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotatedNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.CompileUnit; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.GenericsType; +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.ClassExpression; +import org.codehaus.groovy.ast.expr.ClosureExpression; +import org.codehaus.groovy.ast.expr.ConstantExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.ListExpression; +import org.codehaus.groovy.ast.expr.MapEntryExpression; +import org.codehaus.groovy.ast.expr.MapExpression; +import org.codehaus.groovy.ast.expr.PropertyExpression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.ExpressionStatement; +import org.codehaus.groovy.ast.stmt.Statement; +import org.codehaus.groovy.classgen.Verifier; +import org.codehaus.groovy.control.SourceUnit; +import org.codehaus.groovy.control.messages.SyntaxErrorMessage; +import org.codehaus.groovy.control.messages.WarningMessage; +import org.codehaus.groovy.syntax.SyntaxException; +import org.codehaus.groovy.syntax.Token; +import org.codehaus.groovy.syntax.Types; +import org.codehaus.groovy.transform.AbstractASTTransformation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import static groovyjarjarasm.asm.Opcodes.ACC_ABSTRACT; +import static groovyjarjarasm.asm.Opcodes.ACC_FINAL; +import static org.codehaus.groovy.ast.ClassHelper.STRING_TYPE; +import static org.codehaus.groovy.ast.ClassHelper.makeWithoutCaching; +import static org.codehaus.groovy.ast.expr.CastExpression.asExpression; +import static org.codehaus.groovy.ast.tools.GeneralUtils.args; +import static org.codehaus.groovy.ast.tools.GeneralUtils.closureX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.ctorX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.param; +import static org.codehaus.groovy.ast.tools.GeneralUtils.params; +import static org.codehaus.groovy.ast.tools.GeneralUtils.varX; +import static org.codehaus.groovy.ast.tools.GenericsUtils.makeClassSafe; + +/** + * Created by stephan on 05.12.2016. + */ +public class CommonAstHelper { + + public static final ClassNode[] NO_EXCEPTIONS = ClassNode.EMPTY_ARRAY; + public static final FieldNode NO_SUCH_FIELD = new FieldNode(null, 0, null, null, null); + public static ClassNode COLLECTION_TYPE = makeWithoutCaching(Collection.class); + public static ClassNode SORTED_MAP_TYPE = makeWithoutCaching(SortedMap.class); + + public static AnnotationNode getAnnotation(AnnotatedNode field, ClassNode type) { + List annotation = field.getAnnotations(type); + return annotation.isEmpty() ? null : annotation.get(0); + } + + public static boolean isCollectionOrMap(ClassNode type) { + return isCollection(type) || isMap(type); + } + + public static boolean isCollection(ClassNode type) { + return type.equals(COLLECTION_TYPE) || type.implementsInterface(COLLECTION_TYPE); + } + + public static boolean isMap(ClassNode type) { + return type.equals(ClassHelper.MAP_TYPE) || type.implementsInterface(ClassHelper.MAP_TYPE); + } + + public static boolean isAbstract(ClassNode classNode) { + return (classNode.getModifiers() & ACC_ABSTRACT) != 0; + } + + public static ListExpression listExpression(Expression... expressions) { + return new ListExpression(Arrays.asList(expressions)); + } + + static ArgumentListExpression argsWithOptionalKey(String keyFieldName, String... otherArgs) { + if (keyFieldName == null) + return args(otherArgs); + + ArgumentListExpression result = new ArgumentListExpression(varX(keyFieldName)); + + for (String next : otherArgs) + result.addExpression(varX(next)); + + return result; + } + + public static ArgumentListExpression argsWithEmptyMapAndOptionalKey(String keyFieldName, String... otherArgs) { + ArgumentListExpression result = new ArgumentListExpression(new MapExpression()); + + if (keyFieldName != null) + result.addExpression(varX(keyFieldName)); + + for (String next : otherArgs) + result.addExpression(varX(next)); + + return result; + } + + public static ArgumentListExpression argsWithEmptyMapClassAndOptionalKey(String keyFieldName, String... otherArgs) { + ArgumentListExpression result = new ArgumentListExpression(new MapExpression()); + + result.addExpression(varX("typeToCreate")); + + if (keyFieldName != null) + result.addExpression(varX(keyFieldName)); + + for (String next : otherArgs) + result.addExpression(varX(next)); + + return result; + } + + public static void addCompileError(SourceUnit sourceUnit, String msg, ASTNode node) { + SyntaxException se = new SyntaxException(msg, node.getLineNumber(), node.getColumnNumber()); + sourceUnit.getErrorCollector().addFatalError(new SyntaxErrorMessage(se, sourceUnit)); + } + + public static void addCompileError(String msg, AnnotatedNode node) { + addCompileError(msg, node, node); + } + + public static void addCompileError(String msg, AnnotatedNode node, ASTNode sourcePosition) { + if (node instanceof FieldNode) + addCompileError(msg, (FieldNode) node); + else if (node instanceof ClassNode) + addCompileError(msg, (ClassNode) node); + else + throw new IllegalStateException(node.toString() + " must be either a ClassNode or a FieldNode"); + } + + public static void addCompileError(String msg, FieldNode node) { + addCompileError(msg, node, node); + } + + + public static void addCompileError(String msg, FieldNode node, ASTNode sourcePosition) { + addCompileError(node.getOwner().getModule().getContext(), msg, sourcePosition); + } + + public static void addCompileError(String msg, ClassNode node) { + addCompileError(msg, node, node); + } + + public static void addCompileError(String msg, ClassNode node, ASTNode sourcePosition) { + addCompileError(node.getModule().getContext(), msg, sourcePosition); + } + + public static void addCompileWarning(SourceUnit sourceUnit, String msg, ASTNode node) { + Token token = new Token(Types.UNKNOWN, node.getText(), node.getLineNumber(), node.getColumnNumber()); + sourceUnit.getErrorCollector().addWarning(WarningMessage.LIKELY_ERRORS, msg, token, sourceUnit); + } + + public static void assertMethodIsParameterless(MethodNode method, SourceUnit sourceUnit) { + if (method.getParameters().length > 0) + addCompileError(sourceUnit, "Lifecycle/Validate methods must be parameterless!", method); + } + + public static void assertMethodIsNotPrivate(MethodNode method, SourceUnit sourceUnit) { + if (method.isPrivate()) + addCompileError(sourceUnit, "Lifecycle methods must not be private!", method); + } + + public static void replaceMethod(ClassNode target, MethodNode method) { + MethodNode oldMethod = target.getDeclaredMethod(method.getName(), method.getParameters()); + if (oldMethod != null) + target.removeMethod(oldMethod); + target.addMethod(method); + } + + public static ClosureExpression toStronglyTypedClosure(ClosureExpression validationClosure, ClassNode parameterType) { + String closureParameterName = validationClosure.isParameterSpecified() ? validationClosure.getParameters()[0].getName() : "it"; + ClosureExpression typeValidationClosure = closureX(params(param(parameterType.getPlainNodeReference(), closureParameterName)), validationClosure.getCode()); + typeValidationClosure.copyNodeMetaData(validationClosure); + typeValidationClosure.setSourcePosition(validationClosure); + + typeValidationClosure.visit(new StronglyTypingClosureParameterVisitor(closureParameterName, parameterType.getPlainNodeReference())); + return typeValidationClosure; + } + + static void addPropertyAsFieldWithAccessors(ClassNode cNode, PropertyNode pNode) { + final FieldNode fn = pNode.getField(); + cNode.getFields().remove(fn); + cNode.addField(fn); + + Statement getterBlock = pNode.getGetterBlock(); + Statement setterBlock = pNode.getSetterBlock(); + + int modifiers = pNode.getModifiers(); + String capitalizedName = Verifier.capitalize(pNode.getName()); + + if (getterBlock != null) { + MethodNode getter = + new MethodNode("get" + capitalizedName, modifiers, pNode.getType(), Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, getterBlock); + getter.setSynthetic(true); + addPropertyMethod(cNode, getter); + + if (ClassHelper.boolean_TYPE == pNode.getType() || ClassHelper.Boolean_TYPE == pNode.getType()) { + String secondGetterName = "is" + capitalizedName; + MethodNode secondGetter = + new MethodNode(secondGetterName, modifiers, pNode.getType(), Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, getterBlock); + secondGetter.setSynthetic(true); + addPropertyMethod(cNode, secondGetter); + } + } + if (setterBlock != null) { + Parameter[] setterParameterTypes = {new Parameter(pNode.getType(), "value")}; + MethodNode setter = + new MethodNode("set" + capitalizedName, modifiers, ClassHelper.VOID_TYPE, setterParameterTypes, ClassNode.EMPTY_ARRAY, setterBlock); + setter.setSynthetic(true); + addPropertyMethod(cNode, setter); + } + } + + // from Verifier + static void addPropertyMethod(ClassNode classNode, MethodNode method) { + classNode.addMethod(method); + // GROOVY-4415 / GROOVY-4645: check that there's no abstract method which corresponds to this one + List abstractMethods = classNode.getAbstractMethods(); + if (abstractMethods==null) return; + String methodName = method.getName(); + Parameter[] parameters = method.getParameters(); + ClassNode methodReturnType = method.getReturnType(); + for (MethodNode node : abstractMethods) { + if (!node.getDeclaringClass().equals(classNode)) continue; + if (node.getName().equals(methodName) + && node.getParameters().length==parameters.length) { + if (parameters.length==1) { + // setter + ClassNode abstractMethodParameterType = node.getParameters()[0].getType(); + ClassNode methodParameterType = parameters[0].getType(); + if (!methodParameterType.isDerivedFrom(abstractMethodParameterType) && !methodParameterType.implementsInterface(abstractMethodParameterType)) { + continue; + } + } + ClassNode nodeReturnType = node.getReturnType(); + if (!methodReturnType.isDerivedFrom(nodeReturnType) && !methodReturnType.implementsInterface(nodeReturnType)) { + continue; + } + // matching method, remove abstract status and use the same body + node.setModifiers(node.getModifiers() ^ ACC_ABSTRACT); + node.setCode(method.getCode()); + } + } + } + + public static void replaceProperties(ClassNode annotatedClass, List newNodes) { + for (PropertyNode pNode : newNodes) { + annotatedClass.getProperties().remove(pNode); + addPropertyAsFieldWithAccessors(annotatedClass, pNode); + } + } + + public static List findAllKnownSubclassesOf(ClassNode type) { + return findAllKnownSubclassesOf(type, type.getCompileUnit()); + } + + public static List findAllKnownSubclassesOf(ClassNode type, CompileUnit compileUnit) { + if ((type.getModifiers() & ACC_FINAL) != 0) + return Collections.emptyList(); + List result = new ArrayList(); + + for (ClassNode classInCU : (List) compileUnit.getClasses()) + if (classInCU.isDerivedFrom(type)) + result.add(classInCU); + return result; + } + + @SuppressWarnings("ConstantConditions") + public static GenericsType[] getGenericsTypes(FieldNode fieldNode) { + GenericsType[] types = fieldNode.getType().getGenericsTypes(); + + if (types == null) + addCompileError(fieldNode.getOwner().getModule().getContext(), "Lists and Maps need to be assigned an explicit Generic Type", fieldNode); + return types; + } + + public static ClassNode getElementType(FieldNode fieldNode) { + if (isMap(fieldNode.getType())) + return getGenericsTypes(fieldNode)[1].getType(); + else if (isCollection(fieldNode.getType())) + return getGenericsTypes(fieldNode)[0].getType(); + else + return fieldNode.getType(); + } + + public static String getNullSafeMemberStringValue(AnnotationNode fieldAnnotation, String name, String defaultValue) { + return fieldAnnotation == null ? defaultValue : AbstractASTTransformation.getMemberStringValue(fieldAnnotation, name, defaultValue); + } + + public static T getNullSafeEnumMemberValue(AnnotationNode node, String name, T defaultValue) { + if (node == null) + return defaultValue; + + Expression member = node.getMember(name); + if (member == null) + return defaultValue; + + if (!(member instanceof PropertyExpression)) + return defaultValue; + + String value = ((PropertyExpression) member).getPropertyAsString(); + + return (T) Enum.valueOf(defaultValue.getClass(), value); + } + + public static void initializeCollectionOrMap(FieldNode fieldNode) { + ClassNode fieldType = fieldNode.getType(); + if (isCollection(fieldType)) + initializeField(fieldNode, asExpression(fieldType, new ListExpression())); + else if (fieldType.equals(CommonAstHelper.SORTED_MAP_TYPE)) + initializeField(fieldNode, ctorX(makeClassSafe(TreeMap.class))); + else if (isMap(fieldType)) + initializeField(fieldNode, asExpression(fieldType, new MapExpression())); + else + throw new IllegalStateException("FieldNode " + fieldNode + " is no collection or Map"); + } + + private static void initializeField(FieldNode fieldNode, Expression init) { + if (!fieldNode.hasInitialExpression()) + fieldNode.setInitialValueExpression(init); + } + + public static String getQualifiedName(FieldNode node) { + return node.getOwner().getName() + "." + node.getName(); + } + + public static MapExpression getLiteralMapExpressionFromClosure(ClosureExpression closure) { + BlockStatement code = (BlockStatement) closure.getCode(); + if (code.getStatements().size() != 1) return null; + Statement statement = code.getStatements().get(0); + if (!(statement instanceof ExpressionStatement)) return null; + Expression expression = ((ExpressionStatement) statement).getExpression(); + if (!(expression instanceof MapExpression)) return null; + return (MapExpression) expression; + } + + public static Map getStringClassMapFromClosure(ClosureExpression closureExpression, AnnotatedNode source) { + MapExpression map = getLiteralMapExpressionFromClosure(closureExpression); + if (map == null) + return null; + + Map result = new LinkedHashMap(); + + for (MapEntryExpression entry : map.getMapEntryExpressions()) { + String key = getKeyStringFromLiteralMapEntry(entry, source); + ClassNode value = getClassNodeValueFromLiteralMapEntry(entry, source); + + result.put(key, value); + } + + return result; + } + + public static String getKeyStringFromLiteralMapEntry(MapEntryExpression entryExpression, AnnotatedNode source) { + Expression result = entryExpression.getKeyExpression(); + if (result instanceof ConstantExpression && result.getType().equals(STRING_TYPE)) + return result.getText(); + + CommonAstHelper.addCompileError("Map keys may only be Strings.", source, entryExpression); + return null; + } + + public static ClassNode getClassNodeValueFromLiteralMapEntry(MapEntryExpression entryExpression, AnnotatedNode source) { + Expression result = entryExpression.getValueExpression(); + if (result instanceof ClassExpression) + return result.getType(); + + CommonAstHelper.addCompileError("Map values may only be classes.", source, entryExpression); + return null; + } + +} diff --git a/klum-ast-common/src/main/java/com/blackbuild/klum/common/GenericsMethodBuilder.java b/klum-ast-common/src/main/java/com/blackbuild/klum/common/GenericsMethodBuilder.java new file mode 100644 index 00000000..0b01c2f7 --- /dev/null +++ b/klum-ast-common/src/main/java/com/blackbuild/klum/common/GenericsMethodBuilder.java @@ -0,0 +1,454 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2015-2017 Stephan Pauxberger + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.blackbuild.klum.common; + +import groovy.lang.Closure; +import groovy.lang.DelegatesTo; +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.AnnotationNode; +import org.codehaus.groovy.ast.ClassHelper; +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.FieldNode; +import org.codehaus.groovy.ast.GenericsType; +import org.codehaus.groovy.ast.MethodNode; +import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.expr.ClosureExpression; +import org.codehaus.groovy.ast.expr.Expression; +import org.codehaus.groovy.ast.expr.MethodCallExpression; +import org.codehaus.groovy.ast.stmt.BlockStatement; +import org.codehaus.groovy.ast.stmt.ExpressionStatement; +import org.codehaus.groovy.ast.stmt.ForStatement; +import org.codehaus.groovy.ast.stmt.Statement; +import org.codehaus.groovy.ast.tools.GeneralUtils; +import org.codehaus.groovy.ast.tools.GenericsUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.codehaus.groovy.ast.ClassHelper.CLASS_Type; +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.block; +import static org.codehaus.groovy.ast.tools.GeneralUtils.callX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.classX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.closureX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.constX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.propX; +import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS; +import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt; +import static org.codehaus.groovy.ast.tools.GeneralUtils.varX; +import static org.codehaus.groovy.ast.tools.GenericsUtils.buildWildcardType; +import static org.codehaus.groovy.ast.tools.GenericsUtils.makeClassSafeWithGenerics; +import static org.codehaus.groovy.ast.tools.GenericsUtils.nonGeneric; + +@SuppressWarnings("unchecked") +public abstract class GenericsMethodBuilder { + + public static final ClassNode DEPRECATED_NODE = ClassHelper.make(Deprecated.class); + private static final ClassNode[] EMPTY_EXCEPTIONS = new ClassNode[0]; + private static final Parameter[] EMPTY_PARAMETERS = new Parameter[0]; + private static final ClassNode DELEGATES_TO_ANNOTATION = make(DelegatesTo.class); + private static final ClassNode DELEGATES_TO_TARGET_ANNOTATION = make(DelegatesTo.Target.class); + protected String name; + protected Map metadata = new HashMap(); + private int modifiers; + private ClassNode returnType = ClassHelper.VOID_TYPE; + private List exceptions = new ArrayList(); + private List parameters = new ArrayList(); + private boolean deprecated; + private BlockStatement body = new BlockStatement(); + private boolean optional; + private ASTNode sourceLinkTo; + + protected GenericsMethodBuilder(String name) { + this.name = name; + } + + /** + * Creates the actual method and adds it to the target class. If the target already contains a method with that + * signature, either an exception is thrown or the method is silently dropped, depending on the presence of + * the optional parameter. + * @param target The class node to add to + * @return The newly created method or the existing method if `optional` is set and a method with that signature + * already exists + */ + @SuppressWarnings("ToArrayCallWithZeroLengthArrayArgument") + public MethodNode addTo(ClassNode target) { + + Parameter[] parameterArray = this.parameters.toArray(EMPTY_PARAMETERS); + MethodNode existing = target.getDeclaredMethod(name, parameterArray); + + if (existing != null) { + if (optional) + return existing; + else + throw new MethodBuilderException("Method " + existing + " is already defined.", existing); + } + + MethodNode method = target.addMethod( + name, + modifiers, + returnType, + parameterArray, + exceptions.toArray(EMPTY_EXCEPTIONS), + body + ); + + if (deprecated) + method.addAnnotation(new AnnotationNode(DEPRECATED_NODE)); + + if (sourceLinkTo != null) + method.setSourcePosition(sourceLinkTo); + + for (Map.Entry entry : metadata.entrySet()) { + method.putNodeMetaData(entry.getKey(), entry.getValue()); + } + + + return method; + } + + /** + * Marks this method as optional. If set, {@link #addTo(ClassNode)} does not throw an error if the method already exists. + */ + public T optional() { + this.optional = true; + return (T)this; + } + + /** + * Sets the return type of the generated method. + * @param returnType The return type. + */ + public T returning(ClassNode returnType) { + this.returnType = returnType; + return (T)this; + } + + /** + * Sets the modifiers as defined by {@link groovyjarjarasm.asm.Opcodes}. + */ + public T mod(int modifier) { + modifiers |= modifier; + return (T)this; + } + + /** + * Add a parameter to the method. + */ + public T param(Parameter param) { + parameters.add(param); + return (T)this; + } + + public T deprecated() { + deprecated = true; + return (T)this; + } + + /** + * Adds a map entry to the method signature. + */ + public T namedParams(String name) { + GenericsType wildcard = new GenericsType(ClassHelper.OBJECT_TYPE); + wildcard.setWildcard(true); + return param(makeClassSafeWithGenerics(ClassHelper.MAP_TYPE, new GenericsType(ClassHelper.STRING_TYPE), wildcard), name); + } + + /** + * Adds a statement to the method body that converts each entry in the map into a call of a method with the key as + * methodname and the value as method parameter. + */ + public T applyNamedParams(String parameterMapName) { + statement( + new ForStatement(new Parameter(ClassHelper.DYNAMIC_TYPE, "it"), callX(varX(parameterMapName), "entrySet"), + new ExpressionStatement( + new MethodCallExpression( + varX("$rw"), + "invokeMethod", + args(propX(varX("it"), "key"), propX(varX("it"), "value")) + ) + ) + ) + ); + + return (T)this; + } + + /** + * Adds a parameter of type closure. + */ + public T closureParam(String name) { + param(GeneralUtils.param(GenericsUtils.nonGeneric(ClassHelper.CLOSURE_TYPE), name)); + return (T)this; + } + + /** + * Adds a class parameter which is also used as the target for the {@link DelegatesTo} annotation of a delegating closure parameter + * @param name Name of the parameter + * @param upperBound The base class for the class parameter + */ + public T delegationTargetClassParam(String name, ClassNode upperBound) { + Parameter param = GeneralUtils.param(makeClassSafeWithGenerics(CLASS_Type, buildWildcardType(upperBound)), name); + param.addAnnotation(new AnnotationNode(DELEGATES_TO_TARGET_ANNOTATION)); + return param(param); + } + + /** + * Adds a class parameter without delegation. + * @param name The name of the parameter + * @param upperBound The base class for the class parameter + * @return + */ + public T simpleClassParam(String name, ClassNode upperBound) { + return param(makeClassSafeWithGenerics(CLASS_Type, buildWildcardType(upperBound)), name); + } + + /** + * Adds a string paramter with the given name. + * @param name The name of the string parameter. + */ + public T stringParam(String name) { + return param(ClassHelper.STRING_TYPE, name); + } + + /** + * Convenience method to optionally add a string parameter. The parameter is only added, if 'addIfNotNull' is not null. + * @param name The name of the parameter. + * @param addIfNotNull If this parameter is null, the method does nothing + */ + @Deprecated + public T optionalStringParam(String name, Object addIfNotNull) { + return optionalStringParam(name, addIfNotNull != null); + } + + /** + * Convenience method to optionally add a string parameter. The parameter is only added, if 'addIfNotNull' is not null. + * @param name The name of the parameter. + * @param doAdd If this parameter is null, the method does nothing + */ + public T optionalStringParam(String name, boolean doAdd) { + if (doAdd) + stringParam(name); + return (T)this; + } + + /** + * Add a generic object parameter. + * @param name The name of the parameter + */ + public T objectParam(String name) { + return param(ClassHelper.OBJECT_TYPE, name); + } + + /** + * Add a parameter to the method signature. + * @param type The type of the parameter + * @param name The name of the parameter + */ + public T param(ClassNode type, String name) { + return param(new Parameter(type, name)); + } + + /** + * Add a parameter to the method signature with an optional default value. + * @param type The type of the parameter + * @param name The name of the parameter + * @param defaultValue An expression to use for the default value for the parameter. Can be null. + */ + public T param(ClassNode type, String name, Expression defaultValue) { + return param(new Parameter(type, name, defaultValue)); + } + + /** + * Adds an array parameter with the given type. + * @param type The type of the array elements + * @param name The name of the parameter + */ + public T arrayParam(ClassNode type, String name) { + return param(new Parameter(type.makeArray(), name)); + } + + /** + * Use all parameters of the given source method as parameters to this method. + * @param sourceMethod The source of the parameter list + */ + public T cloneParamsFrom(MethodNode sourceMethod) { + Parameter[] sourceParams = GeneralUtils.cloneParams(sourceMethod.getParameters()); + for (Parameter parameter : sourceParams) { + param(parameter); + } + return (T)this; + } + + /** + * Add custom metadata to the created AST node of the methods + * @param key The key of the metadata + * @param value The name of the metadata + */ + public T withMetadata(Object key, Object value) { + metadata.put(key, value); + return (T)this; + } + + public T delegatingClosureParam(ClassNode delegationTarget, ClosureDefaultValue defaultValue) { + ClosureExpression emptyClosure = null; + if (defaultValue == ClosureDefaultValue.EMPTY_CLOSURE) { + emptyClosure = closureX(block()); + } + Parameter param = GeneralUtils.param( + nonGeneric(ClassHelper.CLOSURE_TYPE), + "closure", + emptyClosure + ); + param.addAnnotation(createDelegatesToAnnotation(delegationTarget)); + return param(param); + } + + /** + * Creates a delegating closure parameter that delegates to the type parameter of an existing class parameter. + */ + public T delegatingClosureParam() { + return delegatingClosureParam(null, ClosureDefaultValue.EMPTY_CLOSURE); + } + + private AnnotationNode createDelegatesToAnnotation(ClassNode target) { + AnnotationNode result = new AnnotationNode(DELEGATES_TO_ANNOTATION); + if (target != null) + result.setMember("value", classX(target)); + else + result.setMember("genericTypeIndex", constX(0)); + result.setMember("strategy", constX(Closure.DELEGATE_ONLY)); + return result; + } + + public T statement(Statement statement) { + body.addStatement(statement); + return (T)this; + } + + public T statementIf(boolean condition, Statement statement) { + if (condition) + body.addStatement(statement); + return (T)this; + } + + public T assignToProperty(String propertyName, Expression value) { + String[] split = propertyName.split("\\.", 2); + if (split.length == 1) + return assignS(propX(varX("this"), propertyName), value); + + return assignS(propX(varX(split[0]), split[1]), value); + } + + public T assignS(Expression target, Expression value) { + return statement(GeneralUtils.assignS(target, value)); + } + + public T optionalAssignPropertyFromPropertyS(String target, String targetProperty, String value, String valueProperty, Object marker) { + if (marker != null) + assignS(propX(varX(target), targetProperty), propX(varX(value), valueProperty)); + return (T)this; + } + + public T declareVariable(String varName, Expression init) { + return statement(GeneralUtils.declS(varX(varName), init)); + } + + public T optionalDeclareVariable(String varName, Expression init, boolean doAdd) { + if (doAdd) + statement(GeneralUtils.declS(varX(varName), init)); + return (T)this; + } + + public T callMethod(Expression receiver, String methodName) { + return callMethod(receiver, methodName, MethodCallExpression.NO_ARGUMENTS); + } + + public T callMethod(String receiverName, String methodName) { + return callMethod(varX(receiverName), methodName); + } + + public T callMethod(Expression receiver, String methodName, Expression args) { + return statement(callX(receiver, methodName, args)); + } + + public T callMethod(String receiverName, String methodName, Expression args) { + return callMethod(varX(receiverName), methodName, args); + } + + public T callThis(String methodName, Expression args) { + return callMethod("this", methodName, args); + } + + public T callThis(String methodName) { + return callMethod("this", methodName); + } + + @Deprecated + public T println(Expression args) { + return callThis("println", args); + } + + @Deprecated + public T println(String string) { + return callThis("println", constX(string)); + } + + public T statement(Expression expression) { + return statement(stmt(expression)); + } + + public T statementIf(boolean condition, Expression expression) { + return statementIf(condition, stmt(expression)); + } + + public T doReturn(String varName) { + return doReturn(varX(varName)); + } + + public T doReturn(Expression expression) { + return statement(returnS(expression)); + } + + public T linkToField(FieldNode fieldNode) { + return (T) inheritDeprecationFrom(fieldNode).sourceLinkTo(fieldNode); + } + + public T inheritDeprecationFrom(FieldNode fieldNode) { + if (!fieldNode.getAnnotations(DEPRECATED_NODE).isEmpty()) { + deprecated = true; + } + return (T)this; + } + + public T sourceLinkTo(ASTNode sourceLinkTo) { + this.sourceLinkTo = sourceLinkTo; + return (T)this; + } + + public enum ClosureDefaultValue { NONE, EMPTY_CLOSURE } +} diff --git a/klum-ast-common/src/main/java/com/blackbuild/klum/common/MethodBuilder.java b/klum-ast-common/src/main/java/com/blackbuild/klum/common/MethodBuilder.java new file mode 100644 index 00000000..80ec6af8 --- /dev/null +++ b/klum-ast-common/src/main/java/com/blackbuild/klum/common/MethodBuilder.java @@ -0,0 +1,54 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2015-2017 Stephan Pauxberger + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.blackbuild.klum.common; + +import groovyjarjarasm.asm.Opcodes; + +public final class MethodBuilder extends GenericsMethodBuilder { + + private MethodBuilder(String name) { + super(name); + } + + public static MethodBuilder createMethod(String name) { + return new MethodBuilder(name); + } + + public static MethodBuilder createPublicMethod(String name) { + return new MethodBuilder(name).mod(Opcodes.ACC_PUBLIC); + } + + public static MethodBuilder createOptionalPublicMethod(String name) { + return new MethodBuilder(name).mod(Opcodes.ACC_PUBLIC).optional(); + } + + public static MethodBuilder createProtectedMethod(String name) { + return new MethodBuilder(name).mod(Opcodes.ACC_PROTECTED); + } + + public static MethodBuilder createPrivateMethod(String name) { + return new MethodBuilder(name).mod(Opcodes.ACC_PRIVATE); + } + +} diff --git a/klum-ast-common/src/main/java/com/blackbuild/klum/common/MethodBuilderException.java b/klum-ast-common/src/main/java/com/blackbuild/klum/common/MethodBuilderException.java new file mode 100644 index 00000000..c0625777 --- /dev/null +++ b/klum-ast-common/src/main/java/com/blackbuild/klum/common/MethodBuilderException.java @@ -0,0 +1,39 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2015-2017 Stephan Pauxberger + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.blackbuild.klum.common; + +import org.codehaus.groovy.ast.ASTNode; + +public class MethodBuilderException extends RuntimeException { + private final transient ASTNode node; + + public MethodBuilderException(String message, ASTNode node) { + super(message); + this.node = node; + } + + public ASTNode getNode() { + return node; + } +} diff --git a/klum-ast-common/src/main/java/com/blackbuild/klum/common/StronglyTypingClosureParameterVisitor.java b/klum-ast-common/src/main/java/com/blackbuild/klum/common/StronglyTypingClosureParameterVisitor.java new file mode 100644 index 00000000..5299b79a --- /dev/null +++ b/klum-ast-common/src/main/java/com/blackbuild/klum/common/StronglyTypingClosureParameterVisitor.java @@ -0,0 +1,48 @@ +/** + * The MIT License (MIT) + * + * Copyright (c) 2015-2017 Stephan Pauxberger + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.blackbuild.klum.common; + +import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.CodeVisitorSupport; +import org.codehaus.groovy.ast.expr.VariableExpression; + +/** + * Created by stephan on 07.04.2017. + */ +public class StronglyTypingClosureParameterVisitor extends CodeVisitorSupport { + private final String name; + private final ClassNode type; + + public StronglyTypingClosureParameterVisitor(String name, ClassNode type) { + this.name = name; + this.type = type; + } + + @Override + public void visitVariableExpression(VariableExpression expression) { + if (!expression.getName().equals(name)) + return; + expression.setAccessedVariable(new VariableExpression(name, type)); + } +} diff --git a/klum-ast/build.gradle b/klum-ast/build.gradle index 05f10352..4934e044 100644 --- a/klum-ast/build.gradle +++ b/klum-ast/build.gradle @@ -9,8 +9,8 @@ test { dependencies { // all dependencies are compile time dependencies (for AST "runtime" is compile time of a client project") + api project(':klum-ast-common') api project(':klum-ast-annotations') api project(':klum-ast-runtime') - api "com.blackbuild.klum.common:klum-common:$commonVersion" } diff --git a/settings.gradle b/settings.gradle index 43dca029..e9dfeef9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,5 @@ rootProject.name = 'klum-ast-root' +include 'klum-ast-common' include 'klum-ast-annotations' include 'klum-ast' include 'klum-ast-runtime'