Bug 560392 - Evaluate Lambda Field variables

When passing method parameters to a returning lambda object from the
method the variable is embedded into lambda object as val$variable_name.
This naming convention cause the evaluator to skip this variable pushed
into evaluator engine and also to not matched with the generated
snippet.

This fix provide way to push this variable into evaluator engine and at
the same time the generated snippet is adjusted to the above mentioned
naming convention,

Change-Id: I3338ab467e107166690b06e05510981afe0b0fe3
Signed-off-by: gayanper <gayanper@gmail.com>
diff --git a/org.eclipse.jdt.debug.tests/java8/Bug560392.java b/org.eclipse.jdt.debug.tests/java8/Bug560392.java
new file mode 100644
index 0000000..1c98324
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/java8/Bug560392.java
@@ -0,0 +1,16 @@
+import java.util.Arrays;
+import java.util.function.Predicate;
+
+public class Bug560392 {
+
+	public static Predicate<String> breakpointMethod(String key) {
+		return (s) -> {
+			System.out.println("Key" + key);
+			return s.contains(key);
+		};
+	}
+
+	public static void main(String[] args) {
+		Arrays.asList("111", "222", "aaa").stream().filter(breakpointMethod("a")).count();
+	}
+}
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 9aa6ac1..ecf53d6 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
@@ -471,6 +471,7 @@
 				cfgs.add(createLaunchConfiguration(jp, "Bug404097BreakpointInLambda"));
 				cfgs.add(createLaunchConfiguration(jp, "Bug404097BreakpointUsingInnerClass"));
 				cfgs.add(createLaunchConfiguration(jp, "Bug404097BreakpointUsingLocalClass"));
+				cfgs.add(createLaunchConfiguration(jp, "Bug560392"));
 	    		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
new file mode 100644
index 0000000..e40b9da
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/eval/LambdaVariableTest.java
@@ -0,0 +1,57 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Gayan Perera and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Gayan Perera - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.debug.tests.eval;
+
+import org.eclipse.debug.core.model.IValue;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.debug.core.IJavaThread;
+import org.eclipse.jdt.debug.tests.AbstractDebugTest;
+
+public class LambdaVariableTest extends AbstractDebugTest {
+	private IJavaThread javaThread;
+
+	@Override
+	protected IJavaProject getProjectContext() {
+		return get18Project();
+	}
+
+	public LambdaVariableTest(String name) {
+		super(name);
+	}
+
+	public void testEvaluate_LambdaFieldVariable() throws Exception {
+		debugWithBreakpoint("Bug560392", 9);
+		String snippet = "key";
+		IValue value = doEval(javaThread, snippet);
+
+		assertEquals("wrong type : ", "java.lang.String", value.getReferenceTypeName());
+		assertEquals("wrong result : ", "a", value.getValueString());
+	}
+
+	private void debugWithBreakpoint(String testClass, int lineNumber) throws Exception {
+		createLineBreakpoint(lineNumber, testClass);
+		javaThread = launchToBreakpoint(testClass);
+		assertNotNull("The program did not suspend", javaThread);
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		try {
+			terminateAndRemove(javaThread);
+		} finally {
+			super.tearDown();
+			removeAllBreakpoints();
+		}
+	}
+}
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 1b5f8f6..fa25001 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
@@ -330,12 +330,13 @@
 			for (IVariable variable : lambdaFrameVariables) {
 				if (variable instanceof IJavaVariable && !isLambdaOrImplicitVariable(variable)) {
 					IJavaVariable javaVariable = (IJavaVariable) variable;
-					String variableName = variable.getName();
-					if (variableName != null && !variableName.contains("$")) { //$NON-NLS-1$
+					final boolean lambdaField = LambdaUtils.isLambdaField(variable);
+					String variableName = (lambdaField) ? variable.getName().substring(ANONYMOUS_VAR_PREFIX.length()) : variable.getName();
+					if (variableName != null && (!variableName.contains("$") || lambdaField)) { //$NON-NLS-1$
 						if (!isLocalType(javaVariable.getSignature()) && !names.contains(variableName)) {
 							locals[numLocals] = javaVariable;
-							names.add(variable.getName());
-							localVariablesWithNull[numLocals++] = variable.getName();
+							names.add(variableName);
+							localVariablesWithNull[numLocals++] = variableName;
 						}
 					}
 				}
@@ -355,8 +356,7 @@
 			// ******
 			String[] localTypesNames = new String[numLocals];
 			for (int i = 0; i < numLocals; i++) {
-				localTypesNames[i] = Signature.toString(
-						locals[i].getGenericSignature()).replace('/', '.');
+				localTypesNames[i] = getFixedUnresolvableGenericTypes(locals[i]);
 			}
 			// Copying local variables removing the nulls in the last
 			// String[] localVariables = Arrays.clonesub(localVariablesWithNull, names.size());
@@ -392,6 +392,21 @@
 		return createExpressionFromAST(snippet, mapper, unit);
 	}
 
+	private String getFixedUnresolvableGenericTypes(IJavaVariable variable) throws DebugException {
+		/*
+		 * This actually fix variables which are type of Generic Types which cannot be resolved to a type in the current content. For example variable
+		 * type like P_OUT in java.util.stream.ReferencePipeline.filter(Predicate<? super P_OUT>)
+		 */
+
+		final String genericSignature = variable.getGenericSignature();
+		final String fqn = Signature.toString(genericSignature).replace('/', '.');
+		if (genericSignature.startsWith(String.valueOf(Signature.C_TYPE_VARIABLE))) {
+			// resolve to the signature of the variable.
+			return Signature.toString(variable.getSignature()).replace('/', '.');
+		}
+		return fqn;
+	}
+
 	private CompilationUnit parseCompilationUnit(char[] source,
 			String unitName, IJavaProject project) {
 		return parseCompilationUnit(source, unitName, project, Collections.EMPTY_MAP);
diff --git a/org.eclipse.jdt.debug/eval/org/eclipse/jdt/internal/debug/eval/ast/instructions/PushLocalVariable.java b/org.eclipse.jdt.debug/eval/org/eclipse/jdt/internal/debug/eval/ast/instructions/PushLocalVariable.java
index f7c3cee..db4817a 100644
--- a/org.eclipse.jdt.debug/eval/org/eclipse/jdt/internal/debug/eval/ast/instructions/PushLocalVariable.java
+++ b/org.eclipse.jdt.debug/eval/org/eclipse/jdt/internal/debug/eval/ast/instructions/PushLocalVariable.java
@@ -70,6 +70,14 @@
 			push(variable);
 			return;
 		}
+
+		// Try search for lambda object variable
+		variable = LambdaUtils.findLambdaFrameVariable(context, ASTEvaluationEngine.ANONYMOUS_VAR_PREFIX + getName());
+		if (variable != null) {
+			push(variable);
+			return;
+		}
+
 		throw new CoreException(
 				new Status(
 						IStatus.ERROR,
diff --git a/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/LambdaUtils.java b/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/LambdaUtils.java
index 851bc81..271ba79 100644
--- a/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/LambdaUtils.java
+++ b/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/LambdaUtils.java
@@ -23,8 +23,11 @@
 import org.eclipse.debug.core.model.IStackFrame;
 import org.eclipse.debug.core.model.IThread;
 import org.eclipse.debug.core.model.IVariable;
+import org.eclipse.jdt.debug.core.IJavaFieldVariable;
+import org.eclipse.jdt.debug.core.IJavaObject;
 import org.eclipse.jdt.debug.core.IJavaStackFrame;
 import org.eclipse.jdt.debug.core.IJavaThread;
+import org.eclipse.jdt.internal.debug.core.logicalstructures.JDILambdaVariable;
 import org.eclipse.jdt.internal.debug.eval.ast.engine.IRuntimeContext;
 
 /**
@@ -87,6 +90,11 @@
 				IStackFrame stackFrame = stackFrames[i];
 				IVariable[] stackFrameVariables = stackFrame.getVariables();
 				variables.addAll(Arrays.asList(stackFrameVariables));
+				for (IVariable frameVariable : stackFrameVariables) {
+					if (isLambdaObjectVariable(frameVariable)) {
+						variables.addAll(extractVariablesFromLambda(frameVariable));
+					}
+				}
 			}
 		}
 		return Collections.unmodifiableList(variables);
@@ -114,4 +122,27 @@
 	public static boolean isLambdaFrame(IJavaStackFrame frame) throws DebugException {
 		return frame.isSynthetic() && frame.getName().startsWith("lambda$"); //$NON-NLS-1$
 	}
+
+	/**
+	 * Returns if the variable represent a variable embedded into Lambda object.
+	 *
+	 * @param variable
+	 *            the variable which needs to be evaluated
+	 * @return <code>True</code> if the variable is inside the Lambda object else return <code>False</code>
+	 * @since 3.15
+	 */
+	public static boolean isLambdaField(IVariable variable) throws DebugException {
+		return (variable instanceof IJavaFieldVariable) && ((IJavaFieldVariable) variable).getDeclaringType().getName().contains("$Lambda$"); //$NON-NLS-1$
+	}
+
+	private static boolean isLambdaObjectVariable(IVariable variable) {
+		return variable instanceof JDILambdaVariable;
+	}
+
+	private static List<IVariable> extractVariablesFromLambda(IVariable variable) throws DebugException {
+		if (variable.getValue() instanceof IJavaObject) {
+			return Arrays.asList(variable.getValue().getVariables());
+		}
+		return Collections.emptyList();
+	}
 }