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<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;
+ * }
+ * };
+ * }
+ * };
+ * }
+ * </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()]);
+ }
+
+}