blob: 4e1376ea0eb1521b57d439b6847b23a1993533b1 [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.ast;
import groovy.lang.Binding;
import org.codehaus.groovy.ast.expr.ArgumentListExpression;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
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.GeneratorContext;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.transform.BaseScriptASTTransformation;
import org.objectweb.asm.Opcodes;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* Represents a module, which consists typically of a class declaration
* but could include some imports, some statements and multiple classes
* intermixed with statements like scripts in Python or Ruby
*/
public class ModuleNode extends ASTNode implements Opcodes {
private List<ClassNode> classes = new LinkedList<>();
private final List<MethodNode> methods = new ArrayList<>();
private final Map<String, ImportNode> imports = new HashMap<>();
private final List<ImportNode> starImports = new ArrayList<>();
private final Map<String, ImportNode> staticImports = new LinkedHashMap<>();
private final Map<String, ImportNode> staticStarImports = new LinkedHashMap<>();
private CompileUnit unit;
private PackageNode packageNode;
private String description;
private boolean createClassForStatements = true;
private transient SourceUnit context;
private boolean importsResolved;
private ClassNode scriptDummy;
private String mainClassName;
private final BlockStatement statementBlock = new BlockStatement();
private final Parameter[] SCRIPT_CONTEXT_CTOR = {new Parameter(ClassHelper.BINDING_TYPE, "context")};
public ModuleNode (SourceUnit context ) {
this.context = context;
}
public ModuleNode (CompileUnit unit) {
this.unit = unit;
}
public BlockStatement getStatementBlock() {
return statementBlock;
}
public List<MethodNode> getMethods() {
return methods;
}
public List<ClassNode> getClasses() {
if (createClassForStatements && (!statementBlock.isEmpty() || !methods.isEmpty() || isPackageInfo())) {
ClassNode mainClass = createStatementsClass();
mainClassName = mainClass.getName();
createClassForStatements = false;
classes.add(0, mainClass);
mainClass.setModule(this);
addToCompileUnit(mainClass);
}
return classes;
}
private boolean isPackageInfo() {
return context != null && context.getName() != null && context.getName().endsWith("package-info.groovy");
}
public List<ImportNode> getImports() {
return new ArrayList<>(imports.values());
}
public List<ImportNode> getStarImports() {
return starImports;
}
/**
* @param alias the name of interest
* @return the class node for the given alias or null if none is available
*/
public ClassNode getImportType(String alias) {
ImportNode importNode = imports.get(alias);
return importNode == null ? null : importNode.getType();
}
/**
* @param alias the name of interest
* @return the import node for the given alias or null if none is available
*/
public ImportNode getImport(String alias) {
return imports.get(alias);
}
public void addImport(String alias, ClassNode type) {
addImport(alias, type, Collections.emptyList());
}
public void addImport(String alias, ClassNode type, List<AnnotationNode> annotations) {
ImportNode importNode = new ImportNode(type, alias);
imports.put(alias, importNode);
importNode.addAnnotations(annotations);
storeLastAddedImportNode(importNode);
}
public void addStarImport(String packageName) {
addStarImport(packageName, Collections.emptyList());
}
public void addStarImport(String packageName, List<AnnotationNode> annotations) {
ImportNode importNode = new ImportNode(packageName);
importNode.addAnnotations(annotations);
starImports.add(importNode);
storeLastAddedImportNode(importNode);
}
public void addStaticImport(ClassNode type, String fieldName, String alias) {
addStaticImport(type, fieldName, alias, Collections.emptyList());
}
public void addStaticImport(ClassNode type, String fieldName, String alias, List<AnnotationNode> annotations) {
ImportNode node = new ImportNode(type, fieldName, alias);
node.addAnnotations(annotations);
ImportNode prev = staticImports.put(alias, node);
if (prev != null) {
staticImports.put(prev.toString(), prev);
staticImports.put(alias, staticImports.remove(alias));
}
storeLastAddedImportNode(node);
}
public void addStaticStarImport(String name, ClassNode type) {
addStaticStarImport(name, type, Collections.emptyList());
}
public void addStaticStarImport(String name, ClassNode type, List<AnnotationNode> annotations) {
ImportNode node = new ImportNode(type);
node.addAnnotations(annotations);
staticStarImports.put(name, node);
storeLastAddedImportNode(node);
}
public void addStatement(Statement node) {
statementBlock.addStatement(node);
}
public void addClass(ClassNode node) {
if(classes.isEmpty()) mainClassName = node.getName();
classes.add(node);
node.setModule(this);
addToCompileUnit(node);
}
private void addToCompileUnit(ClassNode node) {
// register the new class with the compile unit
if (unit != null) {
unit.addClass(node);
}
}
public void addMethod(MethodNode node) {
methods.add(node);
}
public void visit(GroovyCodeVisitor visitor) {
}
public String getPackageName() {
return packageNode == null ? null : packageNode.getName();
}
public PackageNode getPackage() {
return packageNode;
}
// TODO don't allow override?
public void setPackage(PackageNode packageNode) {
this.packageNode = packageNode;
}
// TODO don't allow override?
public void setPackageName(String packageName) {
this.packageNode = new PackageNode(packageName);
}
public boolean hasPackageName(){
return packageNode != null && packageNode.getName() != null;
}
public boolean hasPackage(){
return this.packageNode != null;
}
public SourceUnit getContext() {
return context;
}
/**
* @return the underlying character stream description
*/
public String getDescription() {
if (context != null) {
return context.getName();
} else {
return this.description;
}
}
public void setDescription(String description) {
this.description = description;
}
public CompileUnit getUnit() {
return unit;
}
void setUnit(CompileUnit unit) {
this.unit = unit;
}
public ClassNode getScriptClassDummy() {
if (scriptDummy!=null) {
setScriptBaseClassFromConfig(scriptDummy);
return scriptDummy;
}
String name = getPackageName();
if (name == null) {
name = "";
}
// now let's use the file name to determine the class name
if (getDescription() == null) {
throw new RuntimeException("Cannot generate main(String[]) class for statements when we have no file description");
}
name += GeneratorContext.encodeAsValidClassName(extractClassFromFileDescription());
ClassNode classNode;
if (isPackageInfo()) {
classNode = new ClassNode(name, ACC_ABSTRACT | ACC_INTERFACE, ClassHelper.OBJECT_TYPE);
} else {
classNode = new ClassNode(name, ACC_PUBLIC, ClassHelper.SCRIPT_TYPE);
setScriptBaseClassFromConfig(classNode);
classNode.setScript(true);
classNode.setScriptBody(true);
}
scriptDummy = classNode;
return classNode;
}
private void setScriptBaseClassFromConfig(ClassNode cn) {
String baseClassName = null;
if (unit != null) {
baseClassName = unit.getConfig().getScriptBaseClass();
} else if (context != null) {
baseClassName = context.getConfiguration().getScriptBaseClass();
}
if (baseClassName != null) {
if (!cn.getSuperClass().getName().equals(baseClassName)) {
cn.setSuperClass(ClassHelper.make(baseClassName));
AnnotationNode annotationNode = new AnnotationNode(BaseScriptASTTransformation.MY_TYPE);
cn.addAnnotation(annotationNode);
}
}
}
protected ClassNode createStatementsClass() {
ClassNode classNode = getScriptClassDummy();
if (classNode.getName().endsWith("package-info")) {
return classNode;
}
handleMainMethodIfPresent(methods);
// return new Foo(new ShellContext(args)).run()
classNode.addMethod(
new MethodNode(
"main",
ACC_PUBLIC | ACC_STATIC,
ClassHelper.VOID_TYPE,
new Parameter[] { new Parameter(ClassHelper.STRING_TYPE.makeArray(), "args")},
ClassNode.EMPTY_ARRAY,
new ExpressionStatement(
new MethodCallExpression(
new ClassExpression(ClassHelper.make(InvokerHelper.class)),
"runScript",
new ArgumentListExpression(
new ClassExpression(classNode),
new VariableExpression("args"))))));
MethodNode methodNode = new MethodNode("run", ACC_PUBLIC, ClassHelper.OBJECT_TYPE, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, statementBlock);
methodNode.setIsScriptBody();
classNode.addMethod(methodNode);
classNode.addConstructor(ACC_PUBLIC, Parameter.EMPTY_ARRAY, ClassNode.EMPTY_ARRAY, new BlockStatement());
Statement stmt;
// A script's contextual constructor should call it's super class' contextual constructor, if it has one.
// In practice this will always be true because currently this visitor is run before the AST transformations
// (like @BaseScript) that could change this. But this is cautious and anticipates possible compiler changes.
if (classNode.getSuperClass().getDeclaredConstructor(SCRIPT_CONTEXT_CTOR) != null) {
stmt = new ExpressionStatement(
new ConstructorCallExpression(ClassNode.SUPER,
new ArgumentListExpression(
new VariableExpression("context"))));
} else {
// Fallback for non-standard base "script" classes with no context (Binding) constructor.
stmt = new ExpressionStatement(
new MethodCallExpression(
new VariableExpression("super"),
"setBinding",
new ArgumentListExpression(
new VariableExpression("context"))));
}
classNode.addConstructor(
ACC_PUBLIC,
new Parameter[] { new Parameter(ClassHelper.make(Binding.class), "context")},
ClassNode.EMPTY_ARRAY,
stmt);
for (MethodNode node : methods) {
int modifiers = node.getModifiers();
if ((modifiers & ACC_ABSTRACT) != 0) {
throw new RuntimeException(
"Cannot use abstract methods in a script, they are only available inside classes. Method: "
+ node.getName());
}
// br: the old logic seems to add static to all def f().... in a script, which makes enclosing
// inner classes (including closures) in a def function difficult. Comment it out.
node.setModifiers(modifiers /*| ACC_STATIC*/);
classNode.addMethod(node);
}
return classNode;
}
/*
* If a main method is provided by user, account for it under run() as scripts generate their own 'main' so they can run.
*/
private void handleMainMethodIfPresent(List methods) {
boolean found = false;
for (Iterator iter = methods.iterator(); iter.hasNext();) {
MethodNode node = (MethodNode) iter.next();
if(node.getName().equals("main")) {
if (node.isStatic() && node.getParameters().length == 1) {
boolean retTypeMatches, argTypeMatches;
ClassNode argType = node.getParameters()[0].getType();
ClassNode retType = node.getReturnType();
argTypeMatches = (argType.equals(ClassHelper.OBJECT_TYPE) || argType.getName().contains("String[]"));
retTypeMatches = (retType == ClassHelper.VOID_TYPE || retType == ClassHelper.OBJECT_TYPE);
if(retTypeMatches && argTypeMatches) {
if(found) {
throw new RuntimeException("Repetitive main method found.");
} else {
found = true;
}
// if script has both loose statements as well as main(), then main() is ignored
if(statementBlock.isEmpty()) {
addStatement(node.getCode());
}
iter.remove();
}
}
}
}
}
protected String extractClassFromFileDescription() {
String answer = getDescription();
try {
URI uri = new URI(answer);
String path = uri.getPath();
String schemeSpecific = uri.getSchemeSpecificPart();
if (path!=null) {
answer = path;
} else if (schemeSpecific!=null) {
answer = schemeSpecific;
}
} catch (URISyntaxException e) {}
// let's strip off everything after the last '.'
int slashIdx = answer.lastIndexOf('/');
int separatorIdx = answer.lastIndexOf(File.separatorChar);
int dotIdx = answer.lastIndexOf('.');
if (dotIdx > 0 && dotIdx > Math.max(slashIdx, separatorIdx)) {
answer = answer.substring(0, dotIdx);
}
// new let's strip everything up to and including the path separators
if (slashIdx >= 0) {
answer = answer.substring(slashIdx + 1);
}
// recalculate in case we have already done some stripping
separatorIdx = answer.lastIndexOf(File.separatorChar);
if (separatorIdx >= 0) {
answer = answer.substring(separatorIdx + 1);
}
return answer;
}
public boolean isEmpty() {
return classes.isEmpty() && statementBlock.getStatements().isEmpty();
}
public void sortClasses(){
if (isEmpty()) return;
List<ClassNode> classes = getClasses();
LinkedList<ClassNode> sorted = new LinkedList<>();
int level=1;
while (!classes.isEmpty()) {
for (Iterator<ClassNode> cni = classes.iterator(); cni.hasNext();) {
ClassNode cn = cni.next();
ClassNode sn = cn;
for (int i=0; sn!=null && i<level; i++) sn = sn.getSuperClass();
if (sn!=null && sn.isPrimaryClassNode()) continue;
cni.remove();
sorted.addLast(cn);
}
level++;
}
this.classes = sorted;
}
public boolean hasImportsResolved() {
return importsResolved;
}
public void setImportsResolved(boolean importsResolved) {
this.importsResolved = importsResolved;
}
public Map<String, ImportNode> getStaticImports() {
return staticImports;
}
public Map<String, ImportNode> getStaticStarImports() {
return staticStarImports;
}
// This method only exists as a workaround for GROOVY-6094
// In order to keep binary compatibility
private void storeLastAddedImportNode(final ImportNode node) {
if (getNodeMetaData(ImportNode.class) == ImportNode.class) {
putNodeMetaData(ImportNode.class, node);
}
}
public String getMainClassName() {
return mainClassName;
}
}