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;
+}