Skip to content

Commit e521ed1

Browse files
Leverage AST and Java Runtime to resolve inline values (#368)
* Leverage AST and Java Runtime to resolve inline values
1 parent e55f864 commit e521ed1

File tree

9 files changed

+948
-0
lines changed

9 files changed

+948
-0
lines changed

com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.microsoft.java.debug.core.adapter.handler.ExceptionInfoRequestHandler;
3131
import com.microsoft.java.debug.core.adapter.handler.HotCodeReplaceHandler;
3232
import com.microsoft.java.debug.core.adapter.handler.InitializeRequestHandler;
33+
import com.microsoft.java.debug.core.adapter.handler.InlineValuesRequestHandler;
3334
import com.microsoft.java.debug.core.adapter.handler.LaunchRequestHandler;
3435
import com.microsoft.java.debug.core.adapter.handler.RestartFrameHandler;
3536
import com.microsoft.java.debug.core.adapter.handler.ScopesRequestHandler;
@@ -121,6 +122,7 @@ private void initialize() {
121122
registerHandlerForDebug(new ExceptionInfoRequestHandler());
122123
registerHandlerForDebug(new DataBreakpointInfoRequestHandler());
123124
registerHandlerForDebug(new SetDataBreakpointsRequestHandler());
125+
registerHandlerForDebug(new InlineValuesRequestHandler());
124126

125127
// NO_DEBUG mode only
126128
registerHandlerForNoDebug(new DisconnectRequestWithoutDebuggingHandler());
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2021 Microsoft Corporation and others.
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* http://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Microsoft Corporation - initial API and implementation
10+
*******************************************************************************/
11+
12+
package com.microsoft.java.debug.core.adapter.handler;
13+
14+
import java.util.Arrays;
15+
import java.util.HashMap;
16+
import java.util.List;
17+
import java.util.Map;
18+
import java.util.Objects;
19+
import java.util.concurrent.CancellationException;
20+
import java.util.concurrent.CompletableFuture;
21+
import java.util.concurrent.ExecutionException;
22+
import java.util.logging.Level;
23+
import java.util.logging.Logger;
24+
25+
import com.microsoft.java.debug.core.Configuration;
26+
import com.microsoft.java.debug.core.DebugSettings;
27+
import com.microsoft.java.debug.core.adapter.IDebugAdapterContext;
28+
import com.microsoft.java.debug.core.adapter.IDebugRequestHandler;
29+
import com.microsoft.java.debug.core.adapter.IEvaluationProvider;
30+
import com.microsoft.java.debug.core.adapter.IStackFrameManager;
31+
import com.microsoft.java.debug.core.adapter.variables.IVariableFormatter;
32+
import com.microsoft.java.debug.core.adapter.variables.JavaLogicalStructure;
33+
import com.microsoft.java.debug.core.adapter.variables.JavaLogicalStructureManager;
34+
import com.microsoft.java.debug.core.adapter.variables.StackFrameReference;
35+
import com.microsoft.java.debug.core.adapter.variables.Variable;
36+
import com.microsoft.java.debug.core.adapter.variables.VariableDetailUtils;
37+
import com.microsoft.java.debug.core.protocol.Responses;
38+
import com.microsoft.java.debug.core.protocol.Types;
39+
import com.microsoft.java.debug.core.protocol.Messages.Response;
40+
import com.microsoft.java.debug.core.protocol.Requests.Arguments;
41+
import com.microsoft.java.debug.core.protocol.Requests.Command;
42+
import com.microsoft.java.debug.core.protocol.Requests.InlineVariable;
43+
import com.microsoft.java.debug.core.protocol.Requests.InlineValuesArguments;
44+
import com.sun.jdi.ArrayReference;
45+
import com.sun.jdi.Field;
46+
import com.sun.jdi.IntegerValue;
47+
import com.sun.jdi.Method;
48+
import com.sun.jdi.ObjectReference;
49+
import com.sun.jdi.ReferenceType;
50+
import com.sun.jdi.StackFrame;
51+
import com.sun.jdi.Value;
52+
53+
import org.apache.commons.lang3.math.NumberUtils;
54+
55+
public class InlineValuesRequestHandler implements IDebugRequestHandler {
56+
protected static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME);
57+
58+
@Override
59+
public List<Command> getTargetCommands() {
60+
return Arrays.asList(Command.INLINEVALUES);
61+
}
62+
63+
/**
64+
* This request only resolves the values for those non-local variables, such as
65+
* field variables and captured variables from outer scope. Because the values
66+
* of local variables in current stackframe are usually expanded by Variables View
67+
* by default, inline values can reuse these values directly. However, for field
68+
* variables and variables captured from external scopes, they are hidden as properties
69+
* of 'this' variable and require additional evaluation to get their values.
70+
*/
71+
@Override
72+
public CompletableFuture<Response> handle(Command command, Arguments arguments, Response response,
73+
IDebugAdapterContext context) {
74+
InlineValuesArguments inlineValuesArgs = (InlineValuesArguments) arguments;
75+
int variableCount = inlineValuesArgs == null || inlineValuesArgs.variables == null ? 0 : inlineValuesArgs.variables.length;
76+
InlineVariable[] inlineVariables = inlineValuesArgs.variables;
77+
StackFrameReference stackFrameReference = (StackFrameReference) context.getRecyclableIdPool().getObjectById(inlineValuesArgs.frameId);
78+
if (stackFrameReference == null) {
79+
logger.log(Level.SEVERE, String.format("InlineValues failed: invalid stackframe id %d.", inlineValuesArgs.frameId));
80+
response.body = new Responses.InlineValuesResponse(null);
81+
return CompletableFuture.completedFuture(response);
82+
}
83+
84+
IStackFrameManager stackFrameManager = context.getStackFrameManager();
85+
StackFrame frame = stackFrameManager.getStackFrame(stackFrameReference);
86+
if (frame == null) {
87+
logger.log(Level.SEVERE, String.format("InlineValues failed: stale stackframe id %d.", inlineValuesArgs.frameId));
88+
response.body = new Responses.InlineValuesResponse(null);
89+
return CompletableFuture.completedFuture(response);
90+
}
91+
92+
Variable[] values = new Variable[variableCount];
93+
try {
94+
if (isLambdaFrame(frame)) {
95+
// Lambda expression stores the captured variables from 'outer' scope in a synthetic stackframe below the lambda frame.
96+
StackFrame syntheticLambdaFrame = stackFrameReference.getThread().frame(stackFrameReference.getDepth() + 1);
97+
resolveValuesFromThisVariable(syntheticLambdaFrame.thisObject(), inlineVariables, values, true);
98+
}
99+
100+
resolveValuesFromThisVariable(frame.thisObject(), inlineVariables, values, false);
101+
} catch (Exception ex) {
102+
// do nothig
103+
}
104+
105+
Types.Variable[] result = new Types.Variable[variableCount];
106+
IVariableFormatter variableFormatter = context.getVariableFormatter();
107+
Map<String, Object> formatterOptions = variableFormatter.getDefaultOptions();
108+
Map<InlineVariable, Types.Variable> calculatedValues = new HashMap<>();
109+
IEvaluationProvider evaluationEngine = context.getProvider(IEvaluationProvider.class);
110+
for (int i = 0; i < variableCount; i++) {
111+
if (values[i] == null) {
112+
continue;
113+
}
114+
115+
if (calculatedValues.containsKey(inlineVariables[i])) {
116+
result[i] = calculatedValues.get(inlineVariables[i]);
117+
continue;
118+
}
119+
120+
Value value = values[i].value;
121+
String name = values[i].name;
122+
int indexedVariables = -1;
123+
Value sizeValue = null;
124+
if (value instanceof ArrayReference) {
125+
indexedVariables = ((ArrayReference) value).length();
126+
} else if (value instanceof ObjectReference && DebugSettings.getCurrent().showLogicalStructure && evaluationEngine != null) {
127+
try {
128+
JavaLogicalStructure structure = JavaLogicalStructureManager.getLogicalStructure((ObjectReference) value);
129+
if (structure != null && structure.getSizeExpression() != null) {
130+
sizeValue = structure.getSize((ObjectReference) value, frame.thread(), evaluationEngine);
131+
if (sizeValue != null && sizeValue instanceof IntegerValue) {
132+
indexedVariables = ((IntegerValue) sizeValue).value();
133+
}
134+
}
135+
} catch (CancellationException | IllegalArgumentException | InterruptedException | ExecutionException | UnsupportedOperationException e) {
136+
logger.log(Level.INFO,
137+
String.format("Failed to get the logical size for the type %s.", value.type().name()), e);
138+
}
139+
}
140+
141+
Types.Variable formattedVariable = new Types.Variable(name, variableFormatter.valueToString(value, formatterOptions));
142+
formattedVariable.indexedVariables = Math.max(indexedVariables, 0);
143+
String detailsValue = null;
144+
if (sizeValue != null) {
145+
detailsValue = "size=" + variableFormatter.valueToString(sizeValue, formatterOptions);
146+
} else if (DebugSettings.getCurrent().showToString) {
147+
detailsValue = VariableDetailUtils.formatDetailsValue(value, frame.thread(), variableFormatter, formatterOptions, evaluationEngine);
148+
}
149+
150+
if (detailsValue != null) {
151+
formattedVariable.value = formattedVariable.value + " " + detailsValue;
152+
}
153+
154+
result[i] = formattedVariable;
155+
calculatedValues.put(inlineVariables[i], formattedVariable);
156+
}
157+
158+
response.body = new Responses.InlineValuesResponse(result);
159+
return CompletableFuture.completedFuture(response);
160+
}
161+
162+
private static boolean isCapturedLocalVariable(String fieldName, String variableName) {
163+
String capturedVariableName = "val$" + variableName;
164+
return Objects.equals(fieldName, capturedVariableName)
165+
|| (fieldName.startsWith(capturedVariableName + "$") && NumberUtils.isDigits(fieldName.substring(capturedVariableName.length() + 1)));
166+
}
167+
168+
private static boolean isCapturedThisVariable(String fieldName) {
169+
if (fieldName.startsWith("this$")) {
170+
String suffix = fieldName.substring(5).replaceAll("\\$+$", "");
171+
return NumberUtils.isDigits(suffix);
172+
}
173+
174+
return false;
175+
}
176+
177+
private static boolean isLambdaFrame(StackFrame frame) {
178+
Method method = frame.location().method();
179+
return method.isSynthetic() && method.name().startsWith("lambda$");
180+
}
181+
182+
private void resolveValuesFromThisVariable(ObjectReference thisObj, InlineVariable[] unresolvedVariables, Variable[] result,
183+
boolean isSyntheticLambdaFrame) {
184+
if (thisObj == null) {
185+
return;
186+
}
187+
188+
int unresolved = 0;
189+
for (Variable item : result) {
190+
if (item == null) {
191+
unresolved++;
192+
}
193+
}
194+
195+
try {
196+
ReferenceType type = thisObj.referenceType();
197+
String typeName = type.name();
198+
ObjectReference enclosingInstance = null;
199+
for (Field field : type.allFields()) {
200+
String fieldName = field.name();
201+
boolean isSyntheticField = field.isSynthetic();
202+
Value fieldValue = null;
203+
for (int i = 0; i < unresolvedVariables.length; i++) {
204+
if (result[i] != null) {
205+
continue;
206+
}
207+
208+
InlineVariable inlineVariable = unresolvedVariables[i];
209+
boolean isInlineFieldVariable = (inlineVariable.declaringClass != null);
210+
boolean isMatch = false;
211+
if (isSyntheticLambdaFrame) {
212+
isMatch = !isInlineFieldVariable && Objects.equals(fieldName, inlineVariable.expression);
213+
} else {
214+
boolean isMatchedField = isInlineFieldVariable
215+
&& Objects.equals(fieldName, inlineVariable.expression)
216+
&& Objects.equals(typeName, inlineVariable.declaringClass);
217+
boolean isMatchedCapturedVariable = !isInlineFieldVariable
218+
&& isSyntheticField
219+
&& isCapturedLocalVariable(fieldName, inlineVariable.expression);
220+
isMatch = isMatchedField || isMatchedCapturedVariable;
221+
222+
if (!isMatch && isSyntheticField && enclosingInstance == null && isCapturedThisVariable(fieldName)) {
223+
Value value = thisObj.getValue(field);
224+
if (value instanceof ObjectReference) {
225+
enclosingInstance = (ObjectReference) value;
226+
break;
227+
}
228+
}
229+
}
230+
231+
if (isMatch) {
232+
fieldValue = fieldValue == null ? thisObj.getValue(field) : fieldValue;
233+
result[i] = new Variable(inlineVariable.expression, fieldValue);
234+
unresolved--;
235+
}
236+
}
237+
238+
if (unresolved <= 0) {
239+
break;
240+
}
241+
}
242+
243+
if (unresolved > 0 && enclosingInstance != null) {
244+
resolveValuesFromThisVariable(enclosingInstance, unresolvedVariables, result, isSyntheticLambdaFrame);
245+
}
246+
} catch (Exception ex) {
247+
// do nothing
248+
}
249+
}
250+
}

com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Requests.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import java.util.Arrays;
1515
import java.util.Map;
16+
import java.util.Objects;
1617

1718
import com.google.gson.annotations.SerializedName;
1819
import com.microsoft.java.debug.core.protocol.Types.DataBreakpoint;
@@ -340,6 +341,33 @@ public static class SetDataBreakpointsArguments extends Arguments {
340341
public DataBreakpoint[] breakpoints;
341342
}
342343

344+
public static class InlineValuesArguments extends Arguments {
345+
public int frameId;
346+
public InlineVariable[] variables;
347+
}
348+
349+
public static class InlineVariable {
350+
public String expression;
351+
public String declaringClass;
352+
353+
@Override
354+
public int hashCode() {
355+
return Objects.hash(declaringClass, expression);
356+
}
357+
358+
@Override
359+
public boolean equals(Object obj) {
360+
if (this == obj) {
361+
return true;
362+
}
363+
if (!(obj instanceof InlineVariable)) {
364+
return false;
365+
}
366+
InlineVariable other = (InlineVariable) obj;
367+
return Objects.equals(declaringClass, other.declaringClass) && Objects.equals(expression, other.expression);
368+
}
369+
}
370+
343371
public static enum Command {
344372
INITIALIZE("initialize", InitializeArguments.class),
345373
LAUNCH("launch", LaunchArguments.class),
@@ -372,6 +400,7 @@ public static enum Command {
372400
CONTINUEOTHERS("continueOthers", ThreadOperationArguments.class),
373401
PAUSEALL("pauseAll", ThreadOperationArguments.class),
374402
PAUSEOTHERS("pauseOthers", ThreadOperationArguments.class),
403+
INLINEVALUES("inlineValues", InlineValuesArguments.class),
375404
UNSUPPORTED("", Arguments.class);
376405

377406
private String command;

com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Responses.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.microsoft.java.debug.core.protocol.Types.DataBreakpointAccessType;
1717
import com.microsoft.java.debug.core.protocol.Types.ExceptionBreakMode;
1818
import com.microsoft.java.debug.core.protocol.Types.ExceptionDetails;
19+
import com.microsoft.java.debug.core.protocol.Types.Variable;
1920

2021
/**
2122
* The response content types defined by VSCode Debug Protocol.
@@ -316,4 +317,12 @@ public RedefineClassesResponse(String[] changedClasses, String errorMessage) {
316317
this.errorMessage = errorMessage;
317318
}
318319
}
320+
321+
public static class InlineValuesResponse extends ResponseBody {
322+
public Types.Variable[] variables;
323+
324+
public InlineValuesResponse(Variable[] variables) {
325+
this.variables = variables;
326+
}
327+
}
319328
}

com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Types.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,14 @@ public Variable(String name, String val, String type, int rf, String evaluateNam
101101
this.variablesReference = rf;
102102
this.evaluateName = evaluateName;
103103
}
104+
105+
/**
106+
* Constructor.
107+
*/
108+
public Variable(String name, String value) {
109+
this.name = name;
110+
this.value = value;
111+
}
104112
}
105113

106114
public static class Thread {

com.microsoft.java.debug.plugin/META-INF/MANIFEST.MF

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Require-Bundle: org.eclipse.core.runtime,
1414
org.eclipse.debug.core,
1515
org.eclipse.jdt.debug,
1616
org.eclipse.jdt.core,
17+
org.eclipse.jdt.core.manipulation,
1718
org.eclipse.jdt.ls.core,
1819
org.eclipse.jdt.launching,
1920
com.google.gson,

com.microsoft.java.debug.plugin/plugin.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<command id="vscode.java.fetchPlatformSettings"/>
2121
<command id="vscode.java.resolveClassFilters"/>
2222
<command id="vscode.java.resolveSourceUri"/>
23+
<command id="vscode.java.resolveInlineVariables"/>
2324
</delegateCommandHandler>
2425
</extension>
2526
</plugin>

0 commit comments

Comments
 (0)