Bug 534687 - [1.8] Lambda variable names in variables view is shown
as arg$1 etc

Debugger shows captured lambda variable names as val$outerArg instead of
arg$1 etc.

Change-Id: I84b6e65b3db99fd7ed53ab2c82df437d4dc61391
Signed-off-by: Andrey Loskutov <loskutov@gmx.de>
diff --git a/org.eclipse.jdt.debug.tests/java8/DebugHoverTest18.java b/org.eclipse.jdt.debug.tests/java8/DebugHoverTest18.java
index 9ecd81b..d712774 100644
--- a/org.eclipse.jdt.debug.tests/java8/DebugHoverTest18.java
+++ b/org.eclipse.jdt.debug.tests/java8/DebugHoverTest18.java
@@ -34,9 +34,9 @@
 			System.out.println(arg);
 
 			run(()->{
-				String var3 = "v2";
-				System.out.println(var2);
+				String var3 = "v3";
 				System.out.println(var3);
+				System.out.println(var2);
 				System.out.println(var1);
 				System.out.println(arg);
 			});
diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/ui/DebugHoverTests.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/ui/DebugHoverTests.java
index d9177b6..ac271dd 100644
--- a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/ui/DebugHoverTests.java
+++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/ui/DebugHoverTests.java
@@ -31,7 +31,7 @@
 import org.eclipse.jdt.debug.core.IJavaThread;
 import org.eclipse.jdt.debug.tests.TestUtil;
 import org.eclipse.jdt.debug.ui.IJavaDebugUIConstants;
-import org.eclipse.jdt.internal.debug.core.model.JDILocalVariable;
+import org.eclipse.jdt.internal.debug.core.model.JDIModificationVariable;
 import org.eclipse.jdt.internal.debug.core.model.JDIObjectValue;
 import org.eclipse.jdt.internal.debug.core.model.JDIValue;
 import org.eclipse.jdt.internal.debug.ui.JDIDebugUIPlugin;
@@ -50,6 +50,8 @@
  */
 public class DebugHoverTests extends AbstractDebugUiTests {
 
+	private static final String VAL_PREFIX = new String(org.eclipse.jdt.internal.compiler.lookup.TypeConstants.SYNTHETIC_OUTER_LOCAL_PREFIX);
+
 	public static Test suite() {
 		return new OrderedTestSuite(DebugHoverTests.class);
 	}
@@ -109,9 +111,9 @@
 			hover.setEditor(part);
 
 			Map<String, Region> offsets = new LinkedHashMap<>();
-			offsets.put("arg", new Region(1059, "arg".length()));
-			offsets.put("var1", new Region(1030, "var1".length()));
-			offsets.put("var2", new Region(1001, "var2".length()));
+			offsets.put(VAL_PREFIX + "arg", new Region(1059, "arg".length()));
+			offsets.put(VAL_PREFIX + "var1", new Region(1030, "var1".length()));
+			offsets.put(/* local */ "var2", new Region(1001, "var2".length()));
 
 			Set<Entry<String, Region>> entrySet = offsets.entrySet();
 			int startLine = bpLine1;
@@ -125,12 +127,10 @@
 			part = openEditorAndValidateStack(expectedMethod2, framesNumber2, file, thread);
 
 			offsets = new LinkedHashMap<>();
-			offsets.put("arg", new Region(1216, "arg".length()));
-			offsets.put("var1", new Region(1186, "var1".length()));
-			offsets.put("var3", new Region(1156, "var3".length()));
-			// This will not work yet, I have no idea how to identify parent
-			// lambda element...
-			// offsets.put("var2", new Region(1108, "var2".length()));
+			offsets.put(VAL_PREFIX + "arg", new Region(1216, "arg".length()));
+			offsets.put(VAL_PREFIX + "var1", new Region(1186, "var1".length()));
+			offsets.put(VAL_PREFIX + "var2", new Region(1156, "var2".length()));
+			offsets.put(/* local */ "var3", new Region(1126, "var3".length()));
 
 			entrySet = offsets.entrySet();
 			startLine = bpLine2;
@@ -166,15 +166,16 @@
 	}
 
 	private void validateLine(final int line, int valueIndex, CompilationUnitEditor part, JavaDebugHover hover, Entry<String, Region> varData) throws Exception, DebugException {
-		String variableName = varData.getKey();
+		String debugVarName = varData.getKey();
+		String variableName = debugVarName.startsWith(VAL_PREFIX) ? debugVarName.substring(VAL_PREFIX.length()) : debugVarName;
 		IRegion region = varData.getValue();
 		String text = selectAndReveal(part, line, region);
 		assertEquals(variableName, text);
 		Object args = sync(() -> hover.getHoverInfo2(part.getViewer(), region));
 
 		assertNotNull(args);
-		JDILocalVariable var = (JDILocalVariable) args;
-		assertEquals(variableName, var.getName());
+		JDIModificationVariable var = (JDIModificationVariable) args;
+		assertEquals(debugVarName, var.getName());
 		JDIValue value = (JDIValue) var.getValue();
 		assertEquals(JDIObjectValue.class, value.getClass());
 		JDIObjectValue valueObj = (JDIObjectValue) var.getValue();
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 e292189..9a56d7e 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
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2000, 2018 IBM Corporation and others.
+ * Copyright (c) 2000, 2019 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -19,7 +19,9 @@
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
+import java.util.ListIterator;
 
+import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IAdaptable;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Status;
@@ -32,9 +34,19 @@
 import org.eclipse.debug.core.model.ITerminate;
 import org.eclipse.debug.core.model.IThread;
 import org.eclipse.debug.core.model.IVariable;
+import org.eclipse.jdi.internal.FieldImpl;
+import org.eclipse.jdi.internal.ReferenceTypeImpl;
 import org.eclipse.jdi.internal.ValueImpl;
 import org.eclipse.jdi.internal.VirtualMachineImpl;
+import org.eclipse.jdt.core.IType;
 import org.eclipse.jdt.core.Signature;
+import org.eclipse.jdt.core.dom.AST;
+import org.eclipse.jdt.core.dom.ASTParser;
+import org.eclipse.jdt.core.dom.ASTVisitor;
+import org.eclipse.jdt.core.dom.CompilationUnit;
+import org.eclipse.jdt.core.dom.IMethodBinding;
+import org.eclipse.jdt.core.dom.IVariableBinding;
+import org.eclipse.jdt.core.dom.LambdaExpression;
 import org.eclipse.jdt.debug.core.IJavaClassType;
 import org.eclipse.jdt.debug.core.IJavaModifiers;
 import org.eclipse.jdt.debug.core.IJavaObject;
@@ -44,6 +56,7 @@
 import org.eclipse.jdt.debug.core.IJavaValue;
 import org.eclipse.jdt.debug.core.IJavaVariable;
 import org.eclipse.jdt.internal.debug.core.JDIDebugPlugin;
+import org.eclipse.jdt.internal.debug.core.JavaDebugUtils;
 import org.eclipse.jdt.internal.debug.core.logicalstructures.JDILambdaVariable;
 import org.eclipse.jdt.internal.debug.core.logicalstructures.JDIReturnValueVariable;
 import org.eclipse.jdt.internal.debug.core.model.MethodResult.ResultType;
@@ -128,6 +141,9 @@
 	 */
 	private boolean fIsTop;
 
+	@SuppressWarnings("restriction")
+	private static final String SYNTHETIC_OUTER_LOCAL_PREFIX = new String(org.eclipse.jdt.internal.compiler.lookup.TypeConstants.SYNTHETIC_OUTER_LOCAL_PREFIX);
+
 	/**
 	 * Creates a new stack frame in the given thread.
 	 *
@@ -366,7 +382,9 @@
 					int previousIndex = frames.indexOf(this) + 1;
 					if (previousIndex > 0 && previousIndex < frames.size()) {
 						IJavaStackFrame previousFrame = frames.get(previousIndex);
-						IJavaValue closureValue = JDIValue.createValue((JDIDebugTarget) getDebugTarget(), ((JDIStackFrame) previousFrame).getUnderlyingThisObject());
+						ObjectReference underlyingThisObject = ((JDIStackFrame) previousFrame).getUnderlyingThisObject();
+						IJavaValue closureValue = JDIValue.createValue((JDIDebugTarget) getDebugTarget(), underlyingThisObject);
+						setLambdaVariableNames(closureValue, underlyingThisObject);
 						fVariables.add(new JDILambdaVariable(closureValue));
 					}
 				}
@@ -386,6 +404,73 @@
 		}
 	}
 
+	private void setLambdaVariableNames(IJavaValue value, ObjectReference underlyingThisObject) {
+		try {
+			IType type = JavaDebugUtils.resolveType(value.getJavaType());
+			ASTParser parser = ASTParser.newParser(AST.JLS11);
+			parser.setResolveBindings(true);
+			parser.setSource(type.getTypeRoot());
+			CompilationUnit cu = (CompilationUnit) parser.createAST(null);
+			cu.accept(new LambdaASTVisitor(false, underlyingThisObject, getUnderlyingMethod()));
+		} catch (CoreException e) {
+			logError(e);
+		}
+	}
+
+	private final static class LambdaASTVisitor extends ASTVisitor {
+		private final ObjectReference underlyingThisObject;
+		private Method underlyingMethod;
+
+		private LambdaASTVisitor(boolean visitDocTags, ObjectReference underlyingThisObject, Method underlyingMethod) {
+			super(visitDocTags);
+			this.underlyingThisObject = underlyingThisObject;
+			this.underlyingMethod = underlyingMethod;
+		}
+
+		@Override
+		public boolean visit(LambdaExpression lambdaExpression) {
+			IMethodBinding binding = lambdaExpression.resolveMethodBinding();
+			IVariableBinding[] synVars = binding.getSyntheticOuterLocals();
+			if (synVars == null || synVars.length == 0) {// name cannot be updated if Synthetic Outer Locals are not available
+				return true;
+			}
+			List<Field> allFields = underlyingThisObject.referenceType().fields();
+			ListIterator<Field> listIterator = allFields.listIterator();
+			int i = 0;
+			if (underlyingMethod.isStatic()) {
+				if (synVars.length == allFields.size()) {
+					while (listIterator.hasNext()) {
+						FieldImpl field = (FieldImpl) listIterator.next();
+						String newName = synVars[i].getName();
+						FieldImpl newField = createRenamedCopy(field, newName);
+						listIterator.set(newField);
+						i++;
+					}
+				}
+			} else {
+				if (synVars.length + 1 == allFields.size()) {
+					while (listIterator.hasNext()) {
+						FieldImpl field = (FieldImpl) listIterator.next();
+						String newName = field.name();
+						if (i == 0) {
+							newName = "this"; //$NON-NLS-1$
+						} else {
+							newName = synVars[i - 1].getName();
+						}
+						FieldImpl newField = createRenamedCopy(field, newName);
+						listIterator.set(newField);
+						i++;
+					}
+				}
+			}
+			return true;
+		}
+
+		private FieldImpl createRenamedCopy(FieldImpl field, String newName) {
+			return new FieldImpl((VirtualMachineImpl) field.virtualMachine(), (ReferenceTypeImpl) field.declaringType(), field.getFieldID(), newName, field.signature(), field.genericSignature(), field.modifiers());
+		}
+	}
+
 	/**
 	 * If there is a return value from a "step return" that belongs to this frame, insert it as first element
 	 *
@@ -791,6 +876,19 @@
 				// save for later - check for instance and static variables
 				thisVariable = var;
 			}
+			if (var instanceof JDILambdaVariable) {
+				// Check if we have match in synthetic fields generated
+				// by compiler for the captured variables (they start with "val$")
+				JDILambdaVariable lambda = (JDILambdaVariable) var;
+				JDIObjectValue ov = (JDIObjectValue) lambda.getValue();
+				IVariable[] lvars = ov.getVariables();
+				for (IVariable lv : lvars) {
+					String name = lv.getName();
+					if (name.startsWith(SYNTHETIC_OUTER_LOCAL_PREFIX) && (SYNTHETIC_OUTER_LOCAL_PREFIX + varName).equals(name)) {
+						possibleMatches.add((IJavaVariable) lv);
+					}
+				}
+			}
 		}
 		for(IJavaVariable variable: possibleMatches){
 			// Local Variable has more preference than Field Variable