// 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:dwds/src/config/tool_configuration.dart'; import 'package:dwds/src/debugging/chrome_inspector.dart'; import 'package:dwds/src/debugging/metadata/class.dart'; import 'package:dwds/src/debugging/metadata/function.dart'; import 'package:dwds/src/utilities/conversions.dart'; import 'package:dwds/src/utilities/objects.dart'; import 'package:dwds/src/utilities/shared.dart'; import 'package:logging/logging.dart'; import 'package:vm_service/vm_service.dart'; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; /// Contains a set of methods for getting [Instance]s and [InstanceRef]s. class ChromeAppInstanceHelper { final _logger = Logger('InstanceHelper'); final ChromeClassMetaDataHelper metadataHelper; final ChromeAppInspector inspector; ChromeAppInstanceHelper(this.inspector) : metadataHelper = ChromeClassMetaDataHelper(inspector); 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? remoteObject) { final objectId = remoteObject?.objectId; if (objectId == null) return null; return Instance( identityHashCode: objectId.hashCode, id: objectId, kind: kind, classRef: classRefFor('dart:core', kind), valueAsString: '${remoteObject?.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), ); } Instance? _closureInstanceFor(RemoteObject remoteObject) { 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 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; final metaData = await metadataHelper.metaDataFor(remoteObject); final classRef = metaData?.classRef; if (metaData == null || classRef == null) return null; switch (metaData.runtimeKind) { case RuntimeObjectKind.function: return _closureInstanceFor(remoteObject); case RuntimeObjectKind.recordType: return await _recordTypeInstanceFor( metaData, remoteObject, offset: offset, count: count, ); case RuntimeObjectKind.type: return await _plainTypeInstanceFor( metaData, remoteObject, offset: offset, count: count, ); case RuntimeObjectKind.list: return await _listInstanceFor( metaData, remoteObject, offset: offset, count: count, ); case RuntimeObjectKind.set: return await _setInstanceFor( metaData, remoteObject, offset: offset, count: count, ); case RuntimeObjectKind.map: return await _mapInstanceFor( metaData, remoteObject, offset: offset, count: count, ); case RuntimeObjectKind.record: return await _recordInstanceFor( metaData, remoteObject, offset: offset, count: count, ); case RuntimeObjectKind.object: case RuntimeObjectKind.nativeError: case RuntimeObjectKind.nativeObject: return await _plainInstanceFor( metaData, remoteObject, offset: offset, count: count, ); } } /// 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 _fieldFor(Property property, ClassRef classRef) async { final instance = await _instanceRefForRemote(property.value); // TODO(annagrin): convert JS name to dart and fill missing information. //https://github.com/dart-lang/sdk/issues/46723 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, isConst: false, isFinal: false, isStatic: false, id: createId(), ), value: instance, ); } /// Create a plain instance of [classRef] from [remoteObject] and the JS /// properties [properties]. Future _plainInstanceFor( ClassMetaData metaData, RemoteObject remoteObject, { int? offset, int? count, }) async { final objectId = remoteObject.objectId; if (objectId == null) return null; final fields = await _getInstanceFields( metaData, remoteObject, offset: offset, count: count, ); final result = Instance( kind: InstanceKind.kPlainInstance, id: objectId, identityHashCode: remoteObject.objectId.hashCode, classRef: metaData.classRef, fields: fields, ); return result; } Future> _getInstanceFields( ClassMetaData metaData, RemoteObject remoteObject, { int? offset, int? count, }) async { final objectId = remoteObject.objectId; if (objectId == null) throw StateError('Object id is null for instance'); final properties = await inspector.getProperties( objectId, offset: offset, count: count, length: metaData.kind != InstanceKind.kPlainInstance ? metaData.length : null, ); final dartProperties = await _dartFieldsFor(properties, remoteObject); final boundFields = await Future.wait( dartProperties.map>( (p) => _fieldFor(p, metaData.classRef), ), ); return boundFields .where((bv) => inspector.isDisplayableObject(bv.value)) .toList() ..sort(_compareBoundFields); } 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. /// /// Returns a range of [count] associations, if available, starting from /// the [offset]. /// /// If [offset] is `null`, assumes 0 offset. /// If [count] is `null`, return all fields starting from the offset. Future> _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 = globalToolConfiguration.loadStrategy.dartRuntimeDebugger .getMapElementsJsExpression(); 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 = []; 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]. /// /// Returns an instance containing [count] associations, if available, /// starting from the [offset]. /// /// If [offset] is `null`, assumes 0 offset. /// If [count] is `null`, return all fields starting from the offset. /// [length] is the expected length of the whole object, read from /// the [ClassMetaData]. Future _mapInstanceFor( ClassMetaData metaData, 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: offset, count: count, ); final rangeCount = _calculateRangeCount( count: count, elementCount: associations.length, length: metaData.length, ); return Instance( identityHashCode: remoteObject.objectId.hashCode, kind: InstanceKind.kMap, id: objectId, classRef: metaData.classRef, length: metaData.length, offset: offset, count: rangeCount, associations: associations, ); } /// Create a List instance of [classRef] from [remoteObject]. /// /// Returns an instance containing [count] elements, if available, /// starting from the [offset]. /// /// If [offset] is `null`, assumes 0 offset. /// If [count] is `null`, return all fields starting from the offset. /// [length] is the expected length of the whole object, read from /// the [ClassMetaData]. Future _listInstanceFor( ClassMetaData metaData, RemoteObject remoteObject, { int? offset, int? count, }) async { final objectId = remoteObject.objectId; if (objectId == null) return null; final elements = await _listElements( remoteObject, offset: offset, count: count, length: metaData.length, ); final rangeCount = _calculateRangeCount( count: count, elementCount: elements.length, length: metaData.length, ); return Instance( identityHashCode: remoteObject.objectId.hashCode, kind: InstanceKind.kList, id: objectId, classRef: metaData.classRef, length: metaData.length, elements: elements, offset: offset, count: rangeCount, ); } /// The elements for a Dart List. /// /// Returns a range of [count] elements, if available, starting from /// the [offset]. /// /// If [offset] is `null`, assumes 0 offset. /// If [count] is `null`, return all fields starting from the offset. /// [length] is the expected length of the whole object, read from /// the [ClassMetaData]. Future> _listElements( RemoteObject list, { int? offset, int? count, int? length, }) async { final properties = await inspector.getProperties( list.objectId!, offset: offset, count: count, length: length, ); // Filter out all non-indexed properties final elements = _indexedListProperties(properties); final rangeCount = _calculateRangeCount( count: count, elementCount: elements.length, length: length, ); final range = elements.sublist(0, rangeCount); return Future.wait( range.map((element) => _instanceRefForRemote(element.value)), ); } /// Return elements of the list from [properties]. /// /// Ignore any non-elements like 'length', 'fixed$length', etc. static List _indexedListProperties(List properties) => properties .where((p) => p.name != null && int.tryParse(p.name!) != null) .toList(); /// The field names for a Dart record shape. /// /// Returns a range of [count] fields, if available, starting from /// the [offset]. /// /// If [offset] is `null`, assumes 0 offset. /// If [count] is `null`, return all fields starting from the offset. /// The [shape] object describes the shape using `positionalCount` /// and `named` fields. /// /// Returns list of field names for the record shape. Future> _recordShapeFields( RemoteObject shape, { int? offset, int? count, }) async { final positionalCountObject = await inspector.loadField( shape, 'positionalCount', ); if (positionalCountObject.value is! int) { _logger.warning( 'Unexpected positional count from record: $positionalCountObject', ); return []; } final namedObject = await inspector.loadField(shape, 'named'); final positionalCount = positionalCountObject.value as int; final positionalOffset = offset ?? 0; final positionalAvailable = _remainingCount( positionalOffset, positionalCount, ); final positionalRangeCount = min( positionalAvailable, count ?? positionalAvailable, ); final positionalElements = [ for ( var i = positionalOffset + 1; i <= positionalOffset + positionalRangeCount; i++ ) i, ]; // Collect named fields in the requested range. // Account for already collected positional fields. final namedRangeOffset = offset == null ? null : _remainingCount(positionalCount, offset); final namedRangeCount = count == null ? null : _remainingCount(positionalRangeCount, count); final namedInstance = await instanceFor( namedObject, offset: namedRangeOffset, count: namedRangeCount, ); final namedElements = namedInstance?.elements?.map((e) => e.valueAsString) ?? []; return [...positionalElements, ...namedElements]; } /// The fields for a Dart Record. /// /// Returns a range of [count] fields, if available, starting from /// the [offset]. /// /// If [offset] is `null`, assumes 0 offset. /// If [count] is `null`, return all fields starting from the offset. Future> _recordFields( RemoteObject record, { 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 = globalToolConfiguration.loadStrategy.dartRuntimeDebugger .getRecordFieldsJsExpression(); final result = await inspector.jsCallFunctionOn(record, expression, []); final fieldNameElements = await _recordShapeFields( result, offset: offset, count: count, ); final valuesObject = await inspector.loadField(result, 'values'); final valuesInstance = await instanceFor( valuesObject, offset: offset, count: count, ); final valueElements = valuesInstance?.elements ?? []; return _elementsToBoundFields(fieldNameElements, valueElements); } /// Create a list of `BoundField`s from field [names] and [values]. List _elementsToBoundFields( List names, List values, ) { if (names.length != values.length) { _logger.warning('Bound field names and values are not the same length.'); return []; } final boundFields = []; Map.fromIterables(names, values).forEach((name, value) { boundFields.add(BoundField(name: name, value: value)); }); return boundFields; } static int _remainingCount(int collected, int requested) { return requested < collected ? 0 : requested - collected; } /// Create a Record instance with class [classRef] from [remoteObject]. /// /// Returns an instance containing [count] fields, if available, /// starting from the [offset]. /// /// If [offset] is `null`, assumes 0 offset. /// If [count] is `null`, return all fields starting from the offset. /// [length] is the expected length of the whole object, read from /// the [ClassMetaData]. Future _recordInstanceFor( ClassMetaData metaData, 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: offset, count: count, ); final rangeCount = _calculateRangeCount( count: count, elementCount: fields.length, length: metaData.length, ); return Instance( identityHashCode: remoteObject.objectId.hashCode, kind: InstanceKind.kRecord, id: objectId, classRef: metaData.classRef, length: metaData.length, offset: offset, count: rangeCount, fields: fields, ); } /// Create a RecordType instance with class [classRef] from [remoteObject]. /// /// Returns an instance containing [count] fields, if available, /// starting from the [offset]. /// /// If [offset] is `null`, assumes 0 offset. /// If [count] is `null`, return all fields starting from the offset. /// [length] is the expected length of the whole object, read from /// the [ClassMetaData]. Future _recordTypeInstanceFor( ClassMetaData metaData, 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 _recordTypeFields( remoteObject, offset: offset, count: count, ); final rangeCount = _calculateRangeCount( count: count, elementCount: fields.length, length: metaData.length, ); return Instance( identityHashCode: remoteObject.objectId.hashCode, kind: InstanceKind.kRecordType, id: objectId, classRef: metaData.classRef, length: metaData.length, offset: offset, count: rangeCount, fields: fields, ); } /// The field types for a Dart RecordType. /// /// Returns a range of [count] field types, if available, starting from /// the [offset]. /// /// If [offset] is `null`, assumes 0 offset. /// If [count] is `null`, return all field types starting from the offset. Future> _recordTypeFields( RemoteObject record, { int? offset, int? count, }) async { // We do this in in awkward way because we want the names and types, 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 = globalToolConfiguration.loadStrategy.dartRuntimeDebugger .getRecordTypeFieldsJsExpression(); final result = await inspector.jsCallFunctionOn(record, expression, []); final fieldNameElements = await _recordShapeFields( result, offset: offset, count: count, ); final typesObject = await inspector.loadField(result, 'types'); final typesInstance = await instanceFor( typesObject, offset: offset, count: count, ); final typeElements = typesInstance?.elements ?? []; return _elementsToBoundFields(fieldNameElements, typeElements); } Future _setInstanceFor( ClassMetaData metaData, RemoteObject remoteObject, { int? offset, int? count, }) async { final length = metaData.length; final objectId = remoteObject.objectId; if (objectId == null) return null; final expression = globalToolConfiguration.loadStrategy.dartRuntimeDebugger .getSetElementsJsExpression(); final result = await inspector.jsCallFunctionOn( remoteObject, expression, [], ); final entriesObject = await inspector.loadField(result, 'entries'); final entriesInstance = await instanceFor( entriesObject, offset: offset, count: count, ); final elements = entriesInstance?.elements ?? []; final setInstance = Instance( identityHashCode: remoteObject.objectId.hashCode, kind: InstanceKind.kSet, id: objectId, classRef: metaData.classRef, length: length, elements: elements, ); if (offset != null && offset > 0) { setInstance.offset = offset; } if (length != null && elements.length < length) { setInstance.count = elements.length; } return setInstance; } /// Create Type instance with class [classRef] from [remoteObject]. /// /// Collect information from the internal [remoteObject] and present /// it as an instance of [Type] class. /// /// Returns an instance containing `hashCode` and `runtimeType` fields. /// [length] is the expected length of the whole object, read from /// the [ClassMetaData]. Future _plainTypeInstanceFor( ClassMetaData metaData, RemoteObject remoteObject, { int? offset, int? count, }) async { final objectId = remoteObject.objectId; if (objectId == null) return null; final fields = await _getInstanceFields( metaData, remoteObject, offset: offset, count: count, ); return Instance( identityHashCode: objectId.hashCode, kind: InstanceKind.kType, id: objectId, classRef: metaData.classRef, name: metaData.typeName, length: metaData.length, offset: offset, count: count, fields: fields, ); } /// Return the available count of elements in the requested range. /// Return `null` if the range includes the whole object. /// [count] is the range length requested by the `getObject` call. /// [elementCount] is the number of elements in the runtime object. /// [length] is the expected length of the whole object, read from /// the [ClassMetaData]. static int? _calculateRangeCount({ int? count, int? elementCount, int? length, }) { if (count == null) return null; if (elementCount == null) return null; if (length == elementCount) return null; return min(count, elementCount); } /// 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> _dartFieldsFor( List 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 = globalToolConfiguration .loadStrategy .dartRuntimeDebugger .getObjectFieldNamesJsExpression(); final result = await inspector.jsCallFunctionOn( remoteObject, fieldNameExpression, [], returnByValue: true, ); final names = List.from(result.value as List); // 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 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 _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 metadataHelper.metaDataFor(remoteObject); if (metaData == null) return null; return InstanceRef( kind: metaData.kind, id: objectId, identityHashCode: objectId.hashCode, classRef: metaData.classRef, length: metaData.length, name: metaData.typeName, ); case 'function': final objectId = remoteObject.objectId; if (objectId == null) { return _primitiveInstanceRef(InstanceKind.kNull, remoteObject); } final functionMetaData = await FunctionMetaData.metaDataFor( inspector.remoteDebugger, remoteObject, ); // TODO(annagrin) - fill missing information. // https://github.com/dart-lang/sdk/issues/46723 return InstanceRef( kind: InstanceKind.kClosure, id: objectId, identityHashCode: objectId.hashCode, classRef: classRefForClosure, closureFunction: FuncRef( name: functionMetaData.name, id: createId(), owner: classRefForUnknown, isConst: false, isStatic: false, implicit: false, isGetter: false, isSetter: false, ), closureContext: ContextRef(length: 0, id: createId()), ); default: // Return null for an unsupported type. This is likely a JS construct. return null; } } }