blob: 76bab69dabac245d77f76c02784c5fd75384db69 [file] [log] [blame]
/*
* Copyright 2000-2014 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 org.jetbrains.plugins.groovy.debugger;
import com.intellij.debugger.engine.evaluation.CodeFragmentFactory;
import com.intellij.debugger.engine.evaluation.TextWithImports;
import com.intellij.debugger.engine.evaluation.expression.EvaluatorBuilder;
import com.intellij.debugger.engine.evaluation.expression.EvaluatorBuilderImpl;
import com.intellij.openapi.fileTypes.LanguageFileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.util.ClassUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import gnu.trove.THashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.GroovyFileType;
import org.jetbrains.plugins.groovy.GroovyLanguage;
import org.jetbrains.plugins.groovy.debugger.fragments.GroovyCodeFragment;
import org.jetbrains.plugins.groovy.lang.psi.GroovyFile;
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory;
import org.jetbrains.plugins.groovy.lang.psi.GroovyRecursiveElementVisitor;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrVariable;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.toplevel.imports.GrImportStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.types.GrCodeReferenceElement;
import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.ClosureSyntheticParameter;
import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrBindingVariable;
import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GrLightVariable;
import org.jetbrains.plugins.groovy.lang.psi.util.PsiUtil;
import java.util.*;
import java.util.regex.Pattern;
/**
* @author ven
*/
public class GroovyCodeFragmentFactory extends CodeFragmentFactory {
private static final String EVAL_NAME = "_JETGROOVY_EVAL_";
private static final String IMPORTS = "___$$IMPORTS$$___";
private static final String TEXT = "___$$TEXT$$___";
private static String unwrapVals(List<String> vals) {
return "java.lang.Object[] |vals = new java.lang.Object[]{" + StringUtil.join(vals, ",") + "};\n" +
"java.lang.Object[] |resVals = new java.lang.Object[" + vals.size() + "];\n" +
"for (int |iii =0; |iii<|vals.length; |iii++){java.lang.Object |o = |vals[|iii];\n" +
"if (|o instanceof groovy.lang.Reference) {|o = ((groovy.lang.Reference)|o).get();}\n" +
"|resVals[|iii] = |o;" +
"}\n";
}
@Override
public JavaCodeFragment createCodeFragment(TextWithImports textWithImports, PsiElement context, Project project) {
final Pair<Map<String, String>, GroovyFile> pair = externalParameters(textWithImports.getText(), context);
GroovyFile toEval = pair.second;
final Map<String, String> parameters = pair.first;
List<String> names = new ArrayList<String>(parameters.keySet());
List<String> values = ContainerUtil.map(names, new Function<String, String>() {
@Override
public String fun(String name) {
return parameters.get(name);
}
});
String text = toEval.getText();
final String groovyText = StringUtil.join(names, ", ") + "->" + stripImports(text, toEval);
PsiClass contextClass = PsiUtil.getContextClass(context);
boolean isStatic = isStaticContext(context);
StringBuilder javaText = new StringBuilder();
javaText.append("groovy.lang.MetaClass |mc;\n");
javaText.append("java.lang.Class |clazz;\n");
if (!isStatic) {
javaText.append("java.lang.Object |thiz0;\n");
PsiFile containingFile = context.getContainingFile();
if (containingFile.getContext() != null) {
containingFile = containingFile.getContext().getContainingFile();
}
String fileName = containingFile.getOriginalFile().getName();
String s = StringUtil.escapeStringCharacters(Pattern.quote(fileName));
// We believe what class is reloaded if stacktrace matches one of two patterns:
// 1.) [com.package.Foo$$ENLbVXwm.methodName(FileName.groovy:12), com.package.Foo$$DNLbVXwm.methodName(Unknown Source), *
// 2.) [com.package.Foo$$ENLbVXwm.methodName(FileName.groovy:12), * com.springsource.loaded. *
// Pattern below test this.
//javaText.append("System.out.println(java.util.Arrays.toString(new Exception().getStackTrace()));\n");
//javaText.append("System.out.println(\"\\\\[([^,()]+\\\\$\\\\$)[A-Za-z0-9]{8}(\\\\.[^,()]+)\\\\(" + s + ":\\\\d+\\\\), (\\\\1[A-Za-z0-9]{8}\\\\2\\\\(Unknown Source\\\\), |.+(?:com|org)\\\\.springsource\\\\.loaded\\\\.).+\")\n");
javaText.append(
"if (java.util.Arrays.toString(new Exception().getStackTrace()).matches(\"\\\\[([^,()]+\\\\$\\\\$)[A-Za-z0-9]{8}(\\\\.[^,()]+)\\\\(")
.append(s)
.append(
":\\\\d+\\\\), (\\\\1[A-Za-z0-9]{8}\\\\2\\\\(Unknown Source\\\\), $OR$.+(?:com$OR$org)\\\\.springsource\\\\.loaded\\\\.).+\")) {\n");
javaText.append(" |thiz0 = thiz;\n");
javaText.append(" } else {\n");
javaText.append(" |thiz0 = this;\n");
javaText.append(" }\n");
}
if (!isStatic) {
javaText.append("|clazz = |thiz0.getClass();\n");
javaText.append("|mc = |thiz0.getMetaClass();\n");
} else {
assert contextClass != null;
javaText.append("|clazz = java.lang.Class.forName(\"").append(ClassUtil.getJVMClassName(contextClass)).append("\");\n");
javaText.append("|mc = groovy.lang.GroovySystem.getMetaClassRegistry().getMetaClass(|clazz);\n");
}
javaText.append("final java.lang.ClassLoader |parentLoader = |clazz.getClassLoader();\n" +
" final groovy.lang.GroovyClassLoader |loader = new groovy.lang.GroovyClassLoader(|parentLoader);\n" +
" final java.lang.Class |c = |loader.parseClass(");
javaText.append("\"" + IMPORTS + "class DUMMY").append(" { ").append("public groovy.lang.Closure ")
.append(EVAL_NAME).append(" = {").append(TEXT).append("}}\"");
javaText.append(", \"DUMMY.groovy\");\n" +
" int |i;\n" +
" java.lang.reflect.Field[] |fields = |c.getFields();\n" +
" for (int |j = 0; |j < |fields.length; |j++) if (|fields[|j].getName().equals(\"_JETGROOVY_EVAL_\")) {|i = |j; break;}\n" +
" final java.lang.reflect.Field |field = |fields[|i];\n" +
" final java.lang.Object |closure = |field.get(|c.newInstance());\n");
javaText.append("groovy.lang.ExpandoMetaClass |emc = new groovy.lang.ExpandoMetaClass(|clazz);\n");
if (!isStatic) {
javaText.append("|closure.setDelegate(|thiz0);\n");
javaText.append("|emc.setProperty(\"").append(EVAL_NAME).append("\", |closure);\n");
} else {
javaText.append("|emc.getProperty(\"static\").setProperty(\"").append(EVAL_NAME).append("\", |closure);\n");
}
javaText.append("|emc.initialize();\n");
javaText.append(unwrapVals(values));
if (!isStatic) {
javaText.append("java.lang.Object |res = ((groovy.lang.MetaClassImpl)|emc).invokeMethod(|thiz0, \"").append(EVAL_NAME).append("\", |resVals);\n");
} else {
javaText.append("java.lang.Object |res = ((groovy.lang.MetaClassImpl)|emc).invokeStaticMethod(|clazz, \"").append(EVAL_NAME).append("\", |resVals);\n");
}
javaText.append("if (|res instanceof java.lang.Boolean) ((java.lang.Boolean) |res).booleanValue() else |res");
final PsiElementFactory factory = JavaPsiFacade.getInstance(toEval.getProject()).getElementFactory();
String hiddenJavaVars = StringUtil.replace(javaText.toString(), "|", "_$_" + new Random().nextInt(42));
hiddenJavaVars = hiddenJavaVars.replaceAll("\\$OR\\$", "|");
final String finalText = StringUtil.replace(StringUtil.replace(hiddenJavaVars, TEXT, groovyText), IMPORTS, textWithImports.getImports());
JavaCodeFragment result = JavaCodeFragmentFactory.getInstance(project).createCodeBlockCodeFragment(finalText, null, true);
if (contextClass != null) {
result.setThisType(factory.createType(contextClass));
}
return result;
}
public static Pair<Map<String, String>, GroovyFile> externalParameters(String text, @NotNull final PsiElement context) {
final GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(context.getProject());
final GroovyFile toEval = factory.createGroovyFile(text, false, context);
final GrClosableBlock closure = PsiTreeUtil.getParentOfType(context, GrClosableBlock.class);
final Map<String, String> parameters = new THashMap<String, String>();
final Map<GrExpression, String> replacements = new HashMap<GrExpression, String>();
toEval.accept(new GroovyRecursiveElementVisitor() {
@Override
public void visitReferenceExpression(GrReferenceExpression referenceExpression) {
super.visitReferenceExpression(referenceExpression);
if (PsiUtil.isThisReference(referenceExpression) || PsiUtil.isSuperReference(referenceExpression)) {
replaceWithReference(referenceExpression, "delegate");
return;
}
PsiElement resolved = referenceExpression.resolve();
if (resolved instanceof PsiMember && (resolved instanceof PsiClass || ((PsiMember)resolved).hasModifierProperty(PsiModifier.STATIC))) {
String qName = com.intellij.psi.util.PsiUtil.getMemberQualifiedName((PsiMember)resolved);
if (qName != null && qName.contains(".") && !referenceExpression.isQualified()) {
replaceWithReference(referenceExpression, qName);
return;
}
}
if (shouldDelegate(referenceExpression, resolved)) {
replaceWithReference(referenceExpression, "delegate." + referenceExpression.getReferenceName());
return;
}
if (resolved instanceof GrVariable && !(resolved instanceof GrField) && !PsiTreeUtil.isAncestor(toEval, resolved, false)) {
final String name = ((GrVariable)resolved).getName();
if (resolved instanceof ClosureSyntheticParameter && PsiTreeUtil.isAncestor(toEval, ((ClosureSyntheticParameter) resolved).getClosure(), false)) {
return;
}
if (resolved instanceof GrBindingVariable && !PsiTreeUtil.isAncestor(resolved.getContainingFile(), toEval, false)) {
return;
}
String value;
if (closure != null &&
PsiTreeUtil.findCommonParent(resolved, closure) != closure &&
!(resolved instanceof ClosureSyntheticParameter)) {
// Evaluating inside closure for outer variable definitions
// All non-local variables are accessed by references
value = "this." + name;
} else {
value = name;
}
parameters.put(name, value);
}
}
private boolean shouldDelegate(GrReferenceExpression referenceExpression, @Nullable PsiElement resolved) {
if (referenceExpression.isQualified()) {
return false;
}
if (resolved instanceof GrField) {
return true;
}
if (resolved instanceof PsiMethod && !referenceExpression.isQualified()) {
String methodName = ((PsiMethod)resolved).getName();
if (closure != null && "getDelegate".equals(methodName) || "call".equals(methodName)) {
return true;
}
}
return closure != null && resolved instanceof GrLightVariable && "owner".equals(((GrLightVariable)resolved).getName());
}
private void replaceWithReference(GrExpression expr, final String exprText) {
replacements.put(expr, exprText);
}
@Override
public void visitCodeReferenceElement(GrCodeReferenceElement refElement) {
super.visitCodeReferenceElement(refElement);
if (refElement.getQualifier() == null) {
PsiElement resolved = refElement.resolve();
if (resolved instanceof PsiClass) {
String qName = ((PsiClass)resolved).getQualifiedName();
if (qName != null) {
int dotIndex = qName.lastIndexOf(".");
if (dotIndex < 0) return;
String packageName = qName.substring(0, dotIndex);
refElement.setQualifier(factory.createReferenceElementFromText(packageName));
}
}
}
}
});
for (GrExpression expression : replacements.keySet()) {
expression.replaceWithExpression(factory.createExpressionFromText(replacements.get(expression)), false);
}
return Pair.create(parameters, toEval);
}
private static String stripImports(String text, GroovyFile toEval) {
GrImportStatement[] imports = toEval.getImportStatements();
for (int i = imports.length - 1; i >= 0; i--) {
TextRange range = imports[i].getTextRange();
text = text.substring(0, range.getStartOffset()) + text.substring(range.getEndOffset(), text.length());
}
return StringUtil.escapeStringCharacters(text);
}
@Override
public JavaCodeFragment createPresentationCodeFragment(TextWithImports item, PsiElement context, Project project) {
GroovyCodeFragment result = new GroovyCodeFragment(project, item.getText());
result.setContext(context);
return result;
}
private static boolean isStaticContext(PsiElement context) {
PsiElement parent = context;
while (parent != null) {
if (parent instanceof PsiMember) {
return ((PsiMember)parent).hasModifierProperty(PsiModifier.STATIC);
}
if (parent instanceof GroovyFile && parent.isPhysical()) return false;
if (parent instanceof GrClosableBlock) return false;
parent = parent.getContext();
}
return false;
}
@Override
public boolean isContextAccepted(PsiElement context) {
return context != null && context.getLanguage().equals(GroovyLanguage.INSTANCE);
}
@Override
public LanguageFileType getFileType() {
return GroovyFileType.GROOVY_FILE_TYPE;
}
@Override
public EvaluatorBuilder getEvaluatorBuilder() {
return EvaluatorBuilderImpl.getInstance();
}
}