blob: 4690ba5fb9bf733d7114d414a327319aa0c42e18 [file] [log] [blame]
/*
* Copyright 2000-2009 JetBrains s.r.o.
*
* 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 com.intellij.patterns.compiler;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringHash;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.patterns.ElementPattern;
import com.intellij.patterns.ElementPatternCondition;
import com.intellij.patterns.InitialPatternCondition;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Function;
import com.intellij.util.ProcessingContext;
import com.intellij.util.ReflectionCache;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.Stack;
import com.intellij.util.containers.StringInterner;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.*;
import java.util.*;
/**
* @author Gregory.Shrago
*/
public class PatternCompilerImpl<T> implements PatternCompiler<T> {
private static final Logger LOG = Logger.getInstance(PatternCompilerImpl.class.getName());
private final Set<Method> myStaticMethods;
private final StringInterner myStringInterner = new StringInterner();
public PatternCompilerImpl(final List<Class> patternClasses) {
myStaticMethods = getStaticMethods(patternClasses);
}
private static final Node ERROR_NODE = new Node(null, null, null);
@Override
public ElementPattern<T> createElementPattern(final String text, final String displayName) {
try {
return compileElementPattern(text);
}
catch (Exception ex) {
final Throwable cause = ex.getCause() != null ? ex.getCause() : ex;
LOG.warn("error processing place: " + displayName + " [" + text + "]", cause);
return new LazyPresentablePattern<T>(new Node(ERROR_NODE, text, null));
}
}
//@Override
//public ElementPattern<T> compileElementPattern(final String text) {
// return processElementPatternText(text, new Function<Frame, Object>() {
// public Object fun(final Frame frame) {
// try {
// final Object[] args = frame.params.toArray();
// preInvoke(frame.target, frame.methodName, args);
// return invokeMethod(frame.target, frame.methodName, args, myStaticMethods);
// }
// catch (Throwable throwable) {
// throw new IllegalArgumentException(text, throwable);
// }
// }
// });
//}
@Override
public synchronized ElementPattern<T> compileElementPattern(final String text) {
Node node = processElementPatternText(text, new Function<Frame, Object>() {
public Node fun(final Frame frame) {
final Object[] args = frame.params.toArray();
for (int i = 0, argsLength = args.length; i < argsLength; i++) {
args[i] = args[i] instanceof String ? myStringInterner.intern((String)args[i]) : args[i];
}
return new Node((Node)frame.target, myStringInterner.intern(frame.methodName), args.length == 0 ? ArrayUtil.EMPTY_OBJECT_ARRAY : args);
}
});
if (node == null) node = new Node(ERROR_NODE, text, null);
return new LazyPresentablePattern<T>(node);
}
private static Set<Method> getStaticMethods(List<Class> patternClasses) {
return new THashSet<Method>(ContainerUtil.concat(patternClasses, new Function<Class, Collection<? extends Method>>() {
public Collection<Method> fun(final Class aClass) {
return ContainerUtil.findAll(ReflectionCache.getMethods(aClass), new Condition<Method>() {
public boolean value(final Method method) {
return Modifier.isStatic(method.getModifiers())
&& Modifier.isPublic(method.getModifiers())
&& !Modifier.isAbstract(method.getModifiers())
&& ElementPattern.class.isAssignableFrom(method.getReturnType());
}
});
}
}));
}
private enum State {
init, name, name_end,
param_start, param_end, literal, escape,
invoke, invoke_end
}
private static class Frame {
State state = State.init;
Object target;
String methodName;
ArrayList<Object> params = new ArrayList<Object>();
}
@Nullable
private static <T> T processElementPatternText(final String text, final Function<Frame, Object> executor) {
final Stack<Frame> stack = new Stack<Frame>();
int curPos = 0;
Frame curFrame = new Frame();
Object curResult = null;
final StringBuilder curString = new StringBuilder();
while (true) {
if (curPos > text.length()) break;
final char ch = curPos++ < text.length()? text.charAt(curPos-1) : 0;
switch (curFrame.state) {
case init:
if (Character.isWhitespace(ch)) {
}
else if (Character.isJavaIdentifierStart(ch)) {
curString.append(ch);
curFrame.state = State.name;
}
else {
throwError(curPos, ch, "method call expected");
}
break;
case name:
if (Character.isJavaIdentifierPart(ch)) {
curString.append(ch);
}
else if (ch == '(' || Character.isWhitespace(ch)) {
curFrame.methodName = curString.toString();
curString.setLength(0);
curFrame.state = ch == '('? State.param_start : State.name_end;
}
else {
throwError(curPos, ch, "'"+curString+ch+"' method name start is invalid, '(' expected");
}
break;
case name_end:
if (ch == '(') {
curFrame.state = State.param_start;
}
else if (!Character.isWhitespace(ch)) {
throwError(curPos, ch, "'(' expected after '"+curFrame.methodName+"'");
}
break;
case param_start:
if (Character.isWhitespace(ch)) {
}
else if (Character.isDigit(ch) || ch == '\"') {
curFrame.state = State.literal;
curString.append(ch);
}
else if (ch == ')') {
curFrame.state = State.invoke;
}
else if (Character.isJavaIdentifierStart(ch)) {
curString.append(ch);
stack.push(curFrame);
curFrame = new Frame();
curFrame.state = State.name;
}
else {
throwError(curPos, ch, "expression expected in '" + curFrame.methodName + "' call");
}
break;
case param_end:
if (ch == ')') {
curFrame.state = State.invoke;
}
else if (ch == ',') {
curFrame.state = State.param_start;
}
else if (!Character.isWhitespace(ch)) {
throwError(curPos, ch, "')' or ',' expected in '" + curFrame.methodName + "' call");
}
break;
case literal:
if (curString.charAt(0) == '\"') {
curString.append(ch);
if (ch == '\\') {
curFrame.state = State.escape;
}
else {
if (ch == '\"') {
curFrame.params.add(makeParam(curString.toString()));
curString.setLength(0);
curFrame.state = State.param_end;
}
}
}
else if (Character.isWhitespace(ch) || ch == ',' || ch == ')') {
curFrame.params.add(makeParam(curString.toString()));
curString.setLength(0);
curFrame.state = ch == ')' ? State.invoke :
ch == ',' ? State.param_start : State.param_end;
}
else {
curString.append(ch);
}
break;
case escape:
if (ch != 0) {
curString.append(ch);
curFrame.state = State.literal;
}
else {
throwError(curPos, ch, "unclosed escape sequence");
}
break;
case invoke:
curResult = executor.fun(curFrame);
if (ch == 0 && stack.isEmpty()) {
return (T)curResult;
}
else if (ch == '.') {
curFrame = new Frame();
curFrame.target = curResult;
curFrame.state = State.init;
curResult = null;
}
else if (ch == ',' || ch == ')') {
curFrame = stack.pop();
curFrame.params.add(curResult);
curResult = null;
curFrame.state = ch == ')' ? State.invoke : State.param_start;
}
else if (Character.isWhitespace(ch)) {
curFrame.state = State.invoke_end;
}
else {
throwError(curPos, ch, (stack.isEmpty()? "'.' or <eof>" : "'.' or ')'")
+ "expected after '" + curFrame.methodName + "' call");
}
break;
case invoke_end:
if (ch == 0 && stack.isEmpty()) {
return (T)curResult;
}
else if (ch == ')') {
curFrame.state = State.invoke;
}
else if (ch == ',') {
curFrame.state = State.param_start;
}
else if (ch == '.') {
curFrame = new Frame();
curFrame.target = curResult;
curFrame.state = State.init;
curResult = null;
}
else if (!Character.isWhitespace(ch)) {
throwError(curPos, ch, (stack.isEmpty()? "'.' or <eof>" : "'.' or ')'")
+ "expected after '" + curFrame.methodName + "' call");
}
break;
}
}
return null;
}
private static void throwError(int offset, char ch, String message) {
throw new IllegalStateException(offset+"("+ch+"): "+message);
}
private static Object makeParam(final String s) {
if (s.length() >= 2 && s.startsWith("\"") && s.endsWith("\"")) {
return StringUtil.unescapeStringCharacters(s.substring(1, s.length() - 1));
}
try {
return Integer.valueOf(s);
}
catch (NumberFormatException e) {}
return s;
}
private static Class<?> getNonPrimitiveType(final Class<?> type) {
if (!type.isPrimitive()) return type;
if (type == boolean.class) return Boolean.class;
if (type == byte.class) return Byte.class;
if (type == short.class) return Short.class;
if (type == int.class) return Integer.class;
if (type == long.class) return Long.class;
if (type == float.class) return Float.class;
if (type == double.class) return Double.class;
if (type == char.class) return Character.class;
return type;
}
private static Object invokeMethod(@Nullable final Object target, final String methodName, final Object[] arguments, final Collection<Method> staticMethods) throws Throwable {
final Ref<Boolean> convertVarArgs = Ref.create(Boolean.FALSE);
final Collection<Method> methods = target == null ? staticMethods : Arrays.asList(target.getClass().getMethods());
final Method method = findMethod(methodName, arguments, methods, convertVarArgs);
if (method != null) {
try {
final Object[] newArgs;
if (!convertVarArgs.get()) newArgs = arguments;
else {
final Class<?>[] parameterTypes = method.getParameterTypes();
newArgs = new Object[parameterTypes.length];
System.arraycopy(arguments, 0, newArgs, 0, parameterTypes.length - 1);
final Object[] varArgs = (Object[])Array
.newInstance(parameterTypes[parameterTypes.length - 1].getComponentType(), arguments.length - parameterTypes.length + 1);
System.arraycopy(arguments, parameterTypes.length - 1, varArgs, 0, varArgs.length);
newArgs[parameterTypes.length - 1] = varArgs;
}
return method.invoke(target, newArgs);
}
catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
throw new NoSuchMethodException("unknown symbol: "+methodName + "(" + StringUtil.join(arguments, new Function<Object, String>() {
public String fun(Object o) {
return String.valueOf(o);
}
}, ", ")+")");
}
@Nullable
private static Method findMethod(final String methodName, final Object[] arguments, final Collection<Method> methods, Ref<Boolean> convertVarArgs) {
main: for (Method method : methods) {
if (!methodName.equals(method.getName())) continue;
final Class<?>[] parameterTypes = method.getParameterTypes();
if (!method.isVarArgs() && parameterTypes.length != arguments.length) continue;
convertVarArgs.set(false);
for (int i = 0, parameterTypesLength = parameterTypes.length; i < arguments.length; i++) {
final Class<?> type = getNonPrimitiveType(i < parameterTypesLength ? parameterTypes[i] : parameterTypes[parameterTypesLength - 1]);
final Object argument = arguments[i];
final Class<?> componentType =
method.isVarArgs() && i < parameterTypesLength - 1 ? null : parameterTypes[parameterTypesLength - 1].getComponentType();
if (argument != null) {
if (!type.isInstance(argument)) {
if ((componentType == null || !componentType.isInstance(argument))) continue main;
else convertVarArgs.set(true);
}
}
}
if (parameterTypes.length > arguments.length) {
convertVarArgs.set(true);
}
return method;
}
return null;
}
@Override
public String dumpContextDeclarations() {
final StringBuilder sb = new StringBuilder();
final THashMap<Class, Collection<Class>> classes = new THashMap<Class, Collection<Class>>();
final THashSet<Class> missingClasses = new THashSet<Class>();
classes.put(Object.class, missingClasses);
for (Method method : myStaticMethods) {
for (Class<?> type = method.getReturnType(); type != null && ElementPattern.class.isAssignableFrom(type); type = type.getSuperclass()) {
final Class<?> enclosingClass = type.getEnclosingClass();
if (enclosingClass != null) {
Collection<Class> list = classes.get(enclosingClass);
if (list == null) {
list = new THashSet<Class>();
classes.put(enclosingClass, list);
}
list.add(type);
}
else if (!classes.containsKey(type)) {
classes.put(type, null);
}
}
}
for (Class aClass : classes.keySet()) {
if (aClass == Object.class) continue;
printClass(aClass, classes, sb);
}
for (Method method : myStaticMethods) {
printMethodDeclaration(method, sb, classes);
}
for (Class aClass : missingClasses) {
sb.append("class ").append(aClass.getSimpleName());
final Class superclass = aClass.getSuperclass();
if (missingClasses.contains(superclass)) {
sb.append(" extends ").append(superclass.getSimpleName());
}
sb.append("{}\n");
}
//System.out.println(sb);
return sb.toString();
}
private static void printClass(Class aClass, Map<Class, Collection<Class>> classes, StringBuilder sb) {
final boolean isInterface = aClass.isInterface();
sb.append(isInterface ? "interface ": "class ");
dumpType(aClass, aClass, sb, classes);
final Type superClass = aClass.getGenericSuperclass();
final Class rawSuperClass = (Class)(superClass instanceof ParameterizedType ? ((ParameterizedType)superClass).getRawType() : superClass);
if (superClass != null && classes.containsKey(rawSuperClass)) {
sb.append(" extends ");
dumpType(null, superClass, sb, classes);
}
int implementsIdx = 1;
for (Type superInterface : aClass.getGenericInterfaces()) {
final Class rawSuperInterface = (Class)(superInterface instanceof ParameterizedType ? ((ParameterizedType)superInterface).getRawType() : superClass);
if (classes.containsKey(rawSuperInterface)) {
if (implementsIdx++ == 1) sb.append(isInterface? " extends " : " implements ");
else sb.append(", ");
dumpType(null, superInterface, sb, classes);
}
}
sb.append(" {\n");
for (Method method : aClass.getDeclaredMethods()) {
if (Modifier.isStatic(method.getModifiers()) ||
!Modifier.isPublic(method.getModifiers()) ||
Modifier.isVolatile(method.getModifiers())) continue;
printMethodDeclaration(method, sb.append(" "), classes);
}
final Collection<Class> innerClasses = classes.get(aClass);
sb.append("}\n");
if (innerClasses != null) {
for (Class innerClass : innerClasses) {
printClass(innerClass, classes, sb);
}
}
}
private static void dumpType(GenericDeclaration owner, Type type, StringBuilder sb, Map<Class, Collection<Class>> classes) {
if (type instanceof Class) {
final Class aClass = (Class)type;
final Class enclosingClass = aClass.getEnclosingClass();
if (enclosingClass != null) {
sb.append(enclosingClass.getSimpleName()).append("_");
}
else if (!aClass.isArray() && !aClass.isPrimitive() && !aClass.getName().startsWith("java.") && !classes.containsKey(aClass)) {
classes.get(Object.class).add(aClass);
}
sb.append(aClass.getSimpleName());
if (owner == aClass) {
dumpTypeParametersArray(owner, aClass.getTypeParameters(), sb, "<", ">", classes);
}
}
else if (type instanceof TypeVariable) {
TypeVariable typeVariable = (TypeVariable)type;
sb.append(typeVariable.getName());
if (typeVariable.getGenericDeclaration() == owner) {
dumpTypeParametersArray(null, typeVariable.getBounds(), sb, " extends ", "", classes);
}
}
else if (type instanceof WildcardType) {
final WildcardType wildcardType = (WildcardType)type;
sb.append("?");
dumpTypeParametersArray(owner, wildcardType.getUpperBounds(), sb, " extends ", "", classes);
dumpTypeParametersArray(owner, wildcardType.getLowerBounds(), sb, " super ", "", classes);
}
else if (type instanceof ParameterizedType) {
final ParameterizedType parameterizedType = (ParameterizedType)type;
final Type raw = parameterizedType.getRawType();
dumpType(null, raw, sb, classes);
dumpTypeParametersArray(owner, parameterizedType.getActualTypeArguments(), sb, "<", ">", classes);
}
else if (type instanceof GenericArrayType) {
dumpType(owner, ((GenericArrayType)type).getGenericComponentType(), sb, classes);
sb.append("[]");
}
}
private static void dumpTypeParametersArray(GenericDeclaration owner, final Type[] typeVariables,
final StringBuilder sb,
final String prefix, final String suffix, Map<Class, Collection<Class>> classes) {
int typeVarIdx = 1;
for (Type typeVariable : typeVariables) {
if (typeVariable == Object.class) continue;
if (typeVarIdx++ == 1) sb.append(prefix);
else sb.append(", ");
dumpType(owner, typeVariable, sb, classes);
}
if (typeVarIdx > 1) sb.append(suffix);
}
private static void printMethodDeclaration(Method method, StringBuilder sb, Map<Class, Collection<Class>> classes) {
if (Modifier.isStatic(method.getModifiers())) {
sb.append("static ");
}
dumpTypeParametersArray(method, method.getTypeParameters(), sb, "<", "> ", classes);
dumpType(null, method.getGenericReturnType(), sb, classes);
sb.append(" ").append(method.getName()).append("(");
int paramIdx = 1;
for (Type parameter : method.getGenericParameterTypes()) {
if (paramIdx != 1) sb.append(", ");
dumpType(null, parameter, sb, classes);
sb.append(" ").append("p").append(paramIdx++);
}
sb.append(")");
if (!method.getDeclaringClass().isInterface()) sb.append("{}");
sb.append("\n");
}
//@Nullable
//public static ElementPattern<PsiElement> createElementPatternGroovy(final String text, final String displayName, final String supportId) {
// final Binding binding = new Binding();
// final ArrayList<MetaMethod> metaMethods = new ArrayList<MetaMethod>();
// for (Class aClass : getPatternClasses(supportId)) {
// // walk super classes as well?
// for (CachedMethod method : ReflectionCache.getCachedClass(aClass).getMethods()) {
// if (!Modifier.isStatic(method.getModifiers()) || !Modifier.isPublic(method.getModifiers()) || Modifier.isAbstract(method.getModifiers())) continue;
// metaMethods.add(method);
// }
// }
//
// final ExpandoMetaClass metaClass = new ExpandoMetaClass(Object.class, false, metaMethods.toArray(new MetaMethod[metaMethods.size()]));
// final GroovyShell shell = new GroovyShell(binding);
// try {
// final Script script = shell.parse("return " + text);
// metaClass.initialize();
// script.setMetaClass(metaClass);
// final Object value = script.run();
// return value instanceof ElementPattern ? (ElementPattern<PsiElement>)value : null;
// }
// catch (GroovyRuntimeException ex) {
// Configuration.LOG.warn("error processing place: "+displayName+" ["+text+"]", ex);
// }
// return null;
//}
private static final ElementPattern<?> ALWAYS_FALSE = new FalsePattern();
private static class Node {
final Node target;
final String method;
final Object[] args;
private Node(final Node target, final String method, final Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
}
private static class FalsePattern extends InitialPatternCondition<Object> implements ElementPattern<Object> {
private final ElementPatternCondition<Object> myCondition = new ElementPatternCondition<Object>(this);
protected FalsePattern() {
super(Object.class);
}
@Override
public boolean accepts(@Nullable final Object o) {
return false;
}
@Override
public boolean accepts(@Nullable final Object o, final ProcessingContext context) {
return false;
}
@Override
public ElementPatternCondition<Object> getCondition() {
return myCondition;
}
}
public class LazyPresentablePattern<T> implements ElementPattern<T> {
private ElementPattern<T> myCompiledPattern;
private final Node myNode;
private final long myHashCode;
public LazyPresentablePattern(@NotNull Node node) {
myNode = node;
myHashCode = StringHash.calc(toString());
}
@Override
public boolean accepts(@Nullable final Object o) {
return getCompiledPattern().accepts(o, new ProcessingContext());
}
@Override
public boolean accepts(@Nullable final Object o, final ProcessingContext context) {
return getCompiledPattern().accepts(o, context);
}
@Override
public ElementPatternCondition<T> getCondition() {
return getCompiledPattern().getCondition();
}
public ElementPattern<T> getCompiledPattern() {
if (myCompiledPattern == null) {
Object result;
try {
result = compile();
}
catch (Throwable throwable) {
LOG.error(toString(), throwable);
result = ALWAYS_FALSE;
}
myCompiledPattern = (ElementPattern<T>)result;
}
return myCompiledPattern;
}
public ElementPattern<?> compile() throws Throwable {
return myNode.target == ERROR_NODE? ALWAYS_FALSE : (ElementPattern<?>)execute(myNode);
}
@Override
public String toString() {
return toString(myNode, new StringBuilder()).toString();
}
private StringBuilder toString(final Node node, final StringBuilder sb) {
if (node.target == ERROR_NODE) {
return sb.append(node.method);
}
if (node.target != null) {
toString(node.target, sb);
sb.append('.');
}
sb.append(node.method).append('(');
boolean first = true;
for (Object arg : node.args) {
if (first) first = false;
else sb.append(',').append(' ');
if (arg instanceof Node) {
toString((Node)arg, sb);
}
else if (arg instanceof String) {
sb.append('\"').append(StringUtil.escapeStringCharacters((String)arg)).append('\"');
}
else if (arg instanceof Number) {
sb.append(arg);
}
}
sb.append(')');
return sb;
}
private Object execute(final Node node) throws Throwable {
final Object target = node.target != null? execute(node.target) : null;
final String methodName = node.method;
final Object[] args;
if (node.args.length == 0) {
args = node.args;
}
else {
args = new Object[node.args.length];
for (int i = 0, len = node.args.length; i < len; i++) {
args[i] = node.args[i] instanceof Node? execute((Node)node.args[i]) : node.args[i];
}
}
return invokeMethod(target, methodName, args, myStaticMethods);
}
@Override
public int hashCode() {
return (int)myHashCode;
}
@Override
public boolean equals(final Object obj) {
return obj instanceof LazyPresentablePattern &&
((LazyPresentablePattern)obj).myHashCode == myHashCode;
}
}
}