Bug 562795: handle outer local variables

This fix expose outer local variables which are not available in the
nearest outer object to the debug frame.

Change-Id: Ifb5047f769acf4922e9222b537e5953c74002fcb
Signed-off-by: gayanper <gayanper@gmail.com>
diff --git a/org.eclipse.jdt.debug.tests/testresources/synthetic/bin/Bar.class b/org.eclipse.jdt.debug.tests/testresources/synthetic/bin/Bar.class
new file mode 100644
index 0000000..fb2a878
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/testresources/synthetic/bin/Bar.class
Binary files differ
diff --git a/org.eclipse.jdt.debug.tests/testresources/synthetic/bin/Foo.class b/org.eclipse.jdt.debug.tests/testresources/synthetic/bin/Foo.class
new file mode 100644
index 0000000..7346879
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/testresources/synthetic/bin/Foo.class
Binary files differ
diff --git a/org.eclipse.jdt.debug.tests/testresources/synthetic/bin/SyntheticTest$1$1.class b/org.eclipse.jdt.debug.tests/testresources/synthetic/bin/SyntheticTest$1$1.class
new file mode 100644
index 0000000..f3bc8dc
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/testresources/synthetic/bin/SyntheticTest$1$1.class
Binary files differ
diff --git a/org.eclipse.jdt.debug.tests/testresources/synthetic/bin/SyntheticTest$1.class b/org.eclipse.jdt.debug.tests/testresources/synthetic/bin/SyntheticTest$1.class
new file mode 100644
index 0000000..1bf3317
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/testresources/synthetic/bin/SyntheticTest$1.class
Binary files differ
diff --git a/org.eclipse.jdt.debug.tests/testresources/synthetic/bin/SyntheticTest.class b/org.eclipse.jdt.debug.tests/testresources/synthetic/bin/SyntheticTest.class
new file mode 100644
index 0000000..b4d7b63
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/testresources/synthetic/bin/SyntheticTest.class
Binary files differ
diff --git a/org.eclipse.jdt.debug.tests/testresources/synthetic/src/Bar.java b/org.eclipse.jdt.debug.tests/testresources/synthetic/src/Bar.java
new file mode 100644
index 0000000..d1f6355
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/testresources/synthetic/src/Bar.java
@@ -0,0 +1,17 @@
+/*******************************************************************************
+ * 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
+ *******************************************************************************/
+
+public abstract class Bar {
+	public abstract Foo bar(String vbar);
+}
diff --git a/org.eclipse.jdt.debug.tests/testresources/synthetic/src/Foo.java b/org.eclipse.jdt.debug.tests/testresources/synthetic/src/Foo.java
new file mode 100644
index 0000000..d3cff81
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/testresources/synthetic/src/Foo.java
@@ -0,0 +1,17 @@
+/*******************************************************************************
+ * 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
+ *******************************************************************************/
+
+public abstract class Foo {
+	public abstract String foo(String foo);
+}
diff --git a/org.eclipse.jdt.debug.tests/testresources/synthetic/src/SyntheticTest.java b/org.eclipse.jdt.debug.tests/testresources/synthetic/src/SyntheticTest.java
new file mode 100644
index 0000000..fe5e928
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/testresources/synthetic/src/SyntheticTest.java
@@ -0,0 +1,37 @@
+/*******************************************************************************
+ * 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
+ *******************************************************************************/
+import java.util.function.Predicate;
+
+public class SyntheticTest {
+	public static void main(String[] args) {
+		(new SyntheticTest()).exec(s -> s.isEmpty()).bar("bar").foo("foo");
+	}
+
+	public Bar exec(Predicate<String> predicate) {
+		return new Bar() {
+			private Object bar;
+			@Override
+			public Foo bar(String vbar) {
+				return new Foo() {
+					private Object foo;
+					@Override
+					public String foo(String vfoo) {
+						predicate.test("vfoo");
+						return vfoo;
+					}
+				};
+			}
+		};
+	}
+}
diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java
index 8b73956..389144d 100644
--- a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java
+++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java
@@ -91,6 +91,7 @@
 import org.eclipse.jdt.debug.tests.core.WorkspaceSourceContainerTests;
 import org.eclipse.jdt.debug.tests.eval.GeneralEvalTests;
 import org.eclipse.jdt.debug.tests.eval.GenericsEvalTests;
+import org.eclipse.jdt.debug.tests.eval.SyntheticVariableTests;
 import org.eclipse.jdt.debug.tests.launching.ClasspathShortenerTests;
 import org.eclipse.jdt.debug.tests.launching.ConfigurationEncodingTests;
 import org.eclipse.jdt.debug.tests.launching.ConfigurationResourceMappingTests;
@@ -264,6 +265,7 @@
 		addTest(new TestSuite(JavaDebugTargetTests.class));
 		addTest(new TestSuite(WorkingDirectoryTests.class));
 		addTest(new TestSuite(EventDispatcherTest.class));
+		addTest(new TestSuite(SyntheticVariableTests.class));
 
 	// Refactoring tests
 		//TODO: project rename
diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/eval/SyntheticVariableTests.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/eval/SyntheticVariableTests.java
new file mode 100644
index 0000000..6d21d24
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/eval/SyntheticVariableTests.java
@@ -0,0 +1,177 @@
+/*******************************************************************************
+ * 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 java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.debug.core.DebugException;
+import org.eclipse.debug.core.model.IValue;
+import org.eclipse.debug.internal.ui.stringsubstitution.SelectedResourceManager;
+import org.eclipse.debug.ui.DebugUITools;
+import org.eclipse.debug.ui.contexts.IDebugContextListener;
+import org.eclipse.debug.ui.contexts.IDebugContextProvider;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.debug.core.IJavaThread;
+import org.eclipse.jdt.debug.core.JDIDebugModel;
+import org.eclipse.jdt.debug.testplugin.JavaProjectHelper;
+import org.eclipse.jdt.debug.testplugin.JavaTestPlugin;
+import org.eclipse.jdt.debug.tests.ui.AbstractDebugUiTests;
+import org.eclipse.jdt.internal.debug.ui.contentassist.CurrentFrameContext;
+import org.eclipse.jdt.internal.debug.ui.contentassist.JavaDebugContentAssistProcessor;
+import org.eclipse.jface.text.Document;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.TextViewer;
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkbenchWindow;
+
+public class SyntheticVariableTests extends AbstractDebugUiTests {
+
+	private IJavaThread javaThread;
+	private IDebugContextProvider debugContextProvider;
+	private IJavaProject project;
+
+	public SyntheticVariableTests(String name) {
+		super(name);
+		debugContextProvider = new IDebugContextProvider() {
+			@Override
+			public void removeDebugContextListener(IDebugContextListener listener) {
+			}
+
+			@Override
+			public IWorkbenchPart getPart() {
+				return null;
+			}
+
+			@Override
+			public ISelection getActiveContext() {
+				try {
+					return new StructuredSelection(javaThread.getTopStackFrame());
+				} catch (DebugException e) {
+					return null;
+				}
+			}
+
+			@Override
+			public void addDebugContextListener(IDebugContextListener listener) {
+			}
+		};
+	}
+
+	public void testEvaluateMethodParameter_DeepInTwoNestedClasses() throws Exception {
+		addClasses();
+		createBreakPoint(31);
+		try {
+			javaThread = launchToBreakpoint("SyntheticTest");
+
+			IValue value = doEval(javaThread, "predicate");
+
+			assertNotNull("type is null : ", value.getReferenceTypeName());
+			assertTrue("Not expected lambda type : ", value.getReferenceTypeName().startsWith("SyntheticTest$$Lambda"));
+			assertNotNull("value is null :", value.getValueString());
+		} finally {
+			terminateAndRemove(javaThread);
+			removeAllBreakpoints();
+		}
+	}
+
+	public void testCompleteMethodParameter_DeepInTwoNestedClasses() throws Exception {
+		addClasses();
+		createBreakPoint(31);
+		try {
+			javaThread = launchToBreakpoint("SyntheticTest");
+			registerContextProvider();
+			waitForBuild();
+			List<ICompletionProposal> proposals = computeCompletionProposals(" ", 0);
+
+			assertNotNull("proposals are null : ", proposals);
+			assertTrue("proposals are empty : ", !proposals.isEmpty());
+			System.out.println(proposals);
+			assertTrue("expected variable is not in proposals :", proposals.stream().anyMatch(p -> p.getDisplayString().equals("predicate : Predicate")));
+		} finally {
+			unregisterContextProvider();
+			terminateAndRemove(javaThread);
+			removeAllBreakpoints();
+		}
+	}
+
+	private List<ICompletionProposal> computeCompletionProposals(String source, int completionIndex) throws Exception {
+		JavaDebugContentAssistProcessor comp = new JavaDebugContentAssistProcessor(new CurrentFrameContext());
+		ICompletionProposal[] proposals = sync(new Callable<ICompletionProposal[]>() {
+
+			@Override
+			public ICompletionProposal[] call() throws Exception {
+				ITextViewer viewer = new TextViewer(Display.getDefault().getActiveShell(), SWT.NONE);
+				viewer.setDocument(new Document(source));
+				return comp.computeCompletionProposals(viewer, completionIndex);
+			}
+		});
+		assertNull(String.format("Has errors : %s", comp.getErrorMessage()), comp.getErrorMessage());
+		assertNotNull("proposals are null", proposals);
+
+		return Arrays.asList(proposals);
+	}
+
+	private void createBreakPoint(int lineNumber) throws CoreException {
+		JDIDebugModel.createLineBreakpoint(getProjectContext().getProject(), "SyntheticTest", lineNumber, -1, -1, 0, true, null);
+	}
+
+	@Override
+	protected IJavaProject getProjectContext() {
+		if (project == null) {
+			try {
+				project = createProject("Synthetic", "testresources/synthetic/src", JavaProjectHelper.JAVA_SE_1_8_EE_NAME, true);
+			} catch (Exception e) {
+				fail(e.getMessage());
+			}
+		}
+		return project;
+	}
+
+	private void addClasses() throws Exception {
+		IPath bin = getProjectContext().getPath().append(JavaProjectHelper.BIN_DIR).makeAbsolute();
+		File classDir = JavaTestPlugin.getDefault().getFileInPlugin(new Path("testresources/synthetic/bin"));
+		JavaProjectHelper.importFilesFromDirectory(classDir, bin, null);
+		createLaunchConfiguration("SyntheticTest");
+	}
+
+	private void registerContextProvider() {
+		IWorkbenchWindow activeWindow = SelectedResourceManager.getDefault().getActiveWindow();
+		assertNotNull("activeWindow is null", activeWindow);
+		DebugUITools.getDebugContextManager().getContextService(activeWindow).addDebugContextProvider(debugContextProvider);
+	}
+
+	private void unregisterContextProvider() {
+		IWorkbenchWindow activeWindow = SelectedResourceManager.getDefault().getActiveWindow();
+		assertNotNull("activeWindow is null", activeWindow);
+		DebugUITools.getDebugContextManager().getContextService(activeWindow).removeDebugContextProvider(debugContextProvider);
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		super.tearDown();
+		project.getProject().delete(true, null);
+		project = null;
+	}
+}
diff --git a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/contentassist/CurrentFrameContext.java b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/contentassist/CurrentFrameContext.java
index ed71f21..671e782 100644
--- a/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/contentassist/CurrentFrameContext.java
+++ b/org.eclipse.jdt.debug.ui/ui/org/eclipse/jdt/internal/debug/ui/contentassist/CurrentFrameContext.java
@@ -13,6 +13,9 @@
  *******************************************************************************/
 package org.eclipse.jdt.internal.debug.ui.contentassist;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IAdaptable;
 import org.eclipse.debug.core.DebugException;
@@ -24,6 +27,8 @@
 import org.eclipse.jdt.internal.debug.core.JavaDebugUtils;
 import org.eclipse.jdt.internal.debug.core.logicalstructures.JDIPlaceholderVariable;
 import org.eclipse.jdt.internal.debug.core.model.JDIThisVariable;
+import org.eclipse.jdt.internal.debug.core.model.SyntheticVariableUtils;
+import org.eclipse.jdt.internal.debug.eval.ast.engine.ASTEvaluationEngine;
 
 
 /**
@@ -63,7 +68,7 @@
 	public String[][] getLocalVariables() throws CoreException {
         IJavaStackFrame frame = getStackFrame();
         if (frame != null) {
-            IVariable[] variables = frame.getVariables();
+			IVariable[] variables = extractVariables(frame);
             int index = 0;
 			while (index < variables.length
 					&& (variables[index] instanceof JDIThisVariable || JDIPlaceholderVariable.class.isAssignableFrom(variables[index].getClass()))) {
@@ -72,7 +77,7 @@
             String[][] locals = new String[2][variables.length - index];
             for (int i = 0; i < locals[0].length; i++) {
                 IJavaVariable var = (IJavaVariable) variables[index];
-                locals[0][i] = var.getName();
+				locals[0][i] = resolveVarName(var);
                 try {
                 	locals[1][i] = var.getJavaType().getName();
                 }
@@ -86,9 +91,29 @@
         return super.getLocalVariables();
     }
 
-    /* (non-Javadoc)
-     * @see org.eclipse.jdt.internal.debug.ui.contentassist.IJavaDebugContentAssistContext#isStatic()
-     */
+	private IVariable[] extractVariables(IJavaStackFrame frame) throws DebugException {
+		ArrayList<IVariable> vars = new ArrayList<>(Arrays.asList(frame.getVariables()));
+		for (IVariable var : vars) {
+			if (var instanceof JDIThisVariable) {
+				vars.addAll(Arrays.asList(SyntheticVariableUtils.findSyntheticVariables(var.getValue().getVariables())));
+				break;
+			}
+		}
+		return vars.toArray(new IVariable[0]);
+	}
+
+	private String resolveVarName(IJavaVariable var) throws DebugException {
+		final String name = var.getName();
+		if (name.startsWith(ASTEvaluationEngine.ANONYMOUS_VAR_PREFIX)) {
+			return name.substring(ASTEvaluationEngine.ANONYMOUS_VAR_PREFIX.length());
+		}
+		return name;
+	}
+
+	
+	/* (non-Javadoc)
+	 * @see org.eclipse.jdt.internal.debug.ui.contentassist.IJavaDebugContentAssistContext#isStatic()
+	 */
     @Override
 	public boolean isStatic() throws CoreException {
         IJavaStackFrame frame = getStackFrame();
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 fc682b1..bdfda78 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
@@ -70,6 +70,7 @@
 import org.eclipse.jdt.internal.debug.core.model.JDIThread;
 import org.eclipse.jdt.internal.debug.core.model.JDIValue;
 import org.eclipse.jdt.internal.debug.core.model.LambdaUtils;
+import org.eclipse.jdt.internal.debug.core.model.SyntheticVariableUtils;
 import org.eclipse.jdt.internal.debug.eval.EvaluationResult;
 import org.eclipse.jdt.internal.debug.eval.ast.instructions.InstructionSequence;
 
@@ -299,7 +300,7 @@
 			IJavaObject thisClass = context.getThis();
 			IVariable[] innerClassFields; // For anonymous classes, getting variables from outer class
 			if (null != thisClass) {
-				innerClassFields = thisClass.getVariables();
+				innerClassFields = extractVariables(thisClass);
 			} else {
 				innerClassFields = new IVariable[0];
 			}
@@ -393,6 +394,13 @@
 		return createExpressionFromAST(snippet, mapper, unit);
 	}
 
+	private IVariable[] extractVariables(IJavaObject thisClass) throws DebugException {
+		IVariable[] vars = thisClass.getVariables();
+		List<IVariable> varList = new ArrayList<>(Arrays.asList(vars));
+		varList.addAll(Arrays.asList(SyntheticVariableUtils.findSyntheticVariables(vars)));
+		return varList.toArray(new IVariable[0]);
+	}
+
 	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
diff --git a/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/SyntheticVariableUtils.java b/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/SyntheticVariableUtils.java
new file mode 100644
index 0000000..b898892
--- /dev/null
+++ b/org.eclipse.jdt.debug/model/org/eclipse/jdt/internal/debug/core/model/SyntheticVariableUtils.java
@@ -0,0 +1,81 @@
+/*******************************************************************************
+ * 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.internal.debug.core.model;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import org.eclipse.debug.core.DebugException;
+import org.eclipse.debug.core.model.IVariable;
+import org.eclipse.jdt.internal.debug.eval.ast.engine.ASTEvaluationEngine;
+
+/**
+ * Utility class for handling Synthetic Variables.
+ */
+public final class SyntheticVariableUtils {
+	private static final String ENCLOSING_INSTANCE_PREFIX = "this$"; //$NON-NLS-1$
+	private static final String ANONYMOUS_VAR_PREFIX = ASTEvaluationEngine.ANONYMOUS_VAR_PREFIX;
+
+	private SyntheticVariableUtils() {
+	}
+
+	/**
+	 * When many anonymous objects are nested as below
+	 *
+	 * <pre>
+	 * public Bar exec(Predicate&lt;String&gt; predicate) {
+	 *
+	 * 	return new Bar() {
+	 * 		private Object bar;
+	 *
+	 * 		&#64;Override
+	 * 		public Foo bar(String vbar) {
+	 * 			return new Foo() {
+	 * 				private Object foo;
+	 *
+	 * 				&#64;Override
+	 * 				public String foo(String vfoo) {
+	 * 					predicate.test("vfoo");
+	 * 					return vfoo;
+	 * 				}
+	 * 			};
+	 * 		}
+	 * 	};
+	 * }
+	 * </pre>
+	 *
+	 * The outermost method parameters are not available as synthetic variables in the bottom most object. This method try to extract those variables
+	 * from the variable graph. This method looks for synthetic variables in enclosing instances and traverse variables of those instances and collect
+	 * synthetic outer local variables and return them.
+	 *
+	 * @param variables
+	 *            variable to search on
+	 * @return array of variables
+	 * @throws DebugException
+	 */
+	public static IVariable[] findSyntheticVariables(IVariable[] variables) throws DebugException {
+		ArrayList<IVariable> extracted = new ArrayList<>();
+		for (IVariable variable : variables) {
+			if (variable.getName().startsWith(ANONYMOUS_VAR_PREFIX)) {
+				extracted.add(variable);
+			}
+
+			if (variable.getName().startsWith(ENCLOSING_INSTANCE_PREFIX)) {
+				extracted.addAll(Arrays.asList(findSyntheticVariables(variable.getValue().getVariables())));
+			}
+		}
+		return extracted.toArray(new IVariable[extracted.size()]);
+	}
+
+}