diff --git a/src/main/java/org/javacs/debug/JavaDebugServer.java b/src/main/java/org/javacs/debug/JavaDebugServer.java index 53d36ce68..61dd3d04d 100644 --- a/src/main/java/org/javacs/debug/JavaDebugServer.java +++ b/src/main/java/org/javacs/debug/JavaDebugServer.java @@ -13,6 +13,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Objects; @@ -440,6 +441,7 @@ public void terminate(TerminateArguments req) { @Override public void continue_(ContinueArguments req) { + valueIdTracker.clear(); vm.resume(); } @@ -454,6 +456,7 @@ public void next(NextArguments req) { var step = vm.eventRequestManager().createStepRequest(thread, StepRequest.STEP_LINE, StepRequest.STEP_OVER); step.addCountFilter(1); step.enable(); + valueIdTracker.clear(); vm.resume(); } @@ -468,6 +471,7 @@ public void stepIn(StepInArguments req) { var step = vm.eventRequestManager().createStepRequest(thread, StepRequest.STEP_LINE, StepRequest.STEP_INTO); step.addCountFilter(1); step.enable(); + valueIdTracker.clear(); vm.resume(); } @@ -482,6 +486,7 @@ public void stepOut(StepOutArguments req) { var step = vm.eventRequestManager().createStepRequest(thread, StepRequest.STEP_LINE, StepRequest.STEP_OUT); step.addCountFilter(1); step.enable(); + valueIdTracker.clear(); vm.resume(); } @@ -632,47 +637,157 @@ private com.sun.jdi.StackFrame findFrame(long id) { @Override public ScopesResponseBody scopes(ScopesArguments req) { var resp = new ScopesResponseBody(); + var fields = new Scope(); + fields.name = "Fields"; + fields.presentationHint = "locals"; + fields.expensive = true; // do not expand by default + fields.variablesReference = req.frameId * 2; var locals = new Scope(); locals.name = "Locals"; locals.presentationHint = "locals"; - locals.variablesReference = req.frameId * 2; - var arguments = new Scope(); - arguments.name = "Arguments"; - arguments.presentationHint = "arguments"; - arguments.variablesReference = req.frameId * 2 + 1; - resp.scopes = new Scope[] {locals, arguments}; + locals.variablesReference = req.frameId * 2 + 1; + resp.scopes = new Scope[] {fields, locals}; return resp; } + private static final long VALUE_ID_START = 1000000000; + + private static class ValueIdTracker { + private final HashMap values = new HashMap<>(); + private long nextId = VALUE_ID_START; + + public void clear() { + values.clear(); + // Keep nextId to avoid accidentally accessing wrong Values. + } + + public Value get(long id) { + return values.get(id); + } + + public long put(Value value) { + long id = nextId++; + values.put(id, value); + return id; + } + } + + private final ValueIdTracker valueIdTracker = new ValueIdTracker(); + + private static boolean hasInterestingChildren(Value value) { + return value instanceof ObjectReference && !(value instanceof StringReference); + } + @Override public VariablesResponseBody variables(VariablesArguments req) { - var frameId = req.variablesReference / 2; - var scopeId = (int) (req.variablesReference % 2); - var argumentScope = scopeId == 1; + if (req.variablesReference < VALUE_ID_START) { + var frameId = req.variablesReference / 2; + var scopeId = (int)(req.variablesReference % 2); + return frameVariables(frameId, scopeId); + } + Value value = valueIdTracker.get(req.variablesReference); + return valueChildren(value); + } + + private VariablesResponseBody frameVariables(long frameId, int scopeId) { var frame = findFrame(frameId); + var thread = frame.thread(); + var variables = new ArrayList(); + + if (scopeId == 0) { + var thisValue = frame.thisObject(); + if (thisValue != null) { + variables.addAll(objectFieldsAsVariables(thisValue, thread)); + } + } else { + variables.addAll(frameLocalsAsVariables(frame, thread)); + } + + var resp = new VariablesResponseBody(); + resp.variables = variables.toArray(Variable[]::new); + return resp; + } + + private VariablesResponseBody valueChildren(Value parentValue) { + // TODO: Use an actual owner thread. + ThreadReference mainThread = vm.allThreads().get(0); + var variables = new ArrayList(); + + if (parentValue instanceof ArrayReference array) { + variables.addAll(arrayElementsAsVariables(array, mainThread)); + } else if (parentValue instanceof ObjectReference object) { + variables.addAll(objectFieldsAsVariables(object, mainThread)); + } + + var resp = new VariablesResponseBody(); + resp.variables = variables.toArray(Variable[]::new); + return resp; + } + + private List frameLocalsAsVariables(com.sun.jdi.StackFrame frame, ThreadReference thread) { List visible; try { visible = frame.visibleVariables(); } catch (AbsentInformationException __) { LOG.warning(String.format("No visible variable information in %s", frame.location())); - return new VariablesResponseBody(); + return List.of(); } - var values = frame.getValues(visible); - var thread = frame.thread(); + var variables = new ArrayList(); + var values = frame.getValues(visible); for (var v : visible) { - if (v.isArgument() != argumentScope) continue; + var value = values.get(v); var w = new Variable(); w.name = v.name(); - w.value = print(values.get(v), thread); + w.value = print(value, thread); w.type = v.typeName(); - // TODO set variablesReference and allow inspecting structure of collections and POJOs + if (hasInterestingChildren(value)) { + w.variablesReference = valueIdTracker.put(value); + } + if (value instanceof ArrayReference array) { + w.indexedVariables = array.length(); + } // TODO set variablePresentationHint variables.add(w); } - var resp = new VariablesResponseBody(); - resp.variables = variables.toArray(Variable[]::new); - return resp; + return variables; + } + + private List arrayElementsAsVariables(ArrayReference array, ThreadReference thread) { + var variables = new ArrayList(); + var arrayType = (ArrayType) array.type(); + var values = array.getValues(); + var length = values.size(); + for (int i = 0; i < length; i++) { + var value = values.get(i); + var w = new Variable(); + w.name = Integer.toString(i, 10); + w.value = print(value, thread); + w.type = arrayType.componentTypeName(); + if (hasInterestingChildren(value)) { + w.variablesReference = valueIdTracker.put(value); + } + variables.add(w); + } + return variables; + } + + private List objectFieldsAsVariables(ObjectReference object, ThreadReference thread) { + var variables = new ArrayList(); + var classType = (ClassType) object.type(); + var values = object.getValues(classType.allFields()); + for (var field : values.keySet()) { + var value = values.get(field); + var w = new Variable(); + w.name = field.name(); + w.value = print(value, thread); + w.type = field.typeName(); + if (hasInterestingChildren(value)) { + w.variablesReference = valueIdTracker.put(value); + } + variables.add(w); + } + return variables; } private String print(Value value, ThreadReference t) { diff --git a/src/test/examples/debug/DeepVariables.class b/src/test/examples/debug/DeepVariables.class new file mode 100644 index 000000000..acb420536 Binary files /dev/null and b/src/test/examples/debug/DeepVariables.class differ diff --git a/src/test/examples/debug/DeepVariables.java b/src/test/examples/debug/DeepVariables.java new file mode 100644 index 000000000..79f3623a1 --- /dev/null +++ b/src/test/examples/debug/DeepVariables.java @@ -0,0 +1,14 @@ +public class DeepVariables { + public static void main(String[] args) { + new DeepVariables().run(); + } + + public void run() { + Inner object = new Inner(); + System.out.println(object.value); + } + + private static class Inner { + public final int value = 42; + } +} diff --git a/src/test/java/org/javacs/JavaDebugServerTest.java b/src/test/java/org/javacs/JavaDebugServerTest.java index 63fe72434..c3f9a5c08 100644 --- a/src/test/java/org/javacs/JavaDebugServerTest.java +++ b/src/test/java/org/javacs/JavaDebugServerTest.java @@ -1,5 +1,8 @@ package org.javacs; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; @@ -207,7 +210,7 @@ public void printCollections() throws IOException, InterruptedException { var scopes = server.scopes(requestScopes).scopes; // Get locals var requestLocals = new VariablesArguments(); - requestLocals.variablesReference = scopes[0].variablesReference; + requestLocals.variablesReference = scopes[1].variablesReference; var locals = server.variables(requestLocals).variables; System.out.println("Locals:"); for (var v : locals) { @@ -220,5 +223,66 @@ public void printCollections() throws IOException, InterruptedException { process.waitFor(); } + @Test + public void deepVariables() throws IOException, InterruptedException { + launchProcess("DeepVariables"); + attach(5005); + setBreakpoint("DeepVariables", 8); + server.configurationDone(); + stoppedEvents.take(); + + // Find the main thread + org.javacs.debug.proto.Thread mainThread = null; + for (var t : server.threads().threads) { + if (t.name.equals("main")) { + mainThread = t; + } + } + assertThat(mainThread, notNullValue()); + + // Get the stack trace + var requestTrace = new StackTraceArguments(); + requestTrace.threadId = mainThread.id; + var stack = server.stackTrace(requestTrace); + + // Get variables + var requestScopes = new ScopesArguments(); + requestScopes.frameId = stack.stackFrames[0].id; + var scopes = server.scopes(requestScopes).scopes; + + // Get locals + var requestLocals = new VariablesArguments(); + requestLocals.variablesReference = scopes[1].variablesReference; + var locals = server.variables(requestLocals).variables; + + // Find an object value + Variable objectVariable = null; + for (var v : locals) { + if (v.name.equals("object")) { + objectVariable = v; + } + } + assertThat(objectVariable, notNullValue()); + + // Get an object field + var requestObject = new VariablesArguments(); + requestObject.variablesReference = objectVariable.variablesReference; + var fields = server.variables(requestObject).variables; + + // Inspect an object field + Variable fieldVariable = null; + for (var v : fields) { + if (v.name.equals("value")) { + fieldVariable = v; + } + } + assertThat(fieldVariable, notNullValue()); + assertThat(fieldVariable.value, equalTo("42")); + + // Wait for process to exit + server.continue_(new ContinueArguments()); + process.waitFor(); + } + private static final Logger LOG = Logger.getLogger("main"); }