blob: a28cb9e58af5c9ac1d71d5a10bf2e8ce31849d39 [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.tools.javac;
import org.apache.groovy.ast.tools.ExpressionUtils;
import org.apache.groovy.io.StringBuilderWriter;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.ConstructorNode;
import org.codehaus.groovy.ast.DynamicVariable;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.GenericsType;
import org.codehaus.groovy.ast.ImportNode;
import org.codehaus.groovy.ast.InnerClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.ModuleNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.PropertyNode;
import org.codehaus.groovy.ast.Variable;
import org.codehaus.groovy.ast.decompiled.DecompiledClassNode;
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.Expression;
import org.codehaus.groovy.ast.expr.ListExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
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.FinalVariableAnalyzer;
import org.codehaus.groovy.classgen.Verifier;
import org.codehaus.groovy.classgen.VerifierCodeVisitor;
import org.codehaus.groovy.runtime.FormatHelper;
import org.codehaus.groovy.tools.Utilities;
import org.codehaus.groovy.transform.trait.Traits;
import org.objectweb.asm.Opcodes;
import javax.tools.FileObject;
import javax.tools.JavaFileObject;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.apache.groovy.ast.tools.AnnotatedNodeUtils.markAsGenerated;
import static org.apache.groovy.ast.tools.ConstructorNodeUtils.getFirstIfSpecialConstructorCall;
import static org.codehaus.groovy.ast.ClassHelper.CLASS_Type;
import static org.codehaus.groovy.ast.ClassHelper.getUnwrapper;
import static org.codehaus.groovy.ast.ClassHelper.getWrapper;
import static org.codehaus.groovy.ast.ClassHelper.isBigDecimalType;
import static org.codehaus.groovy.ast.ClassHelper.isCachedType;
import static org.codehaus.groovy.ast.ClassHelper.isClassType;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveBoolean;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveByte;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveChar;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveDouble;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveFloat;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveInt;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveLong;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveShort;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveType;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveVoid;
import static org.codehaus.groovy.ast.ClassHelper.isStaticConstantInitializerType;
import static org.codehaus.groovy.ast.ClassHelper.isStringType;
import static org.codehaus.groovy.ast.tools.GeneralUtils.defaultValueX;
import static org.codehaus.groovy.ast.tools.GenericsUtils.correctToGenericsSpec;
import static org.codehaus.groovy.ast.tools.GenericsUtils.correctToGenericsSpecRecurse;
import static org.codehaus.groovy.ast.tools.GenericsUtils.createGenericsSpec;
import static org.codehaus.groovy.ast.tools.WideningCategories.isFloatingCategory;
import static org.codehaus.groovy.ast.tools.WideningCategories.isLongCategory;
public class JavaStubGenerator {
private final String encoding;
private final boolean requireSuperResolved;
private final File outputPath;
private final List<ConstructorNode> constructors = new ArrayList<>();
private final Map<String, MethodNode> propertyMethods = new LinkedHashMap<>();
private ModuleNode currentModule;
public JavaStubGenerator(final File outputPath) {
this(outputPath, false, Charset.defaultCharset().name());
}
public JavaStubGenerator(final File outputPath, final boolean requireSuperResolved, final String encoding) {
this.requireSuperResolved = requireSuperResolved;
this.outputPath = outputPath;
this.encoding = encoding;
// when outputPath is null, generate stubs in memory
if (outputPath != null) outputPath.mkdirs();
}
private static void mkdirs(final File parent, final String relativeFile) {
int index = relativeFile.lastIndexOf('/');
if (index == -1) return;
File dir = new File(parent, relativeFile.substring(0, index));
dir.mkdirs();
}
public void generateClass(final ClassNode classNode) throws FileNotFoundException {
// only attempt to render if super-class is resolved; else wait for it
if (requireSuperResolved && !classNode.getSuperClass().isResolved()) {
return;
}
// skip private class; they are not visible outside the file
if ((classNode.getModifiers() & Opcodes.ACC_PRIVATE) != 0) {
return;
}
if (classNode instanceof InnerClassNode) {
return;
}
if (outputPath == null) {
generateMemStub(classNode);
} else {
generateFileStub(classNode);
}
}
private void generateMemStub(ClassNode classNode) {
javaStubCompilationUnitSet.add(new MemJavaFileObject(classNode, generateStubContent(classNode)));
}
private void generateFileStub(ClassNode classNode) throws FileNotFoundException {
String fileName = classNode.getName().replace('.', '/');
mkdirs(outputPath, fileName);
File file = createJavaStubFile(fileName);
Writer writer = new OutputStreamWriter(
new FileOutputStream(file),
Charset.forName(encoding)
);
try (PrintWriter out = new PrintWriter(writer)) {
out.print(generateStubContent(classNode));
}
javaStubCompilationUnitSet.add(new RawJavaFileObject(createJavaStubFile(fileName).toPath().toUri()));
}
private String generateStubContent(ClassNode classNode) {
Writer writer = new StringBuilderWriter(8192);
try (PrintWriter out = new PrintWriter(writer)) {
boolean packageInfo = "package-info".equals(classNode.getNameWithoutPackage());
String packageName = classNode.getPackageName();
currentModule = classNode.getModule();
if (packageName != null) {
if (packageInfo) {
printAnnotations(out, classNode.getPackage());
}
out.println("package " + packageName + ";");
}
// should just output the package statement for `package-info` class node
if (!packageInfo) {
printImports(out);
printClassContents(out, classNode);
}
} finally {
currentModule = null;
}
return writer.toString();
}
private static Iterable<ClassNode> findTraits(ClassNode node) {
Set<ClassNode> traits = new LinkedHashSet<>();
LinkedList<ClassNode> todo = new LinkedList<>();
Collections.addAll(todo, node.getInterfaces());
while (!todo.isEmpty()) {
ClassNode next = todo.removeLast();
if (Traits.isTrait(next)) traits.add(next);
Collections.addAll(todo, next.getInterfaces());
}
return traits;
}
private void printClassContents(PrintWriter out, ClassNode classNode) {
if (classNode instanceof InnerClassNode && ((InnerClassNode) classNode).isAnonymous()) {
// if it is an anonymous inner class, don't generate the stub code for it.
return;
}
try {
Verifier verifier = new Verifier() {
@Override
public void visitClass(ClassNode node) {
List<Statement> savedStatements = new ArrayList<>(node.getObjectInitializerStatements());
super.visitClass(node);
node.getObjectInitializerStatements().addAll(savedStatements);
for (ClassNode trait : findTraits(node)) {
// GROOVY-9031: replace property type placeholder with resolved type from trait generics
Map<String, ClassNode> generics = trait.isUsingGenerics() ? createGenericsSpec(trait) : null;
for (PropertyNode traitProperty : trait.getProperties()) {
ClassNode traitPropertyType = traitProperty.getType();
traitProperty.setType(correctToGenericsSpecRecurse(generics, traitPropertyType));
super.visitProperty(traitProperty);
traitProperty.setType(traitPropertyType);
}
}
}
@Override
public void visitConstructor(ConstructorNode node) {
Statement stmt = node.getCode();
if (stmt != null) {
stmt.visit(new VerifierCodeVisitor(getClassNode()));
}
}
@Override
public void visitProperty(PropertyNode pn) {
// GROOVY-8233 skip static properties for traits since they don't make the interface
if (!pn.isStatic() || !Traits.isTrait(pn.getDeclaringClass())) {
super.visitProperty(pn);
}
}
@Override
public void addCovariantMethods(ClassNode cn) {}
@Override
protected void addInitialization(ClassNode cn) {}
@Override
protected void addInitialization(ClassNode cn, ConstructorNode c) {}
@Override
protected void addPropertyMethod(MethodNode mn) {
doAddMethod(mn);
}
@Override
protected void addReturnIfNeeded(MethodNode mn) {}
@Override
protected MethodNode addMethod(ClassNode cn, boolean shouldBeSynthetic, String name, int modifiers, ClassNode returnType, Parameter[] parameters, ClassNode[] exceptions, Statement code) {
return doAddMethod(new MethodNode(name, modifiers, returnType, parameters, exceptions, code));
}
@Override
protected void addConstructor(Parameter[] params, ConstructorNode ctor, Statement code, ClassNode node) {
if (!(code instanceof BlockStatement)) { // GROOVY-4508
Statement stmt = code;
code = new BlockStatement();
((BlockStatement) code).addStatement(stmt);
}
ConstructorNode newCtor = new ConstructorNode(ctor.getModifiers(), params, ctor.getExceptions(), code);
newCtor.setDeclaringClass(node);
markAsGenerated(node, newCtor);
constructors.add(newCtor);
}
@Override
protected void addDefaultParameters(DefaultArgsAction action, MethodNode method) {
final Parameter[] parameters = method.getParameters();
final Expression[] saved = new Expression[parameters.length];
for (int i = 0; i < parameters.length; i++) {
if (parameters[i].hasInitialExpression())
saved[i] = parameters[i].getInitialExpression();
}
super.addDefaultParameters(action, method);
for (int i = 0; i < parameters.length; i++) {
if (saved[i] != null)
parameters[i].setInitialExpression(saved[i]);
}
}
private MethodNode doAddMethod(MethodNode method) {
propertyMethods.putIfAbsent(method.getTypeDescriptor(), method);
return method;
}
@Override
protected void addDefaultConstructor(ClassNode node) {
// not required for stub generation
}
@Override
protected FinalVariableAnalyzer.VariableNotFinalCallback getFinalVariablesCallback() {
return null;
}
};
int origNumConstructors = classNode.getDeclaredConstructors().size();
verifier.visitClass(classNode);
// undo unwanted side effect of verifier
if (origNumConstructors == 0 && classNode.getDeclaredConstructors().size() == 1) {
classNode.getDeclaredConstructors().clear();
}
boolean isInterface = isInterfaceOrTrait(classNode);
boolean isEnum = classNode.isEnum();
boolean isAnnotationDefinition = classNode.isAnnotationDefinition();
printAnnotations(out, classNode);
printModifiers(out, classNode.getModifiers()
& ~(isInterface ? Opcodes.ACC_ABSTRACT : 0)
& ~(isEnum ? Opcodes.ACC_FINAL | Opcodes.ACC_ABSTRACT : 0));
if (isInterface) {
if (isAnnotationDefinition) {
out.print("@");
}
out.print("interface ");
} else if (isEnum) {
out.print("enum ");
} else {
out.print("class ");
}
String className = classNode.getNameWithoutPackage();
if (classNode instanceof InnerClassNode)
className = className.substring(className.lastIndexOf('$') + 1);
out.println(className);
printTypeParameters(out, classNode.getGenericsTypes());
ClassNode superClass = classNode.getUnresolvedSuperClass(false);
if (!isInterface && !isEnum) {
out.print(" extends ");
printType(out, superClass);
}
ClassNode[] interfaces = classNode.getInterfaces();
if (interfaces != null && interfaces.length > 0 && !isAnnotationDefinition) {
if (isInterface) {
out.println(" extends");
} else {
out.println(" implements");
}
for (int i = 0; i < interfaces.length - 1; ++i) {
out.print(" ");
printType(out, interfaces[i]);
out.print(",");
}
out.print(" ");
printType(out, interfaces[interfaces.length - 1]);
}
out.println(" {");
printFields(out, classNode);
printMethods(out, classNode, isEnum);
for (Iterator<InnerClassNode> inner = classNode.getInnerClasses(); inner.hasNext(); ) {
// GROOVY-4004: Clear the methods from the outer class so that they don't get duplicated in inner ones
constructors.clear();
propertyMethods.clear();
printClassContents(out, inner.next());
}
out.println("}");
} finally {
constructors.clear();
propertyMethods.clear();
}
}
private void printMethods(PrintWriter out, ClassNode classNode, boolean isEnum) {
if (!isEnum) printConstructors(out, classNode);
List<MethodNode> methods = new ArrayList<>(propertyMethods.values());
methods.addAll(classNode.getMethods());
for (MethodNode method : methods) {
if (isEnum && method.isSynthetic()) {
// skip values() method and valueOf(String)
String name = method.getName();
Parameter[] params = method.getParameters();
if (params.length == 0 && name.equals("values")) continue;
if (params.length == 1
&& name.equals("valueOf")
&& isStringType(params[0].getType())) {
continue;
}
}
printMethod(out, classNode, method);
}
// print the methods from traits
for (ClassNode trait : findTraits(classNode)) {
Map<String, ClassNode> generics = trait.isUsingGenerics() ? createGenericsSpec(trait) : null;
List<MethodNode> traitMethods = trait.getMethods();
for (MethodNode traitOrigMethod : traitMethods) {
// GROOVY-9606: replace method return type and parameter type placeholder with resolved type from trait generics
MethodNode traitMethod = correctToGenericsSpec(generics, traitOrigMethod);
MethodNode existingMethod = classNode.getMethod(traitMethod.getName(), traitMethod.getParameters());
if (existingMethod != null) continue;
for (MethodNode propertyMethod : propertyMethods.values()) {
if (propertyMethod.getName().equals(traitMethod.getName())) {
boolean sameParams = sameParameterTypes(propertyMethod, traitMethod);
if (sameParams) {
existingMethod = propertyMethod;
break;
}
}
}
if (existingMethod == null && isConcreteTraitMethod(trait, traitMethod)) {
printMethod(out, classNode, traitMethod);
}
}
}
}
private static boolean isConcreteTraitMethod(final ClassNode trait, final MethodNode traitMethod) {
if (!(trait.redirect() instanceof DecompiledClassNode)) {
return !traitMethod.isAbstract();
}
boolean isSynthetic = (traitMethod.getModifiers() & Opcodes.ACC_SYNTHETIC) != 0;
if (!isSynthetic && !traitMethod.getName().contains("$")) {
for (MethodNode helperMethod : Traits.findHelper(trait).getMethods(traitMethod.getName())) {
Parameter[] params = helperMethod.getParameters();
params = Arrays.copyOfRange(params, 1, params.length);
if (sameParameterTypes(params, traitMethod.getParameters())) return true;
}
}
return false;
}
private static boolean sameParameterTypes(final MethodNode firstMethod, final MethodNode secondMethod) {
return sameParameterTypes(firstMethod.getParameters(), secondMethod.getParameters());
}
private static boolean sameParameterTypes(final Parameter[] firstParams, final Parameter[] secondParams) {
return org.codehaus.groovy.ast.tools.ParameterUtils.parametersEqual(firstParams, secondParams);
}
private void printConstructors(final PrintWriter out, final ClassNode classNode) {
List<ConstructorNode> constructors = new ArrayList<>(this.constructors);
constructors.addAll(classNode.getDeclaredConstructors());
for (ConstructorNode constructor : constructors) {
printConstructor(out, classNode, constructor);
}
}
private void printFields(final PrintWriter out, final ClassNode classNode) {
List<FieldNode> fields = classNode.getFields();
if (!fields.isEmpty()) {
List<FieldNode> enumFields = new LinkedList<>();
List<FieldNode> normalFields = new LinkedList<>();
for (FieldNode field : fields) {
if (field.isEnum()) {
enumFields.add(field);
} else if (!field.isPrivate() && (field.getModifiers() & Opcodes.ACC_SYNTHETIC) == 0) {
normalFields.add(field);
}
}
boolean interfaceOrTrait = isInterfaceOrTrait(classNode);
printEnumFields(out, enumFields);
for (FieldNode normalField : normalFields) {
printField(out, normalField, interfaceOrTrait);
}
}
}
private static void printEnumFields(final PrintWriter out, final List<FieldNode> fields) {
if (!fields.isEmpty()) {
int i = 0;
for (FieldNode field : fields) {
if (i++ != 0) {
out.print(", ");
}
out.print(field.getName());
}
}
out.println(';');
}
private void printField(final PrintWriter out, final FieldNode field, final boolean fromFaceOrTrait) {
printAnnotations(out, field);
if (!fromFaceOrTrait) {
printModifiers(out, field.getModifiers());
}
ClassNode type = field.getType();
printType(out, type);
out.print(' ');
out.print(field.getName());
if (fromFaceOrTrait || field.isFinal()) {
out.print(" = ");
if (field.isStatic()) {
Expression value = ExpressionUtils.transformInlineConstants(field.getInitialValueExpression(), type);
if (value instanceof ConstantExpression) {
if (isPrimitiveType(type)) { // do not pass string of length 1 for String field:
value = Verifier.transformToPrimitiveConstantIfPossible((ConstantExpression) value);
}
if ((type.equals(value.getType()) // GROOVY-10611: integer/decimal value
|| (isLongCategory(type) && isPrimitiveInt(value.getType()))
|| (isFloatingCategory(type) && isBigDecimalType(value.getType())))
&& (isPrimitiveBoolean(type) || isStaticConstantInitializerType(type))) {
printValue(out, type, value);
out.println(';');
return;
}
}
// GROOVY-5150, GROOVY-10902, GROOVY-10928, GROOVY-11019: dummy value that prevents inlining
if (isPrimitiveType(type) || isStringType(type)) {
out.print("new " + getWrapper(type) + "(");
printValue(out, type, defaultValueX(type));
out.print(')');
} else {
out.print("null");
}
} else if (isPrimitiveType(type)) {
if (isPrimitiveBoolean(type)) {
out.print("false");
} else {
out.print('(');
printTypeName(out, type);
out.print(')');
out.print('0');
}
} else {
out.print("null");
}
}
out.println(';');
}
private void printConstructor(PrintWriter out, ClassNode clazz, ConstructorNode constructorNode) {
printAnnotations(out, constructorNode);
// printModifiers(out, constructorNode.getModifiers());
out.print("public "); // temporary hack
String className = clazz.getNameWithoutPackage();
if (clazz instanceof InnerClassNode)
className = className.substring(className.lastIndexOf('$') + 1);
out.println(className);
printParams(out, constructorNode);
ClassNode[] exceptions = constructorNode.getExceptions();
printExceptions(out, exceptions);
ConstructorCallExpression constrCall = getFirstIfSpecialConstructorCall(constructorNode.getCode());
if (constrCall == null) {
out.println(" {}");
} else {
out.println(" {");
printSpecialConstructorArgs(out, constructorNode, constrCall);
out.println("}");
}
}
private static Parameter[] selectAccessibleConstructorFromSuper(final ConstructorNode source) {
ClassNode superType = source.getDeclaringClass().getUnresolvedSuperClass();
Map<String, ClassNode> superTypeGenerics = createGenericsSpec(superType);
Parameter[] bestMatch = null;
for (ConstructorNode target : superType.getDeclaredConstructors()) {
// only look at things we can actually call
// TODO: package-private and types are peers
if (!target.isPublic() && !target.isProtected()) continue;
Parameter[] parameters = target.getParameters();
Parameter[] normalized = Arrays.stream(parameters).map(parameter -> {
ClassNode normalizedType = parameter.getOriginType();
if (superType.getGenericsTypes() == null // GROOVY-10407
&& superType.redirect().getGenericsTypes() != null) {
// GROOVY-5859: remove generic type info for raw type
normalizedType = normalizedType.getPlainNodeReference();
} else {
// GROOVY-7306: apply type arguments from declaring type to parameter type
normalizedType = correctToGenericsSpecRecurse(superTypeGenerics, normalizedType);
}
return new Parameter(normalizedType, parameter.getName());
}).toArray(Parameter[]::new);
if (noExceptionToAvoid(source, target)) return normalized;
if (bestMatch == null) bestMatch = normalized;
}
if (bestMatch != null) return bestMatch;
// fall back for parameterless constructor
if (superType.isPrimaryClassNode()) {
return Parameter.EMPTY_ARRAY;
}
return null;
}
private static boolean noExceptionToAvoid(ConstructorNode fromStub, ConstructorNode fromSuper) {
ClassNode[] superExceptions = fromSuper.getExceptions();
if (superExceptions==null || superExceptions.length==0) return true;
ClassNode[] stubExceptions = fromStub.getExceptions();
if (stubExceptions==null || stubExceptions.length==0) return false;
// if all remaining exceptions are used in the stub we are good
outer:
for (ClassNode superExc : superExceptions) {
for (ClassNode stub : stubExceptions) {
if (stub.isDerivedFrom(superExc)) continue outer;
}
// not found
return false;
}
return true;
}
private void printSpecialConstructorArgs(final PrintWriter out, final ConstructorNode ctor, final ConstructorCallExpression ctorCall) {
// Select a constructor from our class, or super-class which is legal to call,
// then write out an invoke w/nulls using casts to avoid ambiguous calls
Parameter[] params = selectAccessibleConstructorFromSuper(ctor);
if (params != null) {
out.print("super (");
for (int i = 0, n = params.length; i < n; i += 1) {
printDefaultValue(out, params[i].getType());
if (i + 1 < n) {
out.print(", ");
}
}
out.println(");");
return;
}
// Otherwise try the older method based on the constructor's call expression
Expression arguments = ctorCall.getArguments();
if (ctorCall.isSuperCall()) {
out.print("super(");
} else {
out.print("this(");
}
if (arguments instanceof ArgumentListExpression) {
List<Expression> args = ((ArgumentListExpression) arguments).getExpressions();
int i = 0, n = args.size();
for (Expression arg : args) {
if (arg instanceof ConstantExpression) {
Object value = ((ConstantExpression) arg).getValue();
if (value instanceof String) {
out.print("(String)null");
} else {
out.print(arg.getText());
}
} else {
printDefaultValue(out, getConstructorArgumentType(arg, ctor));
}
if (++i < n) {
out.print(", ");
}
}
}
out.println(");");
}
private static ClassNode getConstructorArgumentType(final Expression arg, final ConstructorNode ctor) {
if (arg instanceof VariableExpression) {
Variable variable = ((VariableExpression) arg).getAccessedVariable();
if (variable instanceof DynamicVariable) { // GROOVY-10464
return CLASS_Type.getPlainNodeReference();
}
return variable.getType(); // field, property, parameter
}
if (arg instanceof PropertyExpression) {
if ("class".equals(((PropertyExpression) arg).getPropertyAsString())) {
return CLASS_Type.getPlainNodeReference();
}
return null;
}
if (arg instanceof MethodCallExpression) { // GROOVY-10122
MethodCallExpression mce = (MethodCallExpression) arg;
if (ExpressionUtils.isThisExpression(mce.getObjectExpression())) {
MethodNode mn = ctor.getDeclaringClass().tryFindPossibleMethod(mce.getMethodAsString(), mce.getArguments());
if (mn != null) return mn.getReturnType();
}
return null;
}
return arg.getType();
}
private void printMethod(final PrintWriter out, final ClassNode classNode, final MethodNode methodNode) {
if (methodNode.isStaticConstructor()) return;
if (methodNode.isPrivate() || !Utilities.isJavaIdentifier(methodNode.getName())) return;
if (methodNode.isSynthetic() && (methodNode.getName().equals("$getStaticMetaClass") || methodNode.getName().equals("$getLookup"))) return;
printAnnotations(out, methodNode);
if (!isInterfaceOrTrait(classNode)) {
int modifiers = methodNode.getModifiers();
if (isDefaultTraitImpl(methodNode)) {
modifiers ^= Opcodes.ACC_ABSTRACT;
}
printModifiers(out, modifiers & ~(classNode.isEnum() ? Opcodes.ACC_ABSTRACT : 0));
}
printTypeParameters(out, methodNode.getGenericsTypes());
out.print(" ");
printType(out, methodNode.getReturnType());
out.print(" ");
out.print(methodNode.getName());
printParams(out, methodNode);
ClassNode[] exceptions = methodNode.getExceptions();
printExceptions(out, exceptions);
boolean skipBody = (isAbstract(methodNode) && !classNode.isEnum()) || Traits.isTrait(classNode);
if (!skipBody) {
ClassNode type = methodNode.getReturnType();
if (isPrimitiveBoolean(type)) {
out.println(" { return false; }");
} else if (!isPrimitiveType(type)) {
out.println(" { return null; }");
} else if (!isPrimitiveVoid(type)) {
out.println(" { return 0; }");
} else { // void return type
out.println(" { }");
}
} else {
if (classNode.isAnnotationDefinition() && methodNode.hasAnnotationDefault()) {
Statement fs = methodNode.getFirstStatement();
if (fs instanceof ExpressionStatement) {
ExpressionStatement es = (ExpressionStatement) fs;
Expression re = es.getExpression();
ClassNode rt = methodNode.getReturnType();
Consumer<Expression> valuePrinter = (value) -> {
if (isClassType(rt) || (rt.isArray() && isClassType(rt.getComponentType()))) {
if (value.getType().getName().equals("groovy.lang.Closure")) {
out.print("groovy.lang.Closure.class");
return;
}
String valueText = value.getText();
out.print(valueText);
if (!valueText.endsWith(".class")) {
out.print(".class");
}
} else if (value instanceof ConstantExpression) {
printValue(out, rt, value);
} else {
out.print(value.getText());
}
};
out.print(" default ");
if (re instanceof ListExpression) {
out.print("{ ");
ListExpression le = (ListExpression) re;
boolean first = true;
for (Expression e : le.getExpressions()) {
if (first) first = false; else out.print(", ");
valuePrinter.accept(e);
}
out.print(" }");
} else {
valuePrinter.accept(re);
}
}
}
out.println(";");
}
}
private void printExceptions(PrintWriter out, ClassNode[] exceptions) {
for (int i = 0; i < exceptions.length; i++) {
ClassNode exception = exceptions[i];
if (i == 0) {
out.print("throws ");
} else {
out.print(", ");
}
printType(out, exception);
}
}
private static boolean isAbstract(final MethodNode methodNode) {
return methodNode.isAbstract() && !isDefaultTraitImpl(methodNode);
}
private static boolean isDefaultTraitImpl(final MethodNode methodNode) {
return Traits.isTrait(methodNode.getDeclaringClass()) && Traits.hasDefaultImplementation(methodNode);
}
private void printValue(final PrintWriter out, final ClassNode type, final Expression value) {
ClassNode valueType = getUnwrapper(value.getType());
if (isPrimitiveChar(valueType)) {
out.print("'");
out.print(escapeSpecialChars(value.getText().substring(0, 1)));
out.print("'");
} else if (isStringType(valueType)) {
out.print('"');
out.print(escapeSpecialChars(value.getText()));
out.print('"');
} else if (isPrimitiveDouble(valueType)) {
out.print(value.getText());
out.print('d');
} else if (isPrimitiveFloat(valueType)) {
out.print(value.getText());
out.print('f');
} else if (isPrimitiveLong(valueType)) {
out.print(value.getText());
out.print('L');
} else {
if (!isPrimitiveInt(valueType) && !isPrimitiveBoolean(valueType) && !isBigDecimalType(valueType)) {
out.print('(');
printType(out, type); // GROOVY-11019
out.print(')');
}
out.print(value.getText());
}
}
private void printDefaultValue(final PrintWriter out, final ClassNode type) {
if (type != null && !isPrimitiveBoolean(type)) {
out.print("(");
printType(out, type);
out.print(")");
}
if (type != null && isPrimitiveType(type)) {
if (isPrimitiveBoolean(type)) {
out.print("false");
} else {
out.print("0");
}
} else {
out.print("null");
}
}
private void printType(final PrintWriter out, final ClassNode type) {
if (type.isArray()) {
printType(out, type.getComponentType());
out.print("[]");
} else if (type.isGenericsPlaceHolder()) {
out.print(type.getUnresolvedName());
} else {
printTypeName(out, type);
if (!isCachedType(type)) {
printGenericsBounds(out, type.getGenericsTypes());
}
}
}
private String getTypeName(final ClassNode type) {
String name = type.getName();
// check for an alias
ClassNode alias = currentModule.getImportType(name);
if (alias != null) name = alias.getName();
return name.replace('$', '.');
}
private void printTypeName(final PrintWriter out, final ClassNode type) {
if (isPrimitiveType(type)) {
if (isPrimitiveBoolean(type)) {
out.print("boolean");
} else if (isPrimitiveChar(type)) {
out.print("char");
} else if (isPrimitiveInt(type)) {
out.print("int");
} else if (isPrimitiveShort(type)) {
out.print("short");
} else if (isPrimitiveLong(type)) {
out.print("long");
} else if (isPrimitiveFloat(type)) {
out.print("float");
} else if (isPrimitiveDouble(type)) {
out.print("double");
} else if (isPrimitiveByte(type)) {
out.print("byte");
} else {
out.print("void");
}
} else {
out.print(getTypeName(type));
}
}
private void printTypeParameters(final PrintWriter out, final GenericsType[] typeParams) {
if (typeParams == null || typeParams.length == 0) return;
StringJoiner sj = new StringJoiner(", ", "<", ">");
for (GenericsType tp : typeParams) {
ClassNode[] bounds = tp.getUpperBounds();
if (bounds == null || bounds.length == 0) {
sj.add(tp.getName());
} else {
sj.add(tp.getName() + " extends " + Arrays.stream(bounds).map(cn -> {
GenericsType[] typeArguments = cn.getGenericsTypes();
if (typeArguments == null) return getTypeName(cn);
StringJoiner parameterized = new StringJoiner(", ", getTypeName(cn) + "<", ">");
for (GenericsType ta : typeArguments) {
parameterized.add(ta.toString().replace('$', '.'));
}
return parameterized.toString();
}).collect(Collectors.joining(" & ")));
}
}
out.print(sj.toString());
}
private static void printGenericsBounds(final PrintWriter out, final GenericsType[] genericsTypes) {
if (genericsTypes == null || genericsTypes.length == 0) return;
StringJoiner sj = new StringJoiner(", ", "<", ">");
for (GenericsType gt : genericsTypes) {
sj.add(gt.toString().replace('$', '.'));
}
out.print(sj.toString());
}
private void printParams(final PrintWriter out, final MethodNode methodNode) {
out.print("(");
Parameter[] parameters = methodNode.getParameters();
if (parameters != null && parameters.length != 0) {
int lastIndex = parameters.length - 1;
boolean vararg = parameters[lastIndex].getType().isArray();
for (int i = 0; i != parameters.length; ++i) {
printAnnotations(out, parameters[i]);
if (i == lastIndex && vararg) {
printType(out, parameters[i].getType().getComponentType());
out.print("...");
} else {
printType(out, parameters[i].getType());
}
out.print(" ");
out.print(parameters[i].getName());
if (i + 1 < parameters.length) {
out.print(", ");
}
}
}
out.print(")");
}
private void printAnnotations(final PrintWriter out, final AnnotatedNode annotated) {
for (AnnotationNode annotation : annotated.getAnnotations()) {
printAnnotation(out, annotation);
}
}
private void printAnnotation(final PrintWriter out, final AnnotationNode annotation) {
StringJoiner sj = new StringJoiner(", ", "@" + annotation.getClassNode().getName().replace('$', '.') + "(", ") ");
for (Map.Entry<String, Expression> entry : annotation.getMembers().entrySet()) {
sj.add(entry.getKey() + "=" + getAnnotationValue(entry.getValue()));
}
out.print(sj.toString());
}
private String getAnnotationValue(final Object memberValue) {
String val = "null";
boolean replaceDollars = true;
if (memberValue instanceof ListExpression) {
StringJoiner sj = new StringJoiner(",", "{", "}");
ListExpression le = (ListExpression) memberValue;
for (Expression e : le.getExpressions()) {
sj.add(getAnnotationValue(e));
}
val = sj.toString();
} else if (memberValue instanceof ConstantExpression) {
ConstantExpression ce = (ConstantExpression) memberValue;
Object constValue = ce.getValue();
if (constValue instanceof AnnotationNode) {
Writer writer = new StringBuilderWriter();
PrintWriter out = new PrintWriter(writer);
printAnnotation(out, (AnnotationNode) constValue);
val = writer.toString();
} else if (constValue instanceof Number || constValue instanceof Boolean) {
val = constValue.toString();
} else if (constValue instanceof Enum) {
val = constValue.getClass().getName() + "." + constValue.toString();
} else {
val = "\"" + escapeSpecialChars(constValue.toString()) + "\"";
replaceDollars = false;
}
} else if (memberValue instanceof PropertyExpression) {
// assume must be static class field or enum value or class that Java can resolve
val = ((Expression) memberValue).getText();
} else if (memberValue instanceof VariableExpression) {
val = ((Expression) memberValue).getText();
// check for an alias
ImportNode alias = currentModule.getStaticImports().get(val);
if (alias != null)
val = alias.getClassName() + "." + alias.getFieldName();
} else if (memberValue instanceof ClosureExpression) {
// annotation closure; replaced with this specific class literal to cover the
// case where annotation type uses Class<? extends Closure> for the closure's type
val = "groovy.lang.Closure.class";
} else if (memberValue instanceof ClassExpression) {
val = ((Expression) memberValue).getText() + ".class";
}
return replaceDollars ? val.replace('$', '.') : val;
}
private static void printModifiers(PrintWriter out, int modifiers) {
if ((modifiers & Opcodes.ACC_PUBLIC) != 0)
out.print("public ");
if ((modifiers & Opcodes.ACC_PROTECTED) != 0)
out.print("protected ");
if ((modifiers & Opcodes.ACC_PRIVATE) != 0)
out.print("private ");
if ((modifiers & Opcodes.ACC_STATIC) != 0)
out.print("static ");
if ((modifiers & Opcodes.ACC_SYNCHRONIZED) != 0)
out.print("synchronized ");
if ((modifiers & Opcodes.ACC_FINAL) != 0)
out.print("final ");
if ((modifiers & Opcodes.ACC_ABSTRACT) != 0)
out.print("abstract ");
}
private void printImports(final PrintWriter out) {
Map<String, ImportNode> staticImports = currentModule.getStaticImports();
Map<String, ImportNode> staticStarImports = currentModule.getStaticStarImports();
for (Map.Entry<String, ImportNode> entry : staticImports.entrySet()) {
String memberName = entry.getKey();
if (memberName.equals(entry.getValue().getFieldName())
&& !Character.isLowerCase(memberName.charAt(0))) // GROOVY-7510
out.println("import static " + entry.getValue().getType().getName().replace('$', '.') + "." + memberName + ";");
}
for (ImportNode ssi : staticStarImports.values()) {
out.println("import static " + ssi.getType().getName().replace('$', '.') + ".*;");
}
out.println();
}
public void clean() {
Stream<JavaFileObject> javaFileObjectStream =
javaStubCompilationUnitSet.size() < 2
? javaStubCompilationUnitSet.stream()
: javaStubCompilationUnitSet.parallelStream();
javaFileObjectStream.forEach(FileObject::delete);
javaStubCompilationUnitSet.clear();
}
private File createJavaStubFile(String path) {
return new File(outputPath, path + ".java");
}
private static String escapeSpecialChars(String value) {
return FormatHelper.escapeBackslashes(value).replace("\"", "\\\"");
}
private static boolean isInterfaceOrTrait(final ClassNode cn) {
return cn.isInterface() || Traits.isTrait(cn);
}
private final Set<JavaFileObject> javaStubCompilationUnitSet = new HashSet<>();
public Set<JavaFileObject> getJavaStubCompilationUnitSet() {
return javaStubCompilationUnitSet;
}
}