Handle Lists and Maps (#762)
diff --git a/dwds/lib/src/debugging/inspector.dart b/dwds/lib/src/debugging/inspector.dart
index 03d71d5..de8bad5 100644
--- a/dwds/lib/src/debugging/inspector.dart
+++ b/dwds/lib/src/debugging/inspector.dart
@@ -324,8 +324,10 @@
.map((p) => p.value)
.filter((l) => l && sdkUtils.isType(l));
var classList = classes.map(function(clazz) {
- var descriptor = {'name': clazz.name};
-
+ var descriptor = {
+ 'name': clazz.name,
+ 'dartName': sdkUtils.typeName(clazz)
+ };
// TODO(jakemac): static methods once ddc supports them
var methods = sdkUtils.getMethods(clazz);
var methodNames = methods ? Object.keys(methods) : [];
@@ -353,6 +355,7 @@
"isFinal": field.isFinal,
"isStatic": false,
"classRefName": fields[name]["type"]["name"],
+ "classRefDartName": sdkUtils.typeName(fields[name]["type"]),
"classRefLibraryId" : field["type"][libraryUri],
}
}
@@ -370,9 +373,11 @@
.cast<Map<String, Object>>();
var classRefs = <ClassRef>[];
for (var classDescriptor in classDescriptors) {
- var classMetaData =
- ClassMetaData(classDescriptor['name'] as String, libraryRef.id);
- var classRef = ClassRef(name: classMetaData.name, id: classMetaData.id);
+ var classMetaData = ClassMetaData(
+ jsName: classDescriptor['name'] as String,
+ libraryId: libraryRef.id,
+ dartName: classDescriptor['dartName'] as String);
+ var classRef = ClassRef(name: classMetaData.jsName, id: classMetaData.id);
classRefs.add(classRef);
var methodRefs = <FuncRef>[];
@@ -392,9 +397,9 @@
var fieldDescriptors = classDescriptor['fields'] as Map<String, dynamic>;
fieldDescriptors.forEach((name, descriptor) async {
var classMetaData = ClassMetaData(
- descriptor['classRefName'] as String,
- descriptor['classRefLibraryId'] as String,
- );
+ jsName: descriptor['classRefName'],
+ libraryId: descriptor['classRefLibraryId'],
+ dartName: descriptor['classRefDartName']);
fieldRefs.add(FieldRef(
name: name,
owner: classRef,
@@ -402,7 +407,7 @@
id: createId(),
kind: InstanceKind.kType,
classRef:
- ClassRef(name: classMetaData.name, id: classMetaData.id)),
+ ClassRef(name: classMetaData.jsName, id: classMetaData.id)),
isConst: descriptor['isConst'] as bool,
isFinal: descriptor['isFinal'] as bool,
isStatic: descriptor['isStatic'] as bool,
@@ -412,7 +417,7 @@
// TODO: Implement the rest of these
// https://github.com/dart-lang/webdev/issues/176.
_classes[classMetaData.id] = Class(
- name: classMetaData.name,
+ name: classMetaData.jsName,
isAbstract: false,
isConst: false,
library: libraryRef,
diff --git a/dwds/lib/src/debugging/instance.dart b/dwds/lib/src/debugging/instance.dart
index e71fb49..2952cec 100644
--- a/dwds/lib/src/debugging/instance.dart
+++ b/dwds/lib/src/debugging/instance.dart
@@ -73,56 +73,147 @@
}
var metaData = await ClassMetaData.metaDataFor(
_remoteDebugger, remoteObject, inspector);
- if (metaData.name == 'Function') {
+ var classRef = metaData.classRef;
+ if (metaData.jsName == 'Function') {
return _closureInstanceFor(remoteObject);
+ }
+ var properties = await _debugger.getProperties(remoteObject.objectId);
+ if (metaData.jsName == 'JSArray') {
+ return await _listInstanceFor(classRef, remoteObject, properties);
+ } else if (metaData.jsName == 'LinkedMap' ||
+ metaData.jsName == 'IdentityMap') {
+ return await _mapInstanceFor(classRef, remoteObject, properties);
} else {
- var classRef = ClassRef(id: metaData.id, name: metaData.name);
- var properties = await _debugger.getProperties(remoteObject.objectId);
- var dartProperties = await dartPropertiesFor(properties, remoteObject);
- var fields = await Future.wait(
- dartProperties.map<Future<BoundField>>((property) async {
- var instance = await instanceRefFor(property.value);
- return BoundField()
- ..decl = (FieldRef(
- // TODO(grouma) - Convert JS name to Dart.
- name: property.name,
- declaredType: (InstanceRef(
- kind: InstanceKind.kType,
- classRef: instance.classRef,
- id: createId())),
- owner: classRef,
- // TODO(grouma) - Fill these in.
- isConst: false,
- isFinal: false,
- isStatic: false,
- id: createId()))
- ..value = instance;
- }));
- fields.sort((a, b) => a.decl.name.compareTo(b.decl.name));
- var result = Instance(
- kind: InstanceKind.kPlainInstance,
- id: remoteObject.objectId,
- classRef: classRef)
- ..fields = fields;
- return result;
+ return await _plainInstanceFor(classRef, remoteObject, properties);
}
}
+ /// Create a bound field for [property] in an instance of [classRef].
+ Future<BoundField> _fieldFor(Property property, ClassRef classRef) async {
+ var instance = await _instanceRefForRemote(property.value);
+ return BoundField()
+ ..decl = (FieldRef(
+ // TODO(grouma) - Convert JS name to Dart.
+ name: property.name,
+ declaredType: (InstanceRef(
+ kind: InstanceKind.kType,
+ classRef: instance.classRef,
+ 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 {
+ var dartProperties = await _dartFieldsFor(properties, remoteObject);
+ var fields = await Future.wait(
+ dartProperties.map<Future<BoundField>>((p) => _fieldFor(p, classRef)));
+ fields = fields.toList()
+ ..sort((a, b) => a.decl.name.compareTo(b.decl.name));
+ var result = Instance(
+ kind: InstanceKind.kPlainInstance,
+ id: remoteObject.objectId,
+ classRef: classRef)
+ ..fields = fields;
+ return result;
+ }
+
+ /// The associations for a Dart Map or IdentityMap.
+ Future<List<MapAssociation>> _mapAssociations(RemoteObject map) async {
+ var expression = '''
+ function() {
+ var sdkUtils = $loadModule('dart_sdk').dart;
+ var entries = sdkUtils.dloadRepl(this, "entries");
+ entries = sdkUtils.dsendRepl(entries, "toList", []);
+ function asKeyValue(entry) {
+ return {
+ key: sdkUtils.dloadRepl(entry, "key"),
+ value: sdkUtils.dloadRepl(entry, "value")
+ }
+ }
+ return entries.map(asKeyValue);
+ }
+ ''';
+ var keysAndValues = await inspector.jsCallFunctionOn(map, expression, [],
+ returnByValue: true);
+ var associations = <MapAssociation>[];
+ for (var each in keysAndValues.value as List) {
+ associations.add(MapAssociation()
+ ..key = await instanceRefFor(each['key'])
+ ..value = await instanceRefFor(each['value']));
+ }
+ return associations;
+ }
+
+ /// Create a Map instance with class [classRef] from [remoteObject].
+ Future<Instance> _mapInstanceFor(
+ ClassRef classRef, RemoteObject remoteObject, List<Property> _) async {
+ // Maps are complicated, do an eval to get keys and values.
+ var associations = await _mapAssociations(remoteObject);
+ return Instance(
+ kind: InstanceKind.kMap, id: remoteObject.objectId, classRef: classRef)
+ ..length = 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) async {
+ var length = _lengthOf(properties);
+ var indexed = properties.sublist(0, length);
+ var fields = await Future.wait(indexed
+ .map((property) async => await _instanceRefForRemote(property.value)));
+ return Instance(
+ kind: InstanceKind.kList, id: remoteObject.objectId, classRef: classRef)
+ ..length = length
+ ..elements = 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) {
+ var 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 the object.
- Future<List<Property>> dartPropertiesFor(
+ /// 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.
// TODO(alanknight): Handle superclass fields.
final fieldNameExpression = '''function() {
- const sdk_utils = $loadModule("dart_sdk").dart;
- const fields = sdk_utils.getFields(sdk_utils.getType(this));
- const privateFields = Object.getOwnPropertySymbols(fields);
+ const sdk = $loadModule("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);
- const publicFieldNames = Object.getOwnPropertyNames(fields);
+ const publicFieldNames = sdk_utils.getOwnPropertyNames(fields);
return nonSymbolNames.concat(publicFieldNames).join(',');
}
''';
@@ -130,13 +221,34 @@
.jsCallFunctionOn(remoteObject, fieldNameExpression, []))
.value as String;
var 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) {
+ var 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> instanceRefFor(RemoteObject remoteObject) async {
+ Future<InstanceRef> _instanceRefForRemote(RemoteObject remoteObject) async {
// If we have a null result, treat it as a reference to null.
if (remoteObject == null) {
return _primitiveInstance(InstanceKind.kNull, remoteObject);
@@ -155,16 +267,31 @@
case 'undefined':
return _primitiveInstance(InstanceKind.kNull, remoteObject);
case 'object':
- if (remoteObject.type == 'object' && remoteObject.objectId == null) {
+ if (remoteObject.objectId == null) {
return _primitiveInstance(InstanceKind.kNull, remoteObject);
}
var metaData = await ClassMetaData.metaDataFor(
_remoteDebugger, remoteObject, inspector);
if (metaData == null) return null;
+ if (metaData.jsName == 'JSArray') {
+ return InstanceRef(
+ kind: InstanceKind.kList,
+ id: remoteObject.objectId,
+ classRef: metaData.classRef)
+ ..length = metaData.length;
+ }
+ if (metaData.jsName == 'LinkedMap' ||
+ metaData.jsName == 'IdentityMap') {
+ return InstanceRef(
+ kind: InstanceKind.kMap,
+ id: remoteObject.objectId,
+ classRef: metaData.classRef)
+ ..length = metaData.length;
+ }
return InstanceRef(
kind: InstanceKind.kPlainInstance,
id: remoteObject.objectId,
- classRef: ClassRef(name: metaData.name, id: metaData.id));
+ classRef: metaData.classRef);
case 'function':
var functionMetaData =
await FunctionMetaData.metaDataFor(_remoteDebugger, remoteObject);
diff --git a/dwds/lib/src/debugging/metadata.dart b/dwds/lib/src/debugging/metadata.dart
index 6f003c2..16c04dc 100644
--- a/dwds/lib/src/debugging/metadata.dart
+++ b/dwds/lib/src/debugging/metadata.dart
@@ -2,24 +2,45 @@
// 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:async';
-import 'package:dwds/src/utilities/shared.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
import '../debugging/inspector.dart';
import '../services/chrome_proxy_service.dart';
-
+import '../utilities/shared.dart';
+import '../utilities/wrapped_service.dart';
import 'remote_debugger.dart';
/// Meta data for a remote Dart class in Chrome.
class ClassMetaData {
- final String name;
+ /// The name of the JS constructor for the object.
+ ///
+ /// This may be a constructor for a Dart, but it's still a JS name. For
+ /// example, 'Number', 'JSArray', 'Object'.
+ final String jsName;
+
+ /// The length of the object, if applicable.
+ final int length;
+
+ /// The dart type name for the object.
+ ///
+ /// For example, 'int', 'List<String>', 'Null'
+ final String dartName;
+
+ /// The library identifier, which is the URI of the library.
final String libraryId;
- ClassMetaData(this.name, this.libraryId);
+
+ factory ClassMetaData(
+ {Object jsName, Object libraryId, Object dartName, Object length}) {
+ return ClassMetaData._(jsName as String, libraryId as String,
+ dartName as String, length as int);
+ }
+
+ ClassMetaData._(this.jsName, this.libraryId, this.dartName, this.length);
/// Returns the ID of the class.
///
/// Takes the form of 'libraryId:name'.
- String get id => '$libraryId:$name';
+ String get id => '$libraryId:$jsName';
/// Returns the [ClassMetaData] for the Chrome [remoteObject].
///
@@ -29,10 +50,13 @@
try {
var evalExpression = '''
function(arg) {
- var sdkUtils = $loadModule('dart_sdk').dart;
- var classObject = sdkUtils.getType(arg);
- var result = {};
- result['name'] = classObject.name;
+ const sdkUtils = $loadModule('dart_sdk').dart;
+ const classObject = sdkUtils.getReifiedType(arg);
+ const isFunction = sdkUtils.AbstractFunctionType.is(classObject);
+ const result = {};
+ result['name'] = isFunction ? 'Function' : classObject.name;
+ result['dartName'] = sdkUtils.typeName(classObject);
+ result['length'] = arg['length'];
result['libraryId'] = sdkUtils.getLibraryUri(classObject);
return result;
}
@@ -42,11 +66,17 @@
returnByValue: true);
var metadata = result.value as Map;
return ClassMetaData(
- metadata['name'] as String, metadata['libraryId'] as String);
+ jsName: metadata['name'],
+ libraryId: metadata['libraryId'],
+ dartName: metadata['dartName'],
+ length: metadata['length']);
} on ChromeDebugException {
return null;
}
}
+
+ /// Return a [ClassRef] appropriate to this metadata.
+ ClassRef get classRef => ClassRef(name: dartName, id: id);
}
/// Meta data for a remote Dart function in Chrome.
diff --git a/dwds/test/instance_test.dart b/dwds/test/instance_test.dart
index c3eab41..e0bbe6c 100644
--- a/dwds/test/instance_test.dart
+++ b/dwds/test/instance_test.dart
@@ -45,9 +45,15 @@
'$loadModule("dart_sdk").dart.getModuleLibraries("web/scopes_main")'
'["$url"]["$variable"];';
+ /// A reference to the the variable `libraryPublicFinal`, an instance of
+ /// `MyTestClass`.
Future<RemoteObject> libraryPublicFinal() =>
inspector.jsEvaluate(libraryVariableExpression('libraryPublicFinal'));
+ /// A reference to the the variable `libraryPublic`, a List of Strings.
+ Future<RemoteObject> libraryPublic() =>
+ inspector.jsEvaluate(libraryVariableExpression('libraryPublic'));
+
group('instanceRef', () {
test('for a null', () async {
var remoteObject = await libraryPublicFinal();
@@ -95,6 +101,32 @@
}
expect(instanceRef.kind, InstanceKind.kClosure);
});
+
+ test('for a list', () async {
+ var remoteObject = await libraryPublic();
+ var ref = await instanceHelper.instanceRefFor(remoteObject);
+ expect(ref.length, greaterThan(0));
+ expect(ref.kind, InstanceKind.kList);
+ expect(ref.classRef.name, 'List<String>');
+ });
+
+ test('for map', () async {
+ var remoteObject =
+ await inspector.jsEvaluate(libraryVariableExpression('map'));
+ var ref = await instanceHelper.instanceRefFor(remoteObject);
+ expect(ref.length, 1);
+ expect(ref.kind, InstanceKind.kMap);
+ expect(ref.classRef.name, 'LinkedMap<Object, Object>');
+ });
+
+ test('for an IdentityMap', () async {
+ var remoteObject =
+ await inspector.jsEvaluate(libraryVariableExpression('identityMap'));
+ var ref = await instanceHelper.instanceRefFor(remoteObject);
+ expect(ref.length, 2);
+ expect(ref.kind, InstanceKind.kMap);
+ expect(ref.classRef.name, 'IdentityMap<String, int>');
+ });
});
group('instance', () {
@@ -131,7 +163,7 @@
expect(instance.classRef.name, 'Closure');
});
- test('for a nested class ', () async {
+ test('for a nested class', () async {
var libraryRemoteObject = await libraryPublicFinal();
var fieldRemoteObject =
await inspector.loadField(libraryRemoteObject, 'myselfField');
@@ -141,5 +173,50 @@
expect(classRef, isNotNull);
expect(classRef.name, 'MyTestClass');
});
+
+ test('for a list', () async {
+ var remote = await libraryPublic();
+ var instance = await instanceHelper.instanceFor(remote);
+ expect(instance.kind, InstanceKind.kList);
+ var classRef = instance.classRef;
+ expect(classRef, isNotNull);
+ expect(classRef.name, 'List<String>');
+ var first = instance.elements[0];
+ expect(first.valueAsString, 'library');
+ });
+
+ test('for a map', () async {
+ var remote = await inspector.jsEvaluate(libraryVariableExpression('map'));
+ var instance = await instanceHelper.instanceFor(remote);
+ expect(instance.kind, InstanceKind.kMap);
+ var classRef = instance.classRef;
+ expect(classRef.name, 'LinkedMap<Object, Object>');
+ var first = instance.associations[0].value;
+ expect(first.valueAsString, '1');
+ });
+
+ test('for an identityMap', () async {
+ var remote =
+ await inspector.jsEvaluate(libraryVariableExpression('identityMap'));
+ var instance = await instanceHelper.instanceFor(remote);
+ expect(instance.kind, InstanceKind.kMap);
+ var classRef = instance.classRef;
+ expect(classRef.name, 'IdentityMap<String, int>');
+ var first = instance.associations[0].value;
+ expect(first.valueAsString, '1');
+ });
+
+ test('for a class that implements List', () async {
+ // The VM only uses kind List for SDK lists, and we follow that.
+ var remote =
+ await inspector.jsEvaluate(libraryVariableExpression('notAList'));
+ var instance = await instanceHelper.instanceFor(remote);
+ expect(instance.kind, InstanceKind.kPlainInstance);
+ var classRef = instance.classRef;
+ expect(classRef.name, 'NotReallyAList');
+ expect(instance.elements, isNull);
+ var field = instance.fields.first;
+ expect(field.decl.name, '_internal');
+ });
});
}
diff --git a/example/web/scopes_main.dart b/example/web/scopes_main.dart
index 114c7e1..ad1b287 100644
--- a/example/web/scopes_main.dart
+++ b/example/web/scopes_main.dart
@@ -5,22 +5,32 @@
/// An example with more complicated scope
import 'dart:async';
-import 'dart:html';
+
+import 'dart:collection';
final libraryPublicFinal = MyTestClass();
final _libraryPrivateFinal = 1;
Object libraryNull;
-var libraryPublic = ['library', 'public', 'variable'];
+var libraryPublic = <String>['library', 'public', 'variable'];
+var notAList = NotReallyAList();
var _libraryPrivate = ['library', 'private', 'variable'];
+var identityMap = <String, int>{};
+
+var map = <Object, Object>{};
+
void main() async {
print('Initial print from scopes app');
var local = 'local in main';
var intLocalInMain = 42;
var testClass = MyTestClass();
Object localThatsNull;
+ identityMap['a'] = 1;
+ identityMap['b'] = 2;
+ map['a'] = 1;
+ notAList.add(7);
String nestedFunction(String parameter) {
var another = int.tryParse(parameter);
@@ -40,12 +50,13 @@
print(nestedFunction('$ticks ${testClass.message}'));
print(localThatsNull);
print(libraryNull);
+ var localList = libraryPublic;
+ print(localList);
+ localList.add('abc');
var f = testClass.methodWithVariables();
print(f('parameter'));
});
- document.body.append(SpanElement()..text = 'Exercising some scopes');
- window.console.debug('Page Ready');
print(_libraryPrivateFinal);
print(_libraryPrivate);
print(nestedFunction(_libraryPrivate.first));
@@ -117,3 +128,17 @@
// ignore: unused_element
int _libraryPrivateFunction(int a, int b) => a + b;
+
+class NotReallyAList extends ListBase {
+ final List _internal;
+
+ NotReallyAList() : _internal = [];
+ @override
+ Object operator [](x) => _internal[x];
+ @override
+ operator []=(x, y) => _internal[x] = y;
+ @override
+ int get length => _internal.length;
+ @override
+ set length(x) => _internal.length = x;
+}