| // 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:async'; |
| |
| import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; |
| |
| import '../utilities/conversions.dart'; |
| import '../utilities/domain.dart'; |
| import '../utilities/objects.dart'; |
| import '../utilities/shared.dart'; |
| import '../utilities/wrapped_service.dart'; |
| import 'classes.dart'; |
| import 'inspector.dart'; |
| import 'metadata.dart'; |
| |
| /// Creates an [InstanceRef] for a primitive [RemoteObject]. |
| InstanceRef _primitiveInstance(String kind, RemoteObject remoteObject) { |
| var classRef = classRefFor('dart:core', kind); |
| return InstanceRef( |
| kind: kind, classRef: classRef, id: dartIdFor(remoteObject?.value)) |
| ..valueAsString = '${remoteObject?.value}'; |
| } |
| |
| /// Contains a set of methods for getting [Instance]s and [InstanceRef]s. |
| class InstanceHelper extends Domain { |
| InstanceHelper(AppInspector Function() provider) : super(provider); |
| |
| Future<Instance> _stringInstanceFor(RemoteObject remoteObject) async { |
| var actualString = stringFromDartId(remoteObject.objectId); |
| return Instance( |
| kind: InstanceKind.kString, classRef: classRefForString, id: createId()) |
| ..valueAsString = actualString |
| ..length = actualString.length; |
| } |
| |
| Future<Instance> _closureInstanceFor(RemoteObject remoteObject) async { |
| var result = Instance( |
| kind: InstanceKind.kClosure, |
| id: remoteObject.objectId, |
| 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. |
| Future<Instance> instanceFor(RemoteObject remoteObject) async { |
| if (isStringId(remoteObject.objectId)) { |
| return _stringInstanceFor(remoteObject); |
| } |
| var metaData = await ClassMetaData.metaDataFor( |
| inspector.remoteDebugger, remoteObject, inspector); |
| var classRef = metaData.classRef; |
| if (metaData.jsName == 'Function') { |
| return _closureInstanceFor(remoteObject); |
| } |
| var properties = |
| await inspector.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 { |
| 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 { |
| // 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. |
| var expression = ''' |
| function() { |
| var sdkUtils = $loadModule('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) |
| }; |
| } |
| '''; |
| var keysAndValues = await inspector.jsCallFunctionOn(map, expression, []); |
| var keys = await inspector.loadField(keysAndValues, 'keys'); |
| var values = await inspector.loadField(keysAndValues, 'values'); |
| var keysInstance = await instanceFor(keys); |
| var valuesInstance = await instanceFor(values); |
| var associations = <MapAssociation>[]; |
| Map.fromIterables(keysInstance.elements, valuesInstance.elements) |
| .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, 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 [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 = $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 = sdk_utils.getOwnPropertyNames(fields); |
| return nonSymbolNames.concat(publicFieldNames).join(','); |
| } |
| '''; |
| var allNames = (await inspector |
| .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> _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); |
| } |
| switch (remoteObject.type) { |
| case 'string': |
| return InstanceRef( |
| id: dartIdFor(remoteObject.value), |
| classRef: classRefForString, |
| kind: InstanceKind.kString) |
| ..valueAsString = remoteObject.value as String; |
| case 'number': |
| return _primitiveInstance(InstanceKind.kDouble, remoteObject); |
| case 'boolean': |
| return _primitiveInstance(InstanceKind.kBool, remoteObject); |
| case 'undefined': |
| return _primitiveInstance(InstanceKind.kNull, remoteObject); |
| case 'object': |
| if (remoteObject.objectId == null) { |
| return _primitiveInstance(InstanceKind.kNull, remoteObject); |
| } |
| var metaData = await ClassMetaData.metaDataFor( |
| inspector.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: metaData.classRef); |
| case 'function': |
| var functionMetaData = await FunctionMetaData.metaDataFor( |
| inspector.remoteDebugger, remoteObject); |
| return InstanceRef( |
| kind: InstanceKind.kClosure, |
| id: remoteObject.objectId, |
| 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) |
| ..closureContext = (ContextRef()..length = 0); |
| default: |
| // Return null for an unsupported type. This is likely a JS construct. |
| return null; |
| } |
| } |
| } |