forked from dart-lang/webdev
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathinstance.dart
534 lines (501 loc) · 20.7 KB
/
instance.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:math';
import 'package:vm_service/vm_service.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
import '../loaders/strategy.dart';
import '../utilities/conversions.dart';
import '../utilities/domain.dart';
import '../utilities/objects.dart';
import '../utilities/shared.dart';
import 'debugger.dart';
import 'metadata/class.dart';
import 'metadata/function.dart';
/// Contains a set of methods for getting [Instance]s and [InstanceRef]s.
class InstanceHelper extends Domain {
InstanceHelper(AppInspectorInterface appInspector, this.debugger) {
inspector = appInspector;
}
final Debugger debugger;
static final InstanceRef kNullInstanceRef =
_primitiveInstanceRef(InstanceKind.kNull, null);
/// Creates an [InstanceRef] for a primitive [RemoteObject].
static InstanceRef _primitiveInstanceRef(
String kind, RemoteObject? remoteObject) {
final classRef = classRefFor('dart:core', kind);
return InstanceRef(
identityHashCode: dartIdFor(remoteObject?.value).hashCode,
kind: kind,
classRef: classRef,
id: dartIdFor(remoteObject?.value))
..valueAsString = '${remoteObject?.value}';
}
/// Creates an [Instance] for a primitive [RemoteObject].
Instance? _primitiveInstance(String kind, RemoteObject? remote) {
final objectId = remote?.objectId;
if (objectId == null) return null;
return Instance(
identityHashCode: objectId.hashCode,
id: objectId,
kind: kind,
classRef: classRefFor('dart:core', kind))
..valueAsString = '${remote?.value}';
}
Instance? _stringInstanceFor(
RemoteObject? remoteObject, int? offset, int? count) {
// TODO(#777) Consider a way of not passing the whole string around (in the
// ID) in order to find a substring.
final objectId = remoteObject?.objectId;
if (objectId == null) return null;
final fullString = stringFromDartId(objectId);
var preview = fullString;
var truncated = false;
if (offset != null || count != null) {
truncated = true;
final start = offset ?? 0;
final end = count == null ? null : min(start + count, fullString.length);
preview = fullString.substring(start, end);
}
return Instance(
identityHashCode: createId().hashCode,
kind: InstanceKind.kString,
classRef: classRefForString,
id: createId())
..valueAsString = preview
..valueAsStringIsTruncated = truncated
..length = fullString.length
..count = (truncated ? preview.length : null)
..offset = (truncated ? offset : null);
}
Future<Instance?> _closureInstanceFor(RemoteObject remoteObject) async {
final objectId = remoteObject.objectId;
if (objectId == null) return null;
final result = Instance(
kind: InstanceKind.kClosure,
id: objectId,
identityHashCode: remoteObject.objectId.hashCode,
classRef: classRefForClosure);
return result;
}
/// Create an [Instance] for the given [remoteObject].
///
/// Does a remote eval to get instance information. Returns null if there
/// isn't a corresponding instance. For enumerable objects, [offset] and
/// [count] allow retrieving a sub-range of properties.
Future<Instance?> instanceFor(RemoteObject? remoteObject,
{int? offset, int? count}) async {
final primitive = _primitiveInstanceOrNull(remoteObject, offset, count);
if (primitive != null) {
return primitive;
}
final objectId = remoteObject?.objectId;
if (remoteObject == null || objectId == null) return null;
// TODO: This is checking the JS object ID for the dart pattern we use for
// VM objects, which seems wrong (and, we catch 'string' types above).
if (isStringId(objectId)) {
return _stringInstanceFor(remoteObject, offset, count);
}
final metaData = await ClassMetaData.metaDataFor(
inspector.remoteDebugger, remoteObject, inspector);
final classRef = metaData?.classRef;
if (metaData == null || classRef == null) return null;
if (metaData.isFunction) {
return _closureInstanceFor(remoteObject);
}
final properties = await debugger.getProperties(objectId,
offset: offset, count: count, length: metaData.length);
if (metaData.isSystemList) {
return await _listInstanceFor(
classRef, remoteObject, properties, offset, count);
} else if (metaData.isSystemMap) {
return await _mapInstanceFor(classRef, remoteObject, offset, count);
} else if (metaData.isRecord) {
return await _recordInstanceFor(classRef, remoteObject, offset, count);
} else {
return await _plainInstanceFor(classRef, remoteObject, properties);
}
}
/// If [remoteObject] represents a primitive, return an [Instance] for it,
/// otherwise return null.
Instance? _primitiveInstanceOrNull(
RemoteObject? remoteObject, int? offset, int? count) {
switch (remoteObject?.type ?? 'undefined') {
case 'string':
return _stringInstanceFor(remoteObject, offset, count);
case 'number':
return _primitiveInstance(InstanceKind.kDouble, remoteObject);
case 'boolean':
return _primitiveInstance(InstanceKind.kBool, remoteObject);
case 'undefined':
return _primitiveInstance(InstanceKind.kNull, remoteObject);
default:
return null;
}
}
/// Create a bound field for [property] in an instance of [classRef].
Future<BoundField> _fieldFor(Property property, ClassRef classRef) async {
final instance = await _instanceRefForRemote(property.value);
return BoundField(
name: property.name,
decl: FieldRef(
// TODO(grouma) - Convert JS name to Dart.
name: property.name,
declaredType: InstanceRef(
kind: InstanceKind.kType,
classRef: instance?.classRef,
identityHashCode: createId().hashCode,
id: createId()),
owner: classRef,
// TODO(grouma) - Fill these in.
isConst: false,
isFinal: false,
isStatic: false,
id: createId(),
),
value: instance);
}
/// Create a plain instance of [classRef] from [remoteObject] and the JS
/// properties [properties].
Future<Instance?> _plainInstanceFor(ClassRef classRef,
RemoteObject remoteObject, List<Property> properties) async {
final objectId = remoteObject.objectId;
if (objectId == null) return null;
final dartProperties = await _dartFieldsFor(properties, remoteObject);
var boundFields = await Future.wait(
dartProperties.map<Future<BoundField>>((p) => _fieldFor(p, classRef)));
boundFields = boundFields
.where((bv) => isDisplayableObject(bv.value))
.toList()
..sort(_compareBoundFields);
final result = Instance(
kind: InstanceKind.kPlainInstance,
id: objectId,
identityHashCode: remoteObject.objectId.hashCode,
classRef: classRef)
..fields = boundFields;
return result;
}
int _compareBoundFields(BoundField a, BoundField b) {
final aName = a.decl?.name;
final bName = b.decl?.name;
if (aName == null) return bName == null ? 0 : -1;
if (bName == null) return 1;
return aName.compareTo(bName);
}
/// The associations for a Dart Map or IdentityMap.
Future<List<MapAssociation>> _mapAssociations(
RemoteObject map, int? offset, int? count) async {
// We do this in in awkward way because we want the keys and values, but we
// can't return things by value or some Dart objects will come back as
// values that we need to be RemoteObject, e.g. a List of int.
final expression = '''
function() {
var sdkUtils = ${globalLoadStrategy.loadModuleSnippet}('dart_sdk').dart;
var entries = sdkUtils.dloadRepl(this, "entries");
entries = sdkUtils.dsendRepl(entries, "toList", []);
function asKey(entry) {
return sdkUtils.dloadRepl(entry, "key");
}
function asValue(entry) {
return sdkUtils.dloadRepl(entry, "value");
}
return {
keys: entries.map(asKey),
values: entries.map(asValue)
};
}
''';
final keysAndValues = await inspector.jsCallFunctionOn(map, expression, []);
final keys = await inspector.loadField(keysAndValues, 'keys');
final values = await inspector.loadField(keysAndValues, 'values');
final keysInstance = await instanceFor(keys, offset: offset, count: count);
final valuesInstance =
await instanceFor(values, offset: offset, count: count);
final associations = <MapAssociation>[];
final keyElements = keysInstance?.elements;
final valueElements = valuesInstance?.elements;
if (keyElements != null && valueElements != null) {
Map.fromIterables(keyElements, valueElements).forEach((key, value) {
associations.add(MapAssociation(key: key, value: value));
});
}
return associations;
}
/// Create a Map instance with class [classRef] from [remoteObject].
Future<Instance?> _mapInstanceFor(ClassRef classRef,
RemoteObject remoteObject, int? offset, int? count) async {
final objectId = remoteObject.objectId;
if (objectId == null) return null;
// Maps are complicated, do an eval to get keys and values.
final associations = await _mapAssociations(remoteObject, offset, count);
final length = count == null
? associations.length
: (await instanceRefFor(remoteObject))?.length;
return Instance(
identityHashCode: remoteObject.objectId.hashCode,
kind: InstanceKind.kMap,
id: objectId,
classRef: classRef)
..length = length
..offset = offset
..count = (associations.length == length) ? null : associations.length
..associations = associations;
}
/// Create a List instance of [classRef] from [remoteObject] with the JS
/// properties [properties].
Future<Instance?> _listInstanceFor(
ClassRef classRef,
RemoteObject remoteObject,
List<Property> properties,
int? offset,
int? count) async {
final objectId = remoteObject.objectId;
if (objectId == null) return null;
/// TODO(annagrin): split into cases to make the logic clear.
final numberOfProperties = _lengthOf(properties) ?? 0;
final length = count == null
? numberOfProperties
: (await instanceRefFor(remoteObject))?.length;
final indexed = properties.sublist(
0, min(count ?? length ?? numberOfProperties, numberOfProperties));
final fields = await Future.wait(indexed
.map((property) async => await _instanceRefForRemote(property.value)));
return Instance(
identityHashCode: remoteObject.objectId.hashCode,
kind: InstanceKind.kList,
id: objectId,
classRef: classRef)
..length = length
..elements = fields
..offset = offset
..count = (numberOfProperties == length) ? null : numberOfProperties;
}
/// The fields for a Dart Record.
Future<List<BoundField>> _recordFields(
RemoteObject map, int? offset, int? count) async {
// We do this in in awkward way because we want the keys and values, but we
// can't return things by value or some Dart objects will come back as
// values that we need to be RemoteObject, e.g. a List of int.
final expression = '''
function() {
var sdkUtils = ${globalLoadStrategy.loadModuleSnippet}('dart_sdk').dart;
var shape = sdkUtils.dloadRepl(this, "shape");
var positionals = sdkUtils.dloadRepl(shape, "positionals");
var named = sdkUtils.dloadRepl(shape, "named");
var keys = new Array();
for (var i = 0; i < positionals; i++) {
keys.push(i);
}
for (var key in named) {
key.push(key);
}
var values = sdkUtils.dloadRepl(this, "values");
values = sdkUtils.dsendRepl(values, "toList", []);
return {
keys: keys,
values: values
};
}
''';
final keysAndValues = await inspector.jsCallFunctionOn(map, expression, []);
final keys = await inspector.loadField(keysAndValues, 'keys');
final values = await inspector.loadField(keysAndValues, 'values');
final keysInstance = await instanceFor(keys, offset: offset, count: count);
final valuesInstance =
await instanceFor(values, offset: offset, count: count);
final fields = <BoundField>[];
final keyElements = keysInstance?.elements;
final valueElements = valuesInstance?.elements;
if (keyElements != null && valueElements != null) {
Map.fromIterables(keyElements, valueElements).forEach((key, value) {
fields.add(BoundField(name: key.valueAsString, value: value));
});
}
return fields;
}
/// Create a Map instance with class [classRef] from [remoteObject].
Future<Instance?> _recordInstanceFor(ClassRef classRef,
RemoteObject remoteObject, int? offset, int? count) async {
final objectId = remoteObject.objectId;
if (objectId == null) return null;
// Records are complicated, do an eval to get names and values.
final fields = await _recordFields(remoteObject, offset, count);
final length = (count == null)
? fields.length
: (await instanceRefFor(remoteObject))?.length;
return Instance(
identityHashCode: remoteObject.objectId.hashCode,
kind: InstanceKind.kRecord,
id: objectId,
classRef: classRef)
..length = length
..offset = offset
..count = (fields.length == length) ? null : fields.length
..fields = fields;
}
/// Return the value of the length attribute from [properties], if present.
///
/// This is only applicable to Lists or Maps, where we expect a length
/// attribute. Even if a plain instance happens to have a length field, we
/// don't use it to determine the properties to display.
int? _lengthOf(List<Property> properties) {
final lengthProperty = properties.firstWhere((p) => p.name == 'length');
return lengthProperty.value?.value as int?;
}
/// Filter [allJsProperties] and return a list containing only those
/// that correspond to Dart fields on [remoteObject].
///
/// This only applies to objects with named fields, not Lists or Maps.
Future<List<Property>> _dartFieldsFor(
List<Property> allJsProperties, RemoteObject remoteObject) async {
// An expression to find the field names from the types, extract both
// private (named by symbols) and public (named by strings) and return them
// as a comma-separated single string, so we can return it by value and not
// need to make multiple round trips.
//
// For maps and lists it's more complicated. Treat the actual SDK versions
// of these as special.
final fieldNameExpression = '''function() {
const sdk = ${globalLoadStrategy.loadModuleSnippet}("dart_sdk");
const sdk_utils = sdk.dart;
const fields = sdk_utils.getFields(sdk_utils.getType(this)) || [];
if (!fields && (dart_sdk._interceptors.JSArray.is(this) ||
dart_sdk._js_helper.InternalMap.is(this))) {
// Trim off the 'length' property.
const fields = allJsProperties.slice(0, allJsProperties.length -1);
return fields.join(',');
}
const privateFields = sdk_utils.getOwnPropertySymbols(fields);
const nonSymbolNames = privateFields
.map(sym => sym.description
.split('#').slice(-1)[0]);
const publicFieldNames = sdk_utils.getOwnPropertyNames(fields);
const symbolNames = Object.getOwnPropertySymbols(this)
.map(sym => sym.description
.split('#').slice(-1)[0]
.split('.').slice(-1)[0]);
return nonSymbolNames
.concat(publicFieldNames)
.concat(symbolNames).join(',');
}
''';
final allNames = (await inspector
.jsCallFunctionOn(remoteObject, fieldNameExpression, []))
.value as String;
final names = allNames.split(',');
// TODO(#761): Better support for large collections.
return allJsProperties
.where((property) => names.contains(property.name))
.toList();
}
/// Create an InstanceRef for an object, which may be a RemoteObject, or may
/// be something returned by value from Chrome, e.g. number, boolean, or
/// String.
Future<InstanceRef?> instanceRefFor(Object value) {
final remote = value is RemoteObject
? value
: RemoteObject({'value': value, 'type': _chromeType(value)});
return _instanceRefForRemote(remote);
}
/// The Chrome type for a value.
String? _chromeType(Object? value) {
if (value == null) return null;
if (value is String) return 'string';
if (value is num) return 'number';
if (value is bool) return 'boolean';
if (value is Function) return 'function';
return 'object';
}
/// Create an [InstanceRef] for the given Chrome [remoteObject].
Future<InstanceRef?> _instanceRefForRemote(RemoteObject? remoteObject) async {
// If we have a null result, treat it as a reference to null.
if (remoteObject == null) {
return kNullInstanceRef;
}
switch (remoteObject.type) {
case 'string':
final stringValue = remoteObject.value as String?;
// TODO: Support truncation for long strings.
// TODO(#777): dartIdFor() will return an ID containing the entire
// string, even if we're truncating the string value here.
return InstanceRef(
identityHashCode: dartIdFor(remoteObject.value).hashCode,
id: dartIdFor(remoteObject.value),
classRef: classRefForString,
kind: InstanceKind.kString)
..valueAsString = stringValue
..length = stringValue?.length;
case 'number':
return _primitiveInstanceRef(InstanceKind.kDouble, remoteObject);
case 'boolean':
return _primitiveInstanceRef(InstanceKind.kBool, remoteObject);
case 'undefined':
return _primitiveInstanceRef(InstanceKind.kNull, remoteObject);
case 'object':
final objectId = remoteObject.objectId;
if (objectId == null) {
return _primitiveInstanceRef(InstanceKind.kNull, remoteObject);
}
final metaData = await ClassMetaData.metaDataFor(
inspector.remoteDebugger, remoteObject, inspector);
if (metaData == null) return null;
if (metaData.isSystemList) {
return InstanceRef(
kind: InstanceKind.kList,
id: objectId,
identityHashCode: remoteObject.objectId.hashCode,
classRef: metaData.classRef)
..length = metaData.length;
}
if (metaData.isSystemMap) {
return InstanceRef(
kind: InstanceKind.kMap,
id: objectId,
identityHashCode: remoteObject.objectId.hashCode,
classRef: metaData.classRef)
..length = metaData.length;
}
if (metaData.isRecord) {
return InstanceRef(
kind: InstanceKind.kRecord,
id: objectId,
identityHashCode: remoteObject.objectId.hashCode,
classRef: metaData.classRef)
..length = metaData.length;
}
return InstanceRef(
kind: InstanceKind.kPlainInstance,
id: objectId,
identityHashCode: remoteObject.objectId.hashCode,
classRef: metaData.classRef);
case 'function':
final functionMetaData = await FunctionMetaData.metaDataFor(
inspector.remoteDebugger, remoteObject);
final objectId = remoteObject.objectId;
if (objectId == null) {
return _primitiveInstanceRef(InstanceKind.kNull, remoteObject);
}
return InstanceRef(
kind: InstanceKind.kClosure,
id: objectId,
identityHashCode: remoteObject.objectId.hashCode,
classRef: classRefForClosure)
// TODO(grouma) - fill this in properly.
..closureFunction = FuncRef(
name: functionMetaData.name,
id: createId(),
// TODO(alanknight): The right ClassRef
owner: classRefForUnknown,
isConst: false,
isStatic: false,
// TODO(annagrin): get information about getters and setters from symbols.
// https://github.com/dart-lang/sdk/issues/46723
implicit: false)
..closureContext = (ContextRef(length: 0, id: createId()));
default:
// Return null for an unsupported type. This is likely a JS construct.
return null;
}
}
}