/*
* Copyright 2003-2012 the original author or authors.
*
* Licensed 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.stc;
import groovy.lang.*;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.*;
import org.codehaus.groovy.ast.expr.*;
import org.codehaus.groovy.ast.stmt.EmptyStatement;
import org.codehaus.groovy.ast.stmt.ReturnStatement;
import org.codehaus.groovy.classgen.asm.InvocationWriter;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilationUnit;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.codehaus.groovy.control.messages.SimpleMessage;
import org.codehaus.groovy.runtime.InvokerHelper;
import groovyjarjarasm.asm.Opcodes;
import java.io.*;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.logging.Logger;
/**
* Base class for type checking extensions written in Groovy. Compared to its superclass, {@link TypeCheckingExtension},
* this class adds a number of utility methods aimed at leveraging the syntax of the Groovy language to improve
* expressivity and conciseness.
*
* @author Cedric Champeau
* @since 2.1.0
*/
public class GroovyTypeCheckingExtensionSupport extends AbstractTypeCheckingExtension {
// method name to DSL name
private static final Map<String, String> METHOD_ALIASES;
static {
final Map<String, String> aliases = new HashMap<String, String>(20);
aliases.put("onMethodSelection", "onMethodSelection");
aliases.put("afterMethodCall", "afterMethodCall");
aliases.put("beforeMethodCall", "beforeMethodCall");
aliases.put("unresolvedVariable", "handleUnresolvedVariableExpression");
aliases.put("unresolvedProperty", "handleUnresolvedProperty");
aliases.put("unresolvedAttribute", "handleUnresolvedAttribute");
aliases.put("ambiguousMethods", "handleAmbiguousMethods");
aliases.put("methodNotFound", "handleMissingMethod");
aliases.put("afterVisitMethod", "afterVisitMethod");
aliases.put("beforeVisitMethod", "beforeVisitMethod");
aliases.put("afterVisitClass", "afterVisitClass");
aliases.put("beforeVisitClass", "beforeVisitClass");
aliases.put("incompatibleAssignment", "handleIncompatibleAssignment");
aliases.put("incompatibleReturnType", "handleIncompatibleReturnType");
aliases.put("setup","setup");
aliases.put("finish", "finish");
METHOD_ALIASES = Collections.unmodifiableMap(aliases);
}
// the following fields are closures executed in event-based methods
private final Map<String, List<Closure>> eventHandlers = new HashMap<String, List<Closure>>();
private final String scriptPath;
private final CompilationUnit compilationUnit;
/**
* Builds a type checking extension relying on a Groovy script (type checking DSL).
*
* @param typeCheckingVisitor the type checking visitor
* @param scriptPath the path to the type checking script (in classpath)
* @param compilationUnit
*/
public GroovyTypeCheckingExtensionSupport(
final StaticTypeCheckingVisitor typeCheckingVisitor,
final String scriptPath, final CompilationUnit compilationUnit) {
super(typeCheckingVisitor);
this.scriptPath = scriptPath;
this.compilationUnit = compilationUnit;
}
public void setDebug(final boolean debug) {
this.debug = debug;
}
@Override
public void setup() {
CompilerConfiguration config = new CompilerConfiguration();
config.setScriptBaseClass("org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport.TypeCheckingDSL");
ImportCustomizer ic = new ImportCustomizer();
ic.addStarImports("org.codehaus.groovy.ast.expr");
ic.addStaticStars("org.codehaus.groovy.ast.ClassHelper");
ic.addStaticStars("org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport");
config.addCompilationCustomizers(ic);
final GroovyClassLoader transformLoader = compilationUnit!=null?compilationUnit.getTransformLoader():typeCheckingVisitor.getSourceUnit().getClassLoader();
// since Groovy 2.2, it is possible to use FQCN for type checking extension scripts
TypeCheckingDSL script = null;
try {
Class<?> clazz = transformLoader.loadClass(scriptPath, false, true);
if (TypeCheckingDSL.class.isAssignableFrom(clazz)) {
script = (TypeCheckingDSL) clazz.newInstance();
}
} catch (ClassNotFoundException e) {
// silent
} catch (InstantiationException e) {
context.getErrorCollector().addFatalError(
new SimpleMessage("Static type checking extension '" + scriptPath + "' could not be loaded.",
config.getDebug(), typeCheckingVisitor.getSourceUnit()));
} catch (IllegalAccessException e) {
context.getErrorCollector().addFatalError(
new SimpleMessage("Static type checking extension '" + scriptPath + "' could not be loaded.",
config.getDebug(), typeCheckingVisitor.getSourceUnit()));
}
if (script==null) {
ClassLoader cl = typeCheckingVisitor.getSourceUnit().getClassLoader();
// cast to prevent incorrect @since 1.7 warning
InputStream is = ((ClassLoader)transformLoader).getResourceAsStream(scriptPath);
if (is == null) {
// fallback to the source unit classloader
is = cl.getResourceAsStream(scriptPath);
}
if (is == null) {
// fallback to the compiler classloader
cl = GroovyTypeCheckingExtensionSupport.class.getClassLoader();
is = cl.getResourceAsStream(scriptPath);
}
// GRECLIPSE
// don't place an error here if file not found during reconcile.
// might be because build hasn't happened yet.
// /*old{
if (is == null) {
// }new*/
// if (is == null && !typeCheckingVisitor.getSourceUnit().isReconcile) {
//GRECLIPSE end
// if the input stream is still null, we've not found the extension
context.getErrorCollector().addFatalError(
new SimpleMessage("Static type checking extension '" + scriptPath + "' was not found on the classpath.",
config.getDebug(), typeCheckingVisitor.getSourceUnit()));
}
try {
GroovyShell shell = new GroovyShell(transformLoader, new Binding(), config);
script = (TypeCheckingDSL) shell.parse(
new InputStreamReader(is, typeCheckingVisitor.getSourceUnit().getConfiguration().getSourceEncoding())
);
} catch (CompilationFailedException e) {
throw new GroovyBugError("An unexpected error was thrown during custom type checking", e);
} catch (UnsupportedEncodingException e) {
throw new GroovyBugError("Unsupported encoding found in compiler configuration", e);
}
}
if (script!=null) {
script.extension = this;
script.run();
List<Closure> list = eventHandlers.get("setup");
if (list != null) {
for (Closure closure : list) {
safeCall(closure);
}
}
}
}
@Override
public void finish() {
List<Closure> list = eventHandlers.get("finish");
if (list != null) {
for (Closure closure : list) {
safeCall(closure);
}
}
}
@Override
public void onMethodSelection(final Expression expression, final MethodNode target) {
List<Closure> onMethodSelection = eventHandlers.get("onMethodSelection");
if (onMethodSelection != null) {
for (Closure closure : onMethodSelection) {
safeCall(closure, expression, target);
}
}
}
@Override
public void afterMethodCall(final MethodCall call) {
List<Closure> onMethodSelection = eventHandlers.get("afterMethodCall");
if (onMethodSelection != null) {
for (Closure closure : onMethodSelection) {
safeCall(closure, call);
}
}
}
@Override
public boolean beforeMethodCall(final MethodCall call) {
setHandled(false);
List<Closure> onMethodSelection = eventHandlers.get("beforeMethodCall");
if (onMethodSelection != null) {
for (Closure closure : onMethodSelection) {
safeCall(closure, call);
}
}
return handled;
}
@Override
public boolean handleUnresolvedVariableExpression(final VariableExpression vexp) {
setHandled(false);
List<Closure> onMethodSelection = eventHandlers.get("handleUnresolvedVariableExpression");
if (onMethodSelection != null) {
for (Closure closure : onMethodSelection) {
safeCall(closure, vexp);
}
}
return handled;
}
@Override
public boolean handleUnresolvedProperty(final PropertyExpression pexp) {
setHandled(false);
List<Closure> list = eventHandlers.get("handleUnresolvedProperty");
if (list != null) {
for (Closure closure : list) {
safeCall(closure, pexp);
}
}
return handled;
}
@Override
public boolean handleUnresolvedAttribute(final AttributeExpression aexp) {
setHandled(false);
List<Closure> list = eventHandlers.get("handleUnresolvedAttribute");
if (list != null) {
for (Closure closure : list) {
safeCall(closure, aexp);
}
}
return handled;
}
@Override
public void afterVisitMethod(final MethodNode node) {
List<Closure> list = eventHandlers.get("afterVisitMethod");
if (list != null) {
for (Closure closure : list) {
safeCall(closure, node);
}
}
}
@Override
public boolean beforeVisitClass(final ClassNode node) {
setHandled(false);
List<Closure> list = eventHandlers.get("beforeVisitClass");
if (list != null) {
for (Closure closure : list) {
safeCall(closure, node);
}
}
return handled;
}
@Override
public void afterVisitClass(final ClassNode node) {
List<Closure> list = eventHandlers.get("afterVisitClass");
if (list != null) {
for (Closure closure : list) {
safeCall(closure, node);
}
}
}
@Override
public boolean beforeVisitMethod(final MethodNode node) {
setHandled(false);
List<Closure> list = eventHandlers.get("beforeVisitMethod");
if (list != null) {
for (Closure closure : list) {
safeCall(closure, node);
}
}
return handled;
}
@Override
public boolean handleIncompatibleAssignment(final ClassNode lhsType, final ClassNode rhsType, final Expression assignmentExpression) {
setHandled(false);
List<Closure> list = eventHandlers.get("handleIncompatibleAssignment");
if (list != null) {
for (Closure closure : list) {
safeCall(closure, lhsType, rhsType, assignmentExpression);
}
}
return handled;
}
@Override
public boolean handleIncompatibleReturnType(final ReturnStatement returnStatement, ClassNode inferredReturnType) {
setHandled(false);
List<Closure> list = eventHandlers.get("handleIncompatibleReturnType");
if (list != null) {
for (Closure closure : list) {
safeCall(closure, returnStatement, inferredReturnType);
}
}
return handled;
}
@Override
public List<MethodNode> handleMissingMethod(final ClassNode receiver, final String name, final ArgumentListExpression argumentList, final ClassNode[] argumentTypes, final MethodCall call) {
List<Closure> onMethodSelection = eventHandlers.get("handleMissingMethod");
List<MethodNode> methodList = new LinkedList<MethodNode>();
if (onMethodSelection != null) {
for (Closure closure : onMethodSelection) {
Object result = safeCall(closure, receiver, name, argumentList, argumentTypes, call);
if (result != null) {
if (result instanceof MethodNode) {
methodList.add((MethodNode) result);
} else if (result instanceof Collection) {
methodList.addAll((Collection<? extends MethodNode>) result);
} else {
throw new GroovyBugError("Type checking extension returned unexpected method list: " + result);
}
}
}
}
return methodList;
}
@Override
public List<MethodNode> handleAmbiguousMethods(final List<MethodNode> nodes, final Expression origin) {
List<Closure> onMethodSelection = eventHandlers.get("handleAmbiguousMethods");
List<MethodNode> methodList = nodes;
if (onMethodSelection != null) {
Iterator<Closure> iterator = onMethodSelection.iterator();
while (methodList.size()>1 && iterator.hasNext() ) {
final Closure closure = iterator.next();
Object result = safeCall(closure, methodList, origin);
if (result != null) {
if (result instanceof MethodNode) {
methodList = Collections.singletonList((MethodNode) result);
} else if (result instanceof Collection) {
methodList = new LinkedList<MethodNode>((Collection<? extends MethodNode>) result);
} else {
throw new GroovyBugError("Type checking extension returned unexpected method list: " + result);
}
}
}
}
return methodList;
}
// -------------------------------------
// delegate to the type checking context
// -------------------------------------
// --------------------------------------------
// end of delegate to the type checking context
// --------------------------------------------
public abstract static class TypeCheckingDSL extends Script {
private GroovyTypeCheckingExtensionSupport extension;
@Override
public Object getProperty(final String property) {
try {
return InvokerHelper.getProperty(extension, property);
} catch (Exception e) {
return super.getProperty(property);
}
}
@Override
public void setProperty(final String property, final Object newValue) {
try {
InvokerHelper.setProperty(extension, property, newValue);
} catch (Exception e) {
super.setProperty(property, newValue);
}
}
@Override
public Object invokeMethod(final String name, final Object args) {
if (name.startsWith("is") && name.endsWith("Expression") && args instanceof Object[] && ((Object[]) args).length == 1) {
String type = name.substring(2);
Object target = ((Object[]) args)[0];
if (target==null) return false;
try {
Class typeClass = Class.forName("org.codehaus.groovy.ast.expr."+type);
return typeClass.isAssignableFrom(target.getClass());
} catch (ClassNotFoundException e) {
return false;
}
}
if (args instanceof Object[] && ((Object[]) args).length == 1 && ((Object[]) args)[0] instanceof Closure) {
Object[] argsArray = (Object[]) args;
String methodName = METHOD_ALIASES.get(name);
if (methodName == null) {
return InvokerHelper.invokeMethod(extension, name, args);
}
List<Closure> closures = extension.eventHandlers.get(methodName);
if (closures == null) {
closures = new LinkedList<Closure>();
extension.eventHandlers.put(methodName, closures);
}
closures.add((Closure) argsArray[0]);
return null;
} else {
return InvokerHelper.invokeMethod(extension, name, args);
}
}
}
}