blob: 65fc823ad89dbc0b3af54060f79cf38f82317aea [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.apache.groovy.ast.tools;
import org.apache.groovy.util.BeanUtils;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ConstructorNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.PropertyNode;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MapExpression;
import org.codehaus.groovy.ast.expr.SpreadExpression;
import org.codehaus.groovy.ast.expr.TupleExpression;
import org.codehaus.groovy.ast.stmt.Statement;
import org.codehaus.groovy.transform.AbstractASTTransformation;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.isGenerated;
import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.markAsGenerated;
import static org.codehaus.groovy.ast.ClassHelper.boolean_TYPE;
import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
/**
* Utility class for working with ClassNodes
*/
public class ClassNodeUtils {
/**
* Formats a type name into a human readable version. For arrays, appends "[]" to the formatted
* type name of the component. For unit class nodes, uses the class node name.
*
* @param cNode the type to format
* @return a human readable version of the type name (java.lang.String[] for example)
*/
public static String formatTypeName(ClassNode cNode) {
if (cNode.isArray()) {
ClassNode it = cNode;
int dim = 0;
while (it.isArray()) {
dim++;
it = it.getComponentType();
}
StringBuilder sb = new StringBuilder(it.getName().length() + 2 * dim);
sb.append(it.getName());
for (int i = 0; i < dim; i++) {
sb.append("[]");
}
return sb.toString();
}
return cNode.getName();
}
/**
* Return an existing method if one exists or else create a new method and mark it as {@code @Generated}.
*
* @see ClassNode#addMethod(String, int, ClassNode, Parameter[], ClassNode[], Statement)
*/
public static MethodNode addGeneratedMethod(ClassNode cNode, String name,
int modifiers,
ClassNode returnType,
Parameter[] parameters,
ClassNode[] exceptions,
Statement code) {
MethodNode existing = cNode.getDeclaredMethod(name, parameters);
if (existing != null) return existing;
MethodNode result = new MethodNode(name, modifiers, returnType, parameters, exceptions, code);
addGeneratedMethod(cNode, result);
return result;
}
/**
* Add a method and mark it as {@code @Generated}.
*
* @see ClassNode#addMethod(MethodNode)
*/
public static void addGeneratedMethod(ClassNode cNode, MethodNode mNode) {
cNode.addMethod(mNode);
markAsGenerated(cNode, mNode);
}
/**
* Add an inner class that is marked as {@code @Generated}.
*
* @see org.codehaus.groovy.ast.ModuleNode#addClass(ClassNode)
*/
public static void addGeneratedInnerClass(ClassNode cNode, ClassNode inner) {
cNode.getModule().addClass(inner);
markAsGenerated(cNode, inner);
}
/**
* Add a method that is marked as {@code @Generated}.
*
* @see ClassNode#addConstructor(int, Parameter[], ClassNode[], Statement)
*/
public static ConstructorNode addGeneratedConstructor(ClassNode classNode, int modifiers, Parameter[] parameters, ClassNode[] exceptions, Statement code) {
ConstructorNode consNode = classNode.addConstructor(modifiers, parameters, exceptions, code);
markAsGenerated(classNode, consNode);
return consNode;
}
/**
* Add a method that is marked as {@code @Generated}.
*
* @see ClassNode#addConstructor(ConstructorNode)
*/
public static void addGeneratedConstructor(ClassNode classNode, ConstructorNode consNode) {
classNode.addConstructor(consNode);
markAsGenerated(classNode, consNode);
}
/**
* Add methods from the super class.
*
* @param cNode The ClassNode
* @return A map of methods
*/
public static Map<String, MethodNode> getDeclaredMethodsFromSuper(ClassNode cNode) {
ClassNode parent = cNode.getSuperClass();
if (parent == null) {
return new HashMap<>();
}
return parent.getDeclaredMethodsMap();
}
/**
* Adds methods from all interfaces. Existing entries in the methods map
* take precedence. Methods from interfaces visited early take precedence
* over later ones.
*
* @param cNode The ClassNode
* @param methodsMap A map of existing methods to alter
*/
public static void addDeclaredMethodsFromInterfaces(ClassNode cNode, Map<String, MethodNode> methodsMap) {
for (ClassNode iface : cNode.getInterfaces()) {
Map<String, MethodNode> declaredMethods = iface.getDeclaredMethodsMap();
for (Map.Entry<String, MethodNode> entry : declaredMethods.entrySet()) {
if (entry.getValue().getDeclaringClass().isInterface() && (entry.getValue().getModifiers() & ACC_SYNTHETIC) == 0) {
methodsMap.putIfAbsent(entry.getKey(), entry.getValue());
}
}
}
}
/**
* Gets methods from all interfaces. Methods from interfaces visited early
* take precedence over later ones.
*
* @param cNode The ClassNode
* @return A map of methods
*/
public static Map<String, MethodNode> getDeclaredMethodsFromInterfaces(ClassNode cNode) {
Map<String, MethodNode> methodsMap = new HashMap<>();
addDeclaredMethodsFromInterfaces(cNode, methodsMap);
return methodsMap;
}
/**
* Adds methods from interfaces and parent interfaces. Existing entries in the methods map take precedence.
* Methods from interfaces visited early take precedence over later ones.
*
* @param cNode The ClassNode
* @param methodsMap A map of existing methods to alter
*/
public static void addDeclaredMethodsFromAllInterfaces(ClassNode cNode, Map<String, MethodNode> methodsMap) {
List<?> cnInterfaces = Arrays.asList(cNode.getInterfaces());
ClassNode parent = cNode.getSuperClass();
while (parent != null && !parent.equals(ClassHelper.OBJECT_TYPE)) {
ClassNode[] interfaces = parent.getInterfaces();
for (ClassNode iface : interfaces) {
if (!cnInterfaces.contains(iface)) {
methodsMap.putAll(iface.getDeclaredMethodsMap());
}
}
parent = parent.getSuperClass();
}
}
/**
* Returns true if the given method has a possibly matching static method with the given name and arguments.
* Handles default arguments and optionally spread expressions.
*
* @param cNode the ClassNode of interest
* @param name the name of the method of interest
* @param arguments the arguments to match against
* @param trySpread whether to try to account for SpreadExpressions within the arguments
* @return true if a matching method was found
*/
public static boolean hasPossibleStaticMethod(ClassNode cNode, String name, Expression arguments, boolean trySpread) {
int count = 0;
boolean foundSpread = false;
if (arguments instanceof TupleExpression) {
TupleExpression tuple = (TupleExpression) arguments;
for (Expression arg : tuple.getExpressions()) {
if (arg instanceof SpreadExpression) {
foundSpread = true;
} else {
count++;
}
}
} else if (arguments instanceof MapExpression) {
count = 1;
}
for (MethodNode method : cNode.getMethods(name)) {
if (method.isStatic()) {
Parameter[] parameters = method.getParameters();
// do fuzzy match for spread case: count will be number of non-spread args
if (trySpread && foundSpread && parameters.length >= count) return true;
if (parameters.length == count) return true;
// handle varargs case
if (parameters.length > 0 && parameters[parameters.length - 1].getType().isArray()) {
if (count >= parameters.length - 1) return true;
// fuzzy match any spread to a varargs
if (trySpread && foundSpread) return true;
}
// handle parameters with default values
int nonDefaultParameters = 0;
for (Parameter parameter : parameters) {
if (!parameter.hasInitialExpression()) {
nonDefaultParameters++;
}
}
if (count < parameters.length && nonDefaultParameters <= count) {
return true;
}
// TODO handle spread with nonDefaultParams?
}
}
return false;
}
/**
* Return true if we have a static accessor
*/
public static boolean hasPossibleStaticProperty(ClassNode cNode, String methodName) {
// assume explicit static method call checked first so we can assume a simple check here
if (!methodName.startsWith("get") && !methodName.startsWith("is")) {
return false;
}
String propName = getPropNameForAccessor(methodName);
PropertyNode pNode = getStaticProperty(cNode, propName);
return pNode != null && (methodName.startsWith("get") || boolean_TYPE.equals(pNode.getType()));
}
/**
* Returns the property name, e.g. age, given an accessor name, e.g. getAge.
* Returns the original if a valid prefix cannot be removed.
*
* @param accessorName the accessor name of interest, e.g. getAge
* @return the property name, e.g. age, or original if not a valid property accessor name
*/
public static String getPropNameForAccessor(String accessorName) {
if (!isValidAccessorName(accessorName)) return accessorName;
int prefixLength = accessorName.startsWith("is") ? 2 : 3;
return String.valueOf(accessorName.charAt(prefixLength)).toLowerCase() + accessorName.substring(prefixLength + 1);
}
/**
* Detect whether the given accessor name starts with "get", "set" or "is" followed by at least one character.
*
* @param accessorName the accessor name of interest, e.g. getAge
* @return true if a valid prefix is found
*/
public static boolean isValidAccessorName(String accessorName) {
if (accessorName.startsWith("get") || accessorName.startsWith("is") || accessorName.startsWith("set")) {
int prefixLength = accessorName.startsWith("is") ? 2 : 3;
return accessorName.length() > prefixLength;
}
return false;
}
public static boolean hasStaticProperty(ClassNode cNode, String propName) {
PropertyNode found = getStaticProperty(cNode, propName);
if (found == null) {
found = getStaticProperty(cNode, BeanUtils.decapitalize(propName));
}
return found != null;
}
/**
* Detect whether a static property with the given name is within the class
* or a super class.
*
* @param cNode the ClassNode of interest
* @param propName the property name
* @return the static property if found or else null
*/
public static PropertyNode getStaticProperty(ClassNode cNode, String propName) {
ClassNode classNode = cNode;
while (classNode != null) {
for (PropertyNode pn : classNode.getProperties()) {
if (pn.getName().equals(propName) && pn.isStatic()) return pn;
}
classNode = classNode.getSuperClass();
}
return null;
}
/**
* Detect whether a given ClassNode is a inner class (non-static).
*
* @param cNode the ClassNode of interest
* @return true if the given node is a (non-static) inner class, else false
*/
public static boolean isInnerClass(ClassNode cNode) {
return cNode.redirect().getOuterClass() != null
&& !Modifier.isStatic(cNode.getModifiers());
}
private ClassNodeUtils() { }
public static boolean hasNoArgConstructor(ClassNode cNode) {
List<ConstructorNode> constructors = cNode.getDeclaredConstructors();
for (ConstructorNode next : constructors) {
if (next.getParameters().length == 0) {
return true;
}
}
return false;
}
/**
* Determine if an explicit (non-generated) constructor is in the class.
*
* @param xform if non null, add an error if an explicit constructor is found
* @param cNode the type of the containing class
* @return true if an explicit (non-generated) constructor was found
*/
public static boolean hasExplicitConstructor(AbstractASTTransformation xform, ClassNode cNode) {
List<ConstructorNode> declaredConstructors = cNode.getDeclaredConstructors();
for (ConstructorNode constructorNode : declaredConstructors) {
// allow constructors added by other transforms if flagged as Generated
if (isGenerated(constructorNode)) {
continue;
}
if (xform != null) {
xform.addError("Error during " + xform.getAnnotationName() +
" processing. Explicit constructors not allowed for class: " +
cNode.getNameWithoutPackage(), constructorNode);
}
return true;
}
return false;
}
/**
* Determine if the given ClassNode values have the same package name.
*
* @param first a ClassNode
* @param second a ClassNode
* @return true if both given nodes have the same package name
* @throws NullPointerException if either of the given nodes are null
*/
public static boolean samePackageName(ClassNode first, ClassNode second) {
return Objects.equals(first.getPackageName(), second.getPackageName());
}
/**
* Return the (potentially inherited) field of the classnode.
*
* @param classNode the classnode
* @param fieldName the name of the field
* @return the field or null if not found
*/
public static FieldNode getField(ClassNode classNode, String fieldName) {
ClassNode node = classNode;
Set<String> visited = new HashSet<>();
while (node != null) {
FieldNode fn = node.getDeclaredField(fieldName);
if (fn != null) return fn;
ClassNode[] interfaces = node.getInterfaces();
for (ClassNode iNode : interfaces) {
if (visited.contains(iNode.getName())) continue;
FieldNode ifn = getField(iNode, fieldName);
visited.add(iNode.getName());
if (ifn != null) return ifn;
}
node = node.getSuperClass();
}
return null;
}
}