blob: 7c029604559a92343c3f1a5b1f17fe71c0e1a0d3 [file] [log] [blame]
// 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;
}
}
}