Skip to content

Commit 194767e

Browse files
DanTupCommit Queue
authored andcommitted
[dds/dap] Add 'showGettersInDebugViews' which includes getters but lazily
The existing 'evaluateGettersInDebugViews' evaluates getters eagerly which can have side-effects. This adds a setting that allows showing getters in a way that can be shown lazily instead. I added a new setting (instead of just making evaluateGettersInDebugViews=false be lazy) to preserve the ability to have getters not shown at all (since some users are using that today and might prefer this over lots of lazy getters showing up). Fixes Dart-Code/Dart-Code#4234 Change-Id: I56c2a7c8f85aa8c4cc85cfb3120a8cfec6b54c70 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/310164 Reviewed-by: Ben Konyi <[email protected]> Commit-Queue: Ben Konyi <[email protected]>
1 parent c2a4f8e commit 194767e

File tree

8 files changed

+230
-46
lines changed

8 files changed

+230
-46
lines changed

pkg/dds/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# 2.9.1
2+
- [DAP] A new configuration option `bool? showGettersInDebugViews` allows getters to be shown wrapped in Variables/Evaluation responses so that they can be lazily expanded by the user. `evaluateGettersInDebugViews` must be `false` for this behaviour.
23
- [DAP] `runInTerminal` requests are now sent after first responding to the `launchRequest`.
34
- [DAP] Skipped tests are now marked with `!` instead of `` in `Output` events.
45
- [DAP] Implemented `pause` request.

pkg/dds/lib/src/dap/adapters/dart.dart

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ class DartAttachRequestArguments extends DartCommonLaunchAttachRequestArguments
114114
List<String>? additionalProjectPaths,
115115
bool? debugSdkLibraries,
116116
bool? debugExternalPackageLibraries,
117+
bool? showGettersInDebugViews,
117118
bool? evaluateGettersInDebugViews,
118119
bool? evaluateToStringInDebugViews,
119120
bool? sendLogsToClient,
@@ -127,6 +128,7 @@ class DartAttachRequestArguments extends DartCommonLaunchAttachRequestArguments
127128
additionalProjectPaths: additionalProjectPaths,
128129
debugSdkLibraries: debugSdkLibraries,
129130
debugExternalPackageLibraries: debugExternalPackageLibraries,
131+
showGettersInDebugViews: showGettersInDebugViews,
130132
evaluateGettersInDebugViews: evaluateGettersInDebugViews,
131133
evaluateToStringInDebugViews: evaluateToStringInDebugViews,
132134
sendLogsToClient: sendLogsToClient,
@@ -194,13 +196,19 @@ class DartCommonLaunchAttachRequestArguments extends RequestArguments {
194196
/// packages as block boxes.
195197
final bool? debugExternalPackageLibraries;
196198

197-
/// Whether to evaluate getters in debug views like hovers and the variables
199+
/// Whether to show getters in debug views like hovers and the variables
198200
/// list.
201+
final bool? showGettersInDebugViews;
202+
203+
/// Whether to eagerly evaluate getters in debug views like hovers and the
204+
/// variables list.
199205
///
200-
/// Invoking getters has a performance cost and may introduce side-effects,
201-
/// although users may expected this functionality. null is treated like false
202-
/// although clients may have their own defaults (for example Dart-Code sends
203-
/// true by default at the time of writing).
206+
/// If `true`, getters will be invoked automatically and included inline with
207+
/// other fields (implies [showGettersInDebugViews]).
208+
///
209+
/// If `false`, getters will not be included unless [showGettersInDebugViews]
210+
/// is `true`, in which case they will be wrapped and only evaluated when the
211+
/// user expands them.
204212
final bool? evaluateGettersInDebugViews;
205213

206214
/// Whether to call toString() on objects in debug views like hovers and the
@@ -226,6 +234,9 @@ class DartCommonLaunchAttachRequestArguments extends RequestArguments {
226234
required this.additionalProjectPaths,
227235
required this.debugSdkLibraries,
228236
required this.debugExternalPackageLibraries,
237+
// TODO(dantup): Make this 'required' after Flutter subclasses have been
238+
// updated.
239+
this.showGettersInDebugViews,
229240
required this.evaluateGettersInDebugViews,
230241
required this.evaluateToStringInDebugViews,
231242
required this.sendLogsToClient,
@@ -242,6 +253,8 @@ class DartCommonLaunchAttachRequestArguments extends RequestArguments {
242253
debugSdkLibraries = arg.read<bool?>(obj, 'debugSdkLibraries'),
243254
debugExternalPackageLibraries =
244255
arg.read<bool?>(obj, 'debugExternalPackageLibraries'),
256+
showGettersInDebugViews =
257+
arg.read<bool?>(obj, 'showGettersInDebugViews'),
245258
evaluateGettersInDebugViews =
246259
arg.read<bool?>(obj, 'evaluateGettersInDebugViews'),
247260
evaluateToStringInDebugViews =
@@ -260,6 +273,8 @@ class DartCommonLaunchAttachRequestArguments extends RequestArguments {
260273
if (debugSdkLibraries != null) 'debugSdkLibraries': debugSdkLibraries,
261274
if (debugExternalPackageLibraries != null)
262275
'debugExternalPackageLibraries': debugExternalPackageLibraries,
276+
if (showGettersInDebugViews != null)
277+
'showGettersInDebugViews': showGettersInDebugViews,
263278
if (evaluateGettersInDebugViews != null)
264279
'evaluateGettersInDebugViews': evaluateGettersInDebugViews,
265280
if (evaluateToStringInDebugViews != null)
@@ -1745,6 +1760,7 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
17451760
VariablesArguments args,
17461761
void Function(VariablesResponseBody) sendResponse,
17471762
) async {
1763+
final service = vmService;
17481764
final childStart = args.start;
17491765
final childCount = args.count;
17501766
final storedData = isolateManager.getStoredData(args.variablesReference);
@@ -1883,6 +1899,20 @@ abstract class DartDebugAdapter<TL extends LaunchRequestArguments,
18831899
variablesReference: 0,
18841900
));
18851901
}
1902+
} else if (data is VariableGetter && service != null) {
1903+
final variable = await _converter.createVariableForGetter(
1904+
service,
1905+
thread,
1906+
data.instance,
1907+
// Empty names for lazy variable values because they were already shown
1908+
// in the parent object.
1909+
variableName: '',
1910+
getterName: data.getterName,
1911+
evaluateName: data.parentEvaluateName,
1912+
allowCallingToString: data.allowCallingToString,
1913+
format: format,
1914+
);
1915+
variables.add(variable);
18861916
}
18871917

18881918
sendResponse(VariablesResponseBody(variables: variables));
@@ -2558,6 +2588,7 @@ class DartLaunchRequestArguments extends DartCommonLaunchAttachRequestArguments
25582588
List<String>? additionalProjectPaths,
25592589
bool? debugSdkLibraries,
25602590
bool? debugExternalPackageLibraries,
2591+
bool? showGettersInDebugViews,
25612592
bool? evaluateGettersInDebugViews,
25622593
bool? evaluateToStringInDebugViews,
25632594
bool? sendLogsToClient,
@@ -2570,6 +2601,7 @@ class DartLaunchRequestArguments extends DartCommonLaunchAttachRequestArguments
25702601
additionalProjectPaths: additionalProjectPaths,
25712602
debugSdkLibraries: debugSdkLibraries,
25722603
debugExternalPackageLibraries: debugExternalPackageLibraries,
2604+
showGettersInDebugViews: showGettersInDebugViews,
25732605
evaluateGettersInDebugViews: evaluateGettersInDebugViews,
25742606
evaluateToStringInDebugViews: evaluateToStringInDebugViews,
25752607
sendLogsToClient: sendLogsToClient,

pkg/dds/lib/src/dap/protocol_converter.dart

Lines changed: 102 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -255,39 +255,36 @@ class ProtocolConverter {
255255
},
256256
));
257257

258-
// Also evaluate the getters if evaluateGettersInDebugViews=true enabled.
258+
// 'evaluateGettersInDebugViews' implies 'showGettersInDebugViews' so
259+
// either being `true` will cause getters to be included.
260+
final includeGetters = (_adapter.args.showGettersInDebugViews ?? false) ||
261+
(_adapter.args.evaluateGettersInDebugViews ?? false);
262+
final lazyGetters = !(_adapter.args.evaluateGettersInDebugViews ?? false);
259263
final service = _adapter.vmService;
260-
if (service != null &&
261-
(_adapter.args.evaluateGettersInDebugViews ?? false)) {
262-
// Collect getter names for this instances class and its supers.
263-
final getterNames =
264-
await _getterNamesForClassHierarchy(thread, instance.classRef);
265-
266-
/// Helper to evaluate each getter and convert the response to a
267-
/// variable.
268-
Future<dap.Variable> evaluate(int index, String getterName) async {
264+
if (service != null && includeGetters) {
265+
/// Helper to create a Variable for a getter.
266+
Future<dap.Variable> createVariable(
267+
int index, String getterName) async {
269268
try {
270-
final response = await service.evaluate(
271-
thread.isolate.id!,
272-
instance.id!,
273-
getterName,
274-
);
275-
final fieldEvaluateName =
276-
_adapter.combineEvaluateName(evaluateName, '.$getterName');
277-
if (response is vm.InstanceRef) {
278-
_adapter.storeEvaluateName(response, fieldEvaluateName);
279-
}
280-
// Convert results to variables.
281-
return convertVmResponseToVariable(
282-
thread,
283-
response,
284-
name: getterName,
285-
evaluateName:
286-
_adapter.combineEvaluateName(evaluateName, '.$getterName'),
287-
allowCallingToString:
288-
allowCallingToString && index <= maxToStringsPerEvaluation,
289-
format: format,
290-
);
269+
return lazyGetters
270+
? createVariableForLazyGetter(
271+
thread,
272+
instance,
273+
getterName,
274+
evaluateName,
275+
allowCallingToString,
276+
format,
277+
)
278+
: await createVariableForGetter(
279+
service,
280+
thread,
281+
instance,
282+
getterName: getterName,
283+
evaluateName: evaluateName,
284+
allowCallingToString: allowCallingToString &&
285+
index <= maxToStringsPerEvaluation,
286+
format: format,
287+
);
291288
} catch (e) {
292289
return dap.Variable(
293290
name: getterName,
@@ -297,7 +294,12 @@ class ProtocolConverter {
297294
}
298295
}
299296

300-
variables.addAll(await Future.wait(getterNames.mapIndexed(evaluate)));
297+
// Collect getter names for this instances class and its supers.
298+
final getterNames =
299+
await _getterNamesForClassHierarchy(thread, instance.classRef);
300+
301+
final getterVariables = getterNames.mapIndexed(createVariable);
302+
variables.addAll(await Future.wait(getterVariables));
301303
}
302304

303305
// Sort the fields/getters by name.
@@ -311,6 +313,74 @@ class ProtocolConverter {
311313
}
312314
}
313315

316+
/// Creates a Variable for a getter after eagerly fetching its value.
317+
Future<Variable> createVariableForGetter(
318+
vm.VmServiceInterface service,
319+
ThreadInfo thread,
320+
vm.Instance instance, {
321+
String? variableName,
322+
required String getterName,
323+
required String? evaluateName,
324+
required bool allowCallingToString,
325+
required VariableFormat? format,
326+
}) async {
327+
final response = await service.evaluate(
328+
thread.isolate.id!,
329+
instance.id!,
330+
getterName,
331+
);
332+
final fieldEvaluateName =
333+
_adapter.combineEvaluateName(evaluateName, '.$getterName');
334+
if (response is vm.InstanceRef) {
335+
_adapter.storeEvaluateName(response, fieldEvaluateName);
336+
}
337+
// Convert results to variables.
338+
return convertVmResponseToVariable(
339+
thread,
340+
response,
341+
name: variableName ?? getterName,
342+
evaluateName: _adapter.combineEvaluateName(evaluateName, '.$getterName'),
343+
allowCallingToString: allowCallingToString,
344+
format: format,
345+
);
346+
}
347+
348+
/// Creates a Variable for a getter that will be lazily evaluated.
349+
///
350+
/// This stores any data required to call [createVariableForGetter] later.
351+
///
352+
/// Lazy getters are implemented by inserting wrapper values and setting
353+
/// their presentation hint to "lazy". This will instruct clients like VS Code
354+
/// that they can show the value as "..." and fetch the child value into the
355+
/// placeholder when clicked.
356+
Variable createVariableForLazyGetter(
357+
ThreadInfo thread,
358+
vm.Instance instance,
359+
String getterName,
360+
String? evaluateName,
361+
bool allowCallingToString,
362+
VariableFormat? format,
363+
) {
364+
final variablesReference = thread.storeData(
365+
VariableData(
366+
VariableGetter(
367+
instance: instance,
368+
getterName: getterName,
369+
parentEvaluateName: evaluateName,
370+
allowCallingToString: allowCallingToString,
371+
),
372+
format,
373+
),
374+
);
375+
376+
return dap.Variable(
377+
name: getterName,
378+
value: '',
379+
variablesReference: variablesReference,
380+
presentationHint: dap.VariablePresentationHint(lazy: true),
381+
);
382+
}
383+
314384
/// Decodes the bytes of a list from the base64 encoded string
315385
/// [instance.bytes].
316386
List<Object?> _decodeList(vm.Instance instance) {

pkg/dds/lib/src/dap/variables.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ class VariableData {
1515
VariableData(this.data, this.format);
1616
}
1717

18+
/// Data used to lazily evaluate a getter in a Variables request.
19+
class VariableGetter {
20+
final Instance instance;
21+
final String getterName;
22+
final String? parentEvaluateName;
23+
final bool allowCallingToString;
24+
25+
VariableGetter({
26+
required this.instance,
27+
required this.getterName,
28+
required this.parentEvaluateName,
29+
required this.allowCallingToString,
30+
});
31+
}
32+
1833
/// A wrapper around variables for use in `variablesRequest` that can hold
1934
/// additional data, such as a formatting information supplied in an evaluation
2035
/// request.

pkg/dds/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ dependencies:
1414
browser_launcher: ^1.0.0
1515
collection: ^1.15.0
1616
dds_service_extensions: ^1.3.0
17-
dap: ^1.0.0
17+
dap: ^1.1.0
1818
devtools_shared: ^2.14.1
1919
http_multi_server: ^3.0.0
2020
json_rpc_2: ^3.0.0

pkg/dds/test/dap/integration/debug_variables_test.dart

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ void main(List<String> args) {
154154
);
155155
});
156156

157-
test('includes public getters when evaluateGettersInDebugViews=true',
157+
test('includes eager public getters when evaluateGettersInDebugViews=true',
158158
() async {
159159
final client = dap.client;
160160
final testFile = dap.createTestFile('''
@@ -163,8 +163,8 @@ void main(List<String> args) {
163163
print('Hello!'); $breakpointMarker
164164
}
165165
class A {
166-
String get publicString => '';
167-
String get _privateString => '';
166+
String get publicString => '111';
167+
String get _privateString => '222';
168168
}
169169
''');
170170
final breakpointLine = lineWith(testFile, breakpointMarker);
@@ -182,13 +182,62 @@ class A {
182182
expectedName: 'myVariable',
183183
expectedDisplayString: 'A',
184184
expectedVariables: '''
185-
publicString: "", eval: myVariable.publicString
185+
publicString: "111", eval: myVariable.publicString
186186
runtimeType: Type (A), eval: myVariable.runtimeType
187187
''',
188188
ignorePrivate: false,
189189
);
190190
});
191191

192+
test('includes lazy public getters when showGettersInDebugViews=true',
193+
() async {
194+
final client = dap.client;
195+
final testFile = dap.createTestFile('''
196+
void main(List<String> args) {
197+
final myVariable = A();
198+
print('Hello!'); $breakpointMarker
199+
}
200+
class A {
201+
String get publicString => '111';
202+
String get _privateString => '222';
203+
}
204+
''');
205+
final breakpointLine = lineWith(testFile, breakpointMarker);
206+
207+
final stop = await client.hitBreakpoint(
208+
testFile,
209+
breakpointLine,
210+
launch: () => client.launch(
211+
testFile.path,
212+
showGettersInDebugViews: true,
213+
),
214+
);
215+
216+
// Check the first level of variables are flagged as lazy with no values.
217+
final variables = await client.expectLocalVariable(
218+
stop.threadId!,
219+
expectedName: 'myVariable',
220+
expectedDisplayString: 'A',
221+
expectedVariables: '''
222+
publicString: , lazy: true
223+
runtimeType: , lazy: true
224+
''',
225+
ignorePrivate: false,
226+
);
227+
228+
// "Expand" publicString to ensure it resolve correctly to a single-field
229+
// variable with no name and the correct value/eval information.
230+
final namedRecordVariable = variables.variables
231+
.singleWhere((variable) => variable.name == 'publicString');
232+
expect(namedRecordVariable.variablesReference, isPositive);
233+
await client.expectVariables(
234+
namedRecordVariable.variablesReference,
235+
r'''
236+
: "111", eval: myVariable.publicString
237+
''',
238+
);
239+
});
240+
192241
test('includes record fields', () async {
193242
final client = dap.client;
194243
final testFile = dap.createTestFile('''

0 commit comments

Comments
 (0)