Bug 562056 - Cannot evaluate expressions in JSR-45 adopted classes

When debugging Xtext based DSL with Java sources generated from DSL,
evaluations of expressions in context of the lambda frames no longer
work.

The class files generated by compiler from Java sources (that are
generated from DSL sources) are changed by Xtext to contain source line
information that matches original DSL sources, and not Java sources (see
JSR-45 https://jcp.org/en/jsr/detail?id=45).

Under these conditions, line numbers provided by JDIStackFrame do not
match actual compilation unit lines and so
JDIStackFrame.LambdaASTVisitor.visit(LambdaExpression) will not be able
to adjust captured lambda variable names to be human readable form (the
method will return without doing anything). So the fix for bug 561542
will not work, causing the fix for bug 560392 to produce a broken
variable name.

E.g. evaluation engine will try to create & evaluate expression that
contains variable name "1" instead of "arg$1". The invalid variable name
causes compilation error that prevents evaluation of the expression.

This change fixes lambda captured variable names resolving while
debugging code generated from non-Java source code and avoids errors
while evaluating expressions in context of such code:

- we do not try to resolve captured variable names from Java sources if
we know the code was not written in Java,
- we do not apply the fix for bug 560392 if real names of captured
lambda variables are not provided.

Note: both tryToResolveLambdaVariableNames() and isProbablyJavaCode()
are made protected for non-Java based debugger implementations, in case
they want provide this information from their languages.

Change-Id: Ifa6ef1baa8a72a4a811d2dfe4526be38c422f1f4
Signed-off-by: Simeon Andreev <simeon.danailov.andreev@gmail.com>
Signed-off-by: Andrey Loskutov <loskutov@gmx.de>
diff --git a/org.eclipse.jdt.debug.tests/java8/Bug562056.java b/org.eclipse.jdt.debug.tests/java8/Bug562056.java
new file mode 100644
index 0000000..3473703
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/java8/Bug562056.java
@@ -0,0 +1,16 @@
+public class Bug562056 {
+	Object handler = "Hello bug 562056";
+
+	public static void main(String[] args) {
+		(new Bug562056()).run();
+	}
+
+	private Runnable r = () -> {
+		String string = handler.toString(); // breakpoint here, inspect handler.toString()
+		System.out.println(string);
+	};
+
+	public void run() {
+		r.run();
+	}
+}
diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java
index 08c87d2..537c152 100644
--- a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java
+++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java
@@ -474,6 +474,7 @@
 				cfgs.add(createLaunchConfiguration(jp, "Bug404097BreakpointUsingLocalClass"));
 				cfgs.add(createLaunchConfiguration(jp, "Bug560392"));
 				cfgs.add(createLaunchConfiguration(jp, "Bug561715"));
+				cfgs.add(createLaunchConfiguration(jp, "Bug562056"));
 	    		loaded18 = true;
 	    		waitForBuild();
 	        }
diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/eval/LambdaVariableTest.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/eval/LambdaVariableTest.java
index e40b9da..621b316 100644
--- a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/eval/LambdaVariableTest.java
+++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/eval/LambdaVariableTest.java
@@ -30,7 +30,7 @@
 		super(name);
 	}
 
-	public void testEvaluate_LambdaFieldVariable() throws Exception {
+	public void testEvaluate_LambdaCapturedParameter() throws Exception {
 		debugWithBreakpoint("Bug560392", 9);
 		String snippet = "key";
 		IValue value = doEval(javaThread, snippet);
@@ -39,6 +39,15 @@
 		assertEquals("wrong result : ", "a", value.getValueString());
 	}
 
+	public void testEvaluate_LambdaCapturedField() throws Exception {
+		debugWithBreakpoint("Bug562056", 9);
+		String snippet = "handler.toString()";
+		IValue value = doEval(javaThread, snippet);
+
+		assertEquals("wrong type : ", "java.lang.String", value.getReferenceTypeName());
+		assertEquals("wrong result : ", "Hello bug 562056", value.getValueString());
+	}
+
 	private void debugWithBreakpoint(String testClass, int lineNumber) throws Exception {
 		createLineBreakpoint(lineNumber, testClass);
 		javaThread = launchToBreakpoint(testClass);
diff --git a/org.eclipse.jdt.debug/eval/org/eclipse/jdt/internal/debug/eval/ast/engine/ASTEvaluationEngine.java b/org.eclipse.jdt.debug/eval/org/eclipse/jdt/internal/debug/eval/ast/engine/ASTEvaluationEngine.java
index fa25001..fc682b1 100644
--- a/org.eclipse.jdt.debug/eval/org/eclipse/jdt/internal/debug/eval/ast/engine/ASTEvaluationEngine.java
+++ b/org.eclipse.jdt.debug/eval/org/eclipse/jdt/internal/debug/eval/ast/engine/ASTEvaluationEngine.java
@@ -331,7 +331,8 @@
 				if (variable instanceof IJavaVariable && !isLambdaOrImplicitVariable(variable)) {
 					IJavaVariable javaVariable = (IJavaVariable) variable;
 					final boolean lambdaField = LambdaUtils.isLambdaField(variable);
-					String variableName = (lambdaField) ? variable.getName().substring(ANONYMOUS_VAR_PREFIX.length()) : variable.getName();
+					String name = variable.getName();
+					String variableName = (lambdaField && name.startsWith(ANONYMOUS_VAR_PREFIX)) ? name.substring(ANONYMOUS_VAR_PREFIX.length()) : name;
 					if (variableName != null && (!variableName.contains("$") || lambdaField)) { //$NON-NLS-1$
 						if (!isLocalType(javaVariable.getSignature()) && !names.contains(variableName)) {
 							locals[numLocals] = javaVariable;
diff --git a/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIStackFrame.java b/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIStackFrame.java
index 4deac01..f7b5255 100644
--- a/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIStackFrame.java
+++ b/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/JDIStackFrame.java
@@ -384,7 +384,7 @@
 						IJavaStackFrame previousFrame = frames.get(previousIndex);
 						ObjectReference underlyingThisObject = ((JDIStackFrame) previousFrame).getUnderlyingThisObject();
 						IJavaValue closureValue = JDIValue.createValue((JDIDebugTarget) getDebugTarget(), underlyingThisObject);
-						setLambdaVariableNames(closureValue, underlyingThisObject);
+						tryToResolveLambdaVariableNames(closureValue, underlyingThisObject);
 						fVariables.add(new JDILambdaVariable(closureValue));
 					}
 				}
@@ -404,7 +404,15 @@
 		}
 	}
 
-	private void setLambdaVariableNames(IJavaValue value, ObjectReference underlyingThisObject) {
+	/**
+	 * Tries to resolve "real" captured variable names by inspecting corresponding Java source code (if available)
+	 */
+	protected void tryToResolveLambdaVariableNames(IJavaValue value, ObjectReference underlyingThisObject) {
+		if (!isProbablyJavaCode()) {
+			// See bug 562056: we won't parse Java code if the current frame doesn't belong to Java, because
+			// we will most likely have different source line numbers and will produce garbage or errors
+			return;
+		}
 		try {
 			IType type = JavaDebugUtils.resolveType(value.getJavaType());
 			if (type == null) {
@@ -427,6 +435,27 @@
 		}
 	}
 
+	/**
+	 * @return {@code true} if the current frame relates to the class generated from Java source file (and not from some different language)
+	 */
+	protected boolean isProbablyJavaCode() {
+		try {
+			String sourceName = getSourceName();
+			// Note: JavaCore.isJavaLikeFileName(sourceName) is too generic to be used here
+			// because it allows files that aren't using Java syntax, like groovy
+			// See https://github.com/groovy/groovy-eclipse/blob/master/base/org.eclipse.jdt.groovy.core/plugin.xml
+			if (sourceName == null || sourceName.endsWith(".java")) { //$NON-NLS-1$
+				// if nothing is defined (no source attributes), assume Java
+				return true;
+			}
+		} catch (DebugException e) {
+			// If we fail, assume Java
+			return true;
+		}
+		// Underlined source code is most likely not written in Java
+		return false;
+	}
+
 	private final static class LambdaASTVisitor extends ASTVisitor {
 		private final ObjectReference underlyingThisObject;
 		private boolean methodIsStatic;