blob: 4cc060fcd9c0aaa7e657cf026c60e7827be8d3e4 [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.lang.Newify;
import groovy.lang.Reference;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.MethodNode;
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.ConstructorCallExpression;
import org.codehaus.groovy.ast.expr.DeclarationExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.ListExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import static org.codehaus.groovy.ast.ClassHelper.make;
import static org.codehaus.groovy.ast.tools.GeneralUtils.callX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.classX;
/**
* Handles generation of code for the {@code @Newify} AST transform.
*/
@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
public class NewifyASTTransformation extends ClassCodeExpressionTransformer implements ASTTransformation {
private static final ClassNode MY_TYPE = make(Newify.class);
private static final String MY_NAME = MY_TYPE.getNameWithoutPackage();
private static final String BASE_BAD_PARAM_ERROR = "Error during @" + MY_NAME +
" processing. Annotation parameter must be a class or list of classes but found ";
private SourceUnit source;
private ListExpression classesToNewify;
private DeclarationExpression candidate;
private boolean auto;
private Pattern classNamePattern;
private static Map<String, ClassNode> nameToGlobalClassesNodesMap;
private Map<String, NewifyClassData> nameToInnerClassesNodesMap;
// ClassHelper.classes minus interfaces, abstract classes, and classes with private ctors
private static final Class[] globalClasses = new Class[]{
Object.class,
Boolean.TYPE,
Character.TYPE,
Byte.TYPE,
Short.TYPE,
Integer.TYPE,
Long.TYPE,
Double.TYPE,
Float.TYPE,
// Void.TYPE,
// Closure.class,
// GString.class,
// List.class,
// Map.class,
// Range.class,
//Pattern.class,
// Script.class,
String.class,
Boolean.class, // Shall we allow this ? Using Boolean ctors is usually not what user wants...
Character.class,
Byte.class,
Short.class,
Integer.class,
Long.class,
Double.class,
Float.class,
BigDecimal.class,
BigInteger.class,
//Number.class,
//Void.class,
Reference.class,
//Class.class,
//MetaClass.class,
//Iterator.class,
//GeneratedClosure.class,
//GeneratedLambda.class,
//GroovyObjectSupport.class
};
static {
nameToGlobalClassesNodesMap = new ConcurrentHashMap<String, ClassNode>(16, 0.9f, 1);
for (Class globalClass : globalClasses) {
nameToGlobalClassesNodesMap.put(globalClass.getSimpleName(), ClassHelper.makeCached(globalClass));
}
}
private static final Pattern extractNamePattern = Pattern.compile("^(?:.*\\$|)(.*)$");
public static String extractName(final String s) {
return extractNamePattern.matcher(s).replaceFirst("$1");
}
public void visit(ASTNode[] nodes, SourceUnit source) {
this.source = source;
if (nodes.length != 2 || !(nodes[0] instanceof AnnotationNode) || !(nodes[1] instanceof AnnotatedNode)) {
internalError("Expecting [AnnotationNode, AnnotatedClass] but got: " + Arrays.asList(nodes));
}
AnnotatedNode parent = (AnnotatedNode) nodes[1];
AnnotationNode node = (AnnotationNode) nodes[0];
if (!MY_TYPE.equals(node.getClassNode())) {
internalError("Transformation called from wrong annotation: " + node.getClassNode().getName());
}
final boolean autoFlag = determineAutoFlag(node.getMember("auto"));
final Expression classNames = node.getMember("value");
final Pattern cnPattern = determineClassNamePattern(node.getMember("pattern"));
if (parent instanceof ClassNode) {
newifyClass((ClassNode) parent, autoFlag, determineClasses(classNames, false), cnPattern);
} else if (parent instanceof MethodNode || parent instanceof FieldNode) {
newifyMethodOrField(parent, autoFlag, determineClasses(classNames, false), cnPattern);
} else if (parent instanceof DeclarationExpression) {
newifyDeclaration((DeclarationExpression) parent, autoFlag, determineClasses(classNames, true), cnPattern);
}
}
private void newifyClass(ClassNode cNode, boolean autoFlag, ListExpression list, final Pattern cnPattern) {
String cName = cNode.getName();
if (cNode.isInterface()) {
addError("Error processing interface '" + cName + "'. @"
+ MY_NAME + " not allowed for interfaces.", cNode);
}
final ListExpression oldClassesToNewify = classesToNewify;
final boolean oldAuto = auto;
final Pattern oldCnPattern = classNamePattern;
classesToNewify = list;
auto = autoFlag;
classNamePattern = cnPattern;
super.visitClass(cNode);
classesToNewify = oldClassesToNewify;
auto = oldAuto;
classNamePattern = oldCnPattern;
}
private void newifyMethodOrField(AnnotatedNode parent, boolean autoFlag, ListExpression list, final Pattern cnPattern) {
final ListExpression oldClassesToNewify = classesToNewify;
final boolean oldAuto = auto;
final Pattern oldCnPattern = classNamePattern;
checkClassLevelClashes(list);
checkAutoClash(autoFlag, parent);
classesToNewify = list;
auto = autoFlag;
classNamePattern = cnPattern;
if (parent instanceof FieldNode) {
super.visitField((FieldNode) parent);
} else {
super.visitMethod((MethodNode) parent);
}
classesToNewify = oldClassesToNewify;
auto = oldAuto;
classNamePattern = oldCnPattern;
}
private void newifyDeclaration(DeclarationExpression de, boolean autoFlag, ListExpression list, final Pattern cnPattern) {
ClassNode cNode = de.getDeclaringClass();
candidate = de;
final ListExpression oldClassesToNewify = classesToNewify;
final boolean oldAuto = auto;
final Pattern oldCnPattern = classNamePattern;
classesToNewify = list;
auto = autoFlag;
classNamePattern = cnPattern;
super.visitClass(cNode);
classesToNewify = oldClassesToNewify;
auto = oldAuto;
classNamePattern = oldCnPattern;
}
private static boolean determineAutoFlag(Expression autoExpr) {
return !(autoExpr instanceof ConstantExpression && ((ConstantExpression) autoExpr).getValue().equals(false));
}
private Pattern determineClassNamePattern(Expression expr) {
if (!(expr instanceof ConstantExpression)) { return null; }
final ConstantExpression constExpr = (ConstantExpression) expr;
final String text = constExpr.getText();
if (constExpr.getValue() == null || text.equals("")) { return null; }
try {
final Pattern pattern = Pattern.compile(text);
return pattern;
} catch (PatternSyntaxException e) {
addError("Invalid class name pattern: " + e.getMessage(), expr);
return null;
}
}
/**
* allow non-strict mode in scripts because parsing not complete at that point
*/
private ListExpression determineClasses(Expression expr, boolean searchSourceUnit) {
ListExpression list = new ListExpression();
if (expr instanceof ClassExpression) {
list.addExpression(expr);
} else if (expr instanceof VariableExpression && searchSourceUnit) {
VariableExpression ve = (VariableExpression) expr;
ClassNode fromSourceUnit = getSourceUnitClass(ve);
if (fromSourceUnit != null) {
ClassExpression found = classX(fromSourceUnit);
found.setSourcePosition(ve);
list.addExpression(found);
} else {
addError(BASE_BAD_PARAM_ERROR + "an unresolvable reference to '" + ve.getName() + "'.", expr);
}
} else if (expr instanceof ListExpression) {
list = (ListExpression) expr;
final List<Expression> expressions = list.getExpressions();
for (int i = 0; i < expressions.size(); i++) {
Expression next = expressions.get(i);
if (next instanceof VariableExpression && searchSourceUnit) {
VariableExpression ve = (VariableExpression) next;
ClassNode fromSourceUnit = getSourceUnitClass(ve);
if (fromSourceUnit != null) {
ClassExpression found = classX(fromSourceUnit);
found.setSourcePosition(ve);
expressions.set(i, found);
} else {
addError(BASE_BAD_PARAM_ERROR + "a list containing an unresolvable reference to '" + ve.getName() + "'.", next);
}
} else if (!(next instanceof ClassExpression)) {
addError(BASE_BAD_PARAM_ERROR + "a list containing type: " + next.getType().getName() + ".", next);
}
}
checkDuplicateNameClashes(list);
} else if (expr != null) {
addError(BASE_BAD_PARAM_ERROR + "a type: " + expr.getType().getName() + ".", expr);
}
return list;
}
private ClassNode getSourceUnitClass(VariableExpression ve) {
List<ClassNode> classes = source.getAST().getClasses();
for (ClassNode classNode : classes) {
if (classNode.getNameWithoutPackage().equals(ve.getName())) return classNode;
}
return null;
}
public Expression transform(Expression expr) {
if (expr == null) return null;
if (expr instanceof MethodCallExpression && candidate == null) {
MethodCallExpression mce = (MethodCallExpression) expr;
Expression args = transform(mce.getArguments());
if (isNewifyCandidate(mce)) {
Expression transformed = transformMethodCall(mce, args);
transformed.setSourcePosition(mce);
return transformed;
}
Expression method = transform(mce.getMethod());
Expression object = transform(mce.getObjectExpression());
MethodCallExpression transformed = callX(object, method, args);
transformed.setImplicitThis(mce.isImplicitThis());
transformed.setSafe(mce.isSafe());
transformed.setSourcePosition(mce);
return transformed;
} else if (expr instanceof ClosureExpression) {
ClosureExpression ce = (ClosureExpression) expr;
ce.getCode().visit(this);
} else if (expr instanceof ConstructorCallExpression) {
ConstructorCallExpression cce = (ConstructorCallExpression) expr;
if (cce.isUsingAnonymousInnerClass()) {
cce.getType().visitContents(this);
}
} else if (expr instanceof DeclarationExpression) {
DeclarationExpression de = (DeclarationExpression) expr;
if (shouldTransform(de)) {
candidate = null;
Expression left = de.getLeftExpression();
Expression right = transform(de.getRightExpression());
DeclarationExpression newDecl = new DeclarationExpression(left, de.getOperation(), right);
newDecl.addAnnotations(de.getAnnotations());
return newDecl;
}
return de;
}
return expr.transformExpression(this);
}
private boolean shouldTransform(DeclarationExpression exp) {
return exp == candidate || auto || hasClassesToNewify();
}
private boolean hasClassesToNewify() {
return (classesToNewify != null && !classesToNewify.getExpressions().isEmpty()) || (classNamePattern != null);
}
private void checkDuplicateNameClashes(ListExpression list) {
final Set<String> seen = new HashSet<String>();
@SuppressWarnings("unchecked")
List<ClassExpression> classes = (List) list.getExpressions();
for (ClassExpression ce : classes) {
final String name = ce.getType().getNameWithoutPackage();
if (seen.contains(name)) {
addError("Duplicate name '" + name + "' found during @" + MY_NAME + " processing.", ce);
}
seen.add(name);
}
}
private void checkAutoClash(boolean autoFlag, AnnotatedNode parent) {
if (auto && !autoFlag) {
addError("Error during @" + MY_NAME + " processing. The 'auto' flag can't be false at " +
"method/constructor/field level if it is true at the class level.", parent);
}
}
private void checkClassLevelClashes(ListExpression list) {
@SuppressWarnings("unchecked")
List<ClassExpression> classes = (List) list.getExpressions();
for (ClassExpression ce : classes) {
final String name = ce.getType().getNameWithoutPackage();
if (findClassWithMatchingBasename(name)) {
addError("Error during @" + MY_NAME + " processing. Class '" + name + "' can't appear at " +
"method/constructor/field level if it already appears at the class level.", ce);
}
}
}
private boolean isNewifyCandidate(MethodCallExpression mce) {
return (auto && isNewMethodStyle(mce)) || (mce.isImplicitThis()
&& mce.getObjectExpression() instanceof VariableExpression
&& ((VariableExpression) mce.getObjectExpression()).isThisExpression());
}
private static boolean isNewMethodStyle(MethodCallExpression mce) {
final Expression obj = mce.getObjectExpression();
final Expression meth = mce.getMethod();
return (obj instanceof ClassExpression && meth instanceof ConstantExpression
&& ((ConstantExpression) meth).getValue().equals("new"));
}
private Expression transformMethodCall(MethodCallExpression mce, Expression argsExp) {
ClassNode classType;
if (isNewMethodStyle(mce)) {
classType = mce.getObjectExpression().getType();
} else {
classType = findMatchingCandidateClass(mce);
}
if (classType != null) {
Expression argsToUse = argsExp;
if (classType.getOuterClass() != null && ((classType.getModifiers() & org.objectweb.asm.Opcodes.ACC_STATIC) == 0)) {
if (!(argsExp instanceof ArgumentListExpression)) {
addError("Non-static inner constructor arguments must be an argument list expression; pass 'this' pointer explicitely as first constructor argument otherwise.", mce);
return mce;
}
final ArgumentListExpression argsListExp = (ArgumentListExpression) argsExp;
final List<Expression> argExpList = argsListExp.getExpressions();
final VariableExpression thisVarExp = new VariableExpression("this");
final List<Expression> expressionsWithThis = new ArrayList<Expression>(argExpList.size() + 1);
expressionsWithThis.add(thisVarExp);
expressionsWithThis.addAll(argExpList);
argsToUse = new ArgumentListExpression(expressionsWithThis);
}
return new ConstructorCallExpression(classType, argsToUse);
}
// set the args as they might have gotten Newify transformed GROOVY-3491
mce.setArguments(argsExp);
return mce;
}
private boolean findClassWithMatchingBasename(String nameWithoutPackage) {
// For performance reasons test against classNamePattern first
if (classNamePattern != null && classNamePattern.matcher(nameWithoutPackage).matches()) {
return true;
}
if (classesToNewify != null) {
@SuppressWarnings("unchecked")
List<ClassExpression> classes = (List) classesToNewify.getExpressions();
for (ClassExpression ce : classes) {
if (ce.getType().getNameWithoutPackage().equals(nameWithoutPackage)) {
return true;
}
}
}
return false;
}
private ClassNode findMatchingCandidateClass(MethodCallExpression mce) {
final String methodName = mce.getMethodAsString();
if (classesToNewify != null) {
@SuppressWarnings("unchecked")
List<ClassExpression> classes = (List) classesToNewify.getExpressions();
for (ClassExpression ce : classes) {
final ClassNode type = ce.getType();
if (type.getNameWithoutPackage().equals(methodName)) {
return type;
}
}
}
if (classNamePattern != null && classNamePattern.matcher(methodName).matches()) {
// One-time-fill inner classes lookup map
if (nameToInnerClassesNodesMap == null) {
final List<ClassNode> innerClassNodes = source.getAST().getClasses();
nameToInnerClassesNodesMap = new HashMap<>(innerClassNodes.size());
for (ClassNode type : innerClassNodes) {
final String pureClassName = extractName(type.getNameWithoutPackage());
final NewifyClassData classData = nameToInnerClassesNodesMap.get(pureClassName);
if (classData == null) {
nameToInnerClassesNodesMap.put(pureClassName, new NewifyClassData(pureClassName, type));
} else {
// If class name is looked up below, additional types will be used in error message
classData.addAdditionalType(type);
}
}
}
// Inner classes
final NewifyClassData innerTypeClassData = nameToInnerClassesNodesMap.get(methodName);
if (innerTypeClassData != null) {
if (innerTypeClassData.types != null) {
addError("Inner class name lookup is ambiguous between the following classes: " + DefaultGroovyMethods.join(innerTypeClassData.types, ", ") + ". Use new keyword and qualify name to break ambiguity.", mce);
return null;
}
return innerTypeClassData.type;
}
// Imported classes
final ClassNode importedType = source.getAST().getImportType(methodName);
if (importedType != null) {
return importedType;
}
// Global classes
final ClassNode globalType = nameToGlobalClassesNodesMap.get(methodName);
if (globalType != null) {
return globalType;
}
}
return null;
}
private static void internalError(String message) {
throw new GroovyBugError("Internal error: " + message);
}
protected SourceUnit getSourceUnit() {
return source;
}
private static class NewifyClassData {
final String name;
final ClassNode type;
List<ClassNode> types;
public NewifyClassData(final String name, final ClassNode type) {
this.name = name;
this.type = type;
}
public void addAdditionalType(final ClassNode additionalType) {
if (types == null) {
types = new LinkedList<>();
types.add(type);
}
types.add(additionalType);
}
}
}