diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index 65c501916..19956d899 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -3,6 +3,7 @@ - Do not show async frame errors on evaluation. - [#2073](https://github.com/dart-lang/webdev/pull/2073) - Refactor code for presenting record instances. - [#2074](https://github.com/dart-lang/webdev/pull/2074) - Display record types concisely. - [#2070](https://github.com/dart-lang/webdev/pull/2070) +- Display type objects concisely. - [#2103](https://github.com/dart-lang/webdev/pull/2103) ## 19.0.0 diff --git a/dwds/lib/src/debugging/classes.dart b/dwds/lib/src/debugging/classes.dart index f5a529a93..bc425141a 100644 --- a/dwds/lib/src/debugging/classes.dart +++ b/dwds/lib/src/debugging/classes.dart @@ -199,8 +199,11 @@ class ClassHelper extends Domain { fieldDescriptors.forEach((name, descriptor) { final classMetaData = ClassMetaData( jsName: descriptor['classRefName'], - libraryId: descriptor['classRefLibraryId'], - dartName: descriptor['classRefDartName'], + runtimeKind: RuntimeObjectKind.type, + classRef: classRefFor( + descriptor['classRefLibraryId'], + descriptor['classRefDartName'], + ), ); fieldRefs.add( FieldRef( @@ -209,8 +212,7 @@ class ClassHelper extends Domain { declaredType: InstanceRef( identityHashCode: createId().hashCode, id: createId(), - kind: InstanceKind.kType, - // TODO(elliette): Is this the same as classRef? + kind: classMetaData.kind, classRef: classMetaData.classRef, ), isConst: descriptor['isConst'] as bool, diff --git a/dwds/lib/src/debugging/dart_scope.dart b/dwds/lib/src/debugging/dart_scope.dart index efd53aba6..341af8904 100644 --- a/dwds/lib/src/debugging/dart_scope.dart +++ b/dwds/lib/src/debugging/dart_scope.dart @@ -2,7 +2,7 @@ // 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 'package:dwds/src/debugging/debugger.dart'; +import 'package:dwds/src/utilities/domain.dart'; import 'package:dwds/src/utilities/objects.dart'; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; @@ -25,7 +25,7 @@ final previousDdcTemporaryVariableRegExp = /// /// See chromedevtools.github.io/devtools-protocol/tot/Debugger#type-CallFrame. Future> visibleProperties({ - required Debugger debugger, + required AppInspectorInterface inspector, required WipCallFrame frame, }) async { final allProperties = []; @@ -48,7 +48,7 @@ Future> visibleProperties({ for (var scope in filterScopes(frame).reversed) { final objectId = scope.object.objectId; if (objectId != null) { - final properties = await debugger.getProperties(objectId); + final properties = await inspector.getProperties(objectId); allProperties.addAll(properties); } } diff --git a/dwds/lib/src/debugging/debugger.dart b/dwds/lib/src/debugging/debugger.dart index fba98ee1e..d85bb49ad 100644 --- a/dwds/lib/src/debugging/debugger.dart +++ b/dwds/lib/src/debugging/debugger.dart @@ -3,17 +3,14 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; -import 'dart:math' as math; import 'package:dwds/src/debugging/dart_scope.dart'; import 'package:dwds/src/debugging/frame_computer.dart'; import 'package:dwds/src/debugging/location.dart'; -import 'package:dwds/src/debugging/metadata/class.dart'; import 'package:dwds/src/debugging/remote_debugger.dart'; import 'package:dwds/src/debugging/skip_list.dart'; import 'package:dwds/src/loaders/strategy.dart'; import 'package:dwds/src/services/chrome_debug_exception.dart'; -import 'package:dwds/src/utilities/conversions.dart'; import 'package:dwds/src/utilities/dart_uri.dart'; import 'package:dwds/src/utilities/domain.dart'; import 'package:dwds/src/utilities/objects.dart' show Property; @@ -394,7 +391,8 @@ class Debugger extends Domain { /// The variables visible in a frame in Dart protocol [BoundVariable] form. Future> variablesFor(WipCallFrame frame) async { // TODO(alanknight): Can these be moved to dart_scope.dart? - final properties = await visibleProperties(debugger: this, frame: frame); + final properties = + await visibleProperties(inspector: inspector, frame: frame); final boundVariables = await Future.wait( properties.map(_boundVariable), ); @@ -402,7 +400,7 @@ class Debugger extends Domain { // Filter out variables that do not come from dart code, such as native // JavaScript objects return boundVariables - .where((bv) => isDisplayableObject(bv?.value)) + .where((bv) => inspector.isDisplayableObject(bv?.value)) .toList() .cast(); } @@ -432,84 +430,6 @@ class Debugger extends Domain { return null; } - static bool _isEmptyRange({ - required int length, - int? offset, - int? count, - }) { - if (count == 0) return true; - if (offset == null) return false; - return offset >= length; - } - - static bool _isSubRange({ - int? offset, - int? count, - }) { - if (offset == 0 && count == null) return false; - return offset != null || count != null; - } - - /// Compute the last possible element index in the range of [offset]..end - /// that includes [count] elements, if available. - static int? _calculateRangeEnd({ - int? count, - required int offset, - required int length, - }) => - count == null ? null : math.min(offset + count, length); - - /// Calculate the number of available elements in the range. - static int _calculateRangeCount({ - int? count, - required int offset, - required int length, - }) => - count == null ? length - offset : math.min(count, length - offset); - - /// Find a sub-range of the entries for a Map/List when offset and/or count - /// have been specified on a getObject request. - /// - /// If the object referenced by [id] is not a system List or Map then this - /// will just return a RemoteObject for it and ignore [offset], [count] and - /// [length]. If it is, then [length] should be the number of entries in the - /// List/Map and [offset] and [count] should indicate the desired range. - Future _subRange( - String id, { - required int offset, - int? count, - required int length, - }) async { - // TODO(#809): Sometimes we already know the type of the object, and - // we could take advantage of that to short-circuit. - final receiver = remoteObjectFor(id); - final end = - _calculateRangeEnd(count: count, offset: offset, length: length); - final rangeCount = - _calculateRangeCount(count: count, offset: offset, length: length); - final args = - [offset, rangeCount, end].map(dartIdFor).map(remoteObjectFor).toList(); - // If this is a List, just call sublist. If it's a Map, get the entries, but - // avoid doing a toList on a large map using skip/take to get the section we - // want. To make those alternatives easier in JS, pass both count and end. - final expression = ''' - function (offset, count, end) { - const sdk = ${globalLoadStrategy.loadModuleSnippet}("dart_sdk"); - if (sdk.core.Map.is(this)) { - const entries = sdk.dart.dload(this, "entries"); - const skipped = sdk.dart.dsend(entries, "skip", [offset]) - const taken = sdk.dart.dsend(skipped, "take", [count]); - return sdk.dart.dsend(taken, "toList", []); - } else if (sdk.core.List.is(this)) { - return sdk.dart.dsendRepl(this, "sublist", [offset, end]); - } else { - return this; - } - } - '''; - return await inspector.jsCallFunctionOn(receiver, expression, args); - } - // TODO(elliette): https://github.com/dart-lang/webdev/issues/1501 Re-enable // after checking with Chrome team if there is a way to check if the Chrome // DevTools is showing an overlay. Both cannot be shown at the same time: @@ -533,48 +453,6 @@ class Debugger extends Domain { // _pausedOverlayVisible = false; // } - /// Calls the Chrome Runtime.getProperties API for the object with [objectId]. - /// - /// Note that the property names are JS names, e.g. - /// Symbol(DartClass.actualName) and will need to be converted. For a system - /// List or Map, [offset] and/or [count] can be provided to indicate a desired - /// range of entries. They will be ignored if there is no [length]. - Future> getProperties( - String objectId, { - int? offset, - int? count, - int? length, - }) async { - String rangeId = objectId; - // Ignore offset/count if there is no length: - if (length != null) { - if (_isEmptyRange(offset: offset, count: count, length: length)) { - return []; - } - if (_isSubRange(offset: offset, count: count)) { - final range = await _subRange( - objectId, - offset: offset ?? 0, - count: count, - length: length, - ); - rangeId = range.objectId ?? rangeId; - } - } - final jsProperties = await sendCommandAndValidateResult( - _remoteDebugger, - method: 'Runtime.getProperties', - resultField: 'result', - params: { - 'objectId': rangeId, - 'ownProperties': true, - }, - ); - return jsProperties - .map((each) => Property(each as Map)) - .toList(); - } - /// Returns a Dart [Frame] for a JS [frame]. Future calculateDartFrameFor( WipCallFrame frame, @@ -658,7 +536,7 @@ class Debugger extends Domain { if (map['type'] == 'object') { final obj = RemoteObject(map); exception = await inspector.instanceRefFor(obj); - if (exception != null && isNativeJsError(exception)) { + if (exception != null && inspector.isNativeJsError(exception)) { if (obj.description != null) { // Create a string exception object. final description = @@ -845,23 +723,6 @@ Future sendCommandAndValidateResult( return result; } -/// Returns true for objects we display for the user. -bool isDisplayableObject(Object? object) => - object is Sentinel || - object is InstanceRef && - !isNativeJsObject(object) && - !isNativeJsError(object); - -/// Returns true for non-dart JavaScript objects. -bool isNativeJsObject(InstanceRef instanceRef) { - return isNativeJsObjectRef(instanceRef.classRef); -} - -/// Returns true of JavaScript exceptions. -bool isNativeJsError(InstanceRef instanceRef) { - return instanceRef.classRef == classRefForNativeJsError; -} - /// Returns the Dart line number for the provided breakpoint. int _lineNumberFor(Breakpoint breakpoint) => int.parse(breakpoint.id!.split('#').last.split(':').first); diff --git a/dwds/lib/src/debugging/inspector.dart b/dwds/lib/src/debugging/inspector.dart index ba9178dce..7d0c75027 100644 --- a/dwds/lib/src/debugging/inspector.dart +++ b/dwds/lib/src/debugging/inspector.dart @@ -2,6 +2,8 @@ // 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' as math; + import 'package:async/async.dart'; import 'package:collection/collection.dart'; import 'package:dwds/src/connections/app_connection.dart'; @@ -17,6 +19,7 @@ import 'package:dwds/src/readers/asset_reader.dart'; import 'package:dwds/src/utilities/conversions.dart'; import 'package:dwds/src/utilities/dart_uri.dart'; import 'package:dwds/src/utilities/domain.dart'; +import 'package:dwds/src/utilities/objects.dart'; import 'package:dwds/src/utilities/server.dart'; import 'package:dwds/src/utilities/shared.dart'; import 'package:logging/logging.dart'; @@ -96,17 +99,13 @@ class AppInspector implements AppInspectorInterface { this._locations, this._root, this._executionContext, - ) : _isolateRef = _toIsolateRef(_isolate); - - Future initialize( - LibraryHelper libraryHelper, - ClassHelper classHelper, - InstanceHelper instanceHelper, - ) async { - _libraryHelper = libraryHelper; - _classHelper = classHelper; - _instanceHelper = instanceHelper; + ) : _isolateRef = _toIsolateRef(_isolate) { + _libraryHelper = LibraryHelper(this); + _classHelper = ClassHelper(this); + _instanceHelper = InstanceHelper(this); + } + Future initialize() async { final libraries = await _libraryHelper.libraryRefs; isolate.rootLib = await _libraryHelper.rootLib; isolate.libraries?.addAll(libraries); @@ -176,16 +175,7 @@ class AppInspector implements AppInspectorInterface { ); debugger.updateInspector(inspector); - - final libraryHelper = LibraryHelper(inspector); - final classHelper = ClassHelper(inspector); - final instanceHelper = InstanceHelper(inspector, debugger); - - await inspector.initialize( - libraryHelper, - classHelper, - instanceHelper, - ); + await inspector.initialize(); return inspector; } @@ -568,6 +558,147 @@ class AppInspector implements AppInspectorInterface { return ScriptList(scripts: await scriptRefs); } + /// Calls the Chrome Runtime.getProperties API for the object with [objectId]. + /// + /// Note that the property names are JS names, e.g. + /// Symbol(DartClass.actualName) and will need to be converted. For a system + /// List or Map, [offset] and/or [count] can be provided to indicate a desired + /// range of entries. They will be ignored if there is no [length]. + @override + Future> getProperties( + String objectId, { + int? offset, + int? count, + int? length, + }) async { + String rangeId = objectId; + // Ignore offset/count if there is no length: + if (length != null) { + if (_isEmptyRange(offset: offset, count: count, length: length)) { + return []; + } + if (_isSubRange(offset: offset, count: count)) { + final range = await _subRange( + objectId, + offset: offset ?? 0, + count: count, + length: length, + ); + rangeId = range.objectId ?? rangeId; + } + } + final jsProperties = await sendCommandAndValidateResult( + _remoteDebugger, + method: 'Runtime.getProperties', + resultField: 'result', + params: { + 'objectId': rangeId, + 'ownProperties': true, + }, + ); + return jsProperties + .map((each) => Property(each as Map)) + .toList(); + } + + /// Compute the last possible element index in the range of [offset]..end + /// that includes [count] elements, if available. + static int? _calculateRangeEnd({ + int? count, + required int offset, + required int length, + }) => + count == null ? null : math.min(offset + count, length); + + /// Calculate the number of available elements in the range. + static int _calculateRangeCount({ + int? count, + required int offset, + required int length, + }) => + count == null ? length - offset : math.min(count, length - offset); + + /// Find a sub-range of the entries for a Map/List when offset and/or count + /// have been specified on a getObject request. + /// + /// If the object referenced by [id] is not a system List or Map then this + /// will just return a RemoteObject for it and ignore [offset], [count] and + /// [length]. If it is, then [length] should be the number of entries in the + /// List/Map and [offset] and [count] should indicate the desired range. + Future _subRange( + String id, { + required int offset, + required int length, + int? count, + }) async { + // TODO(#809): Sometimes we already know the type of the object, and + // we could take advantage of that to short-circuit. + final receiver = remoteObjectFor(id); + final end = + _calculateRangeEnd(count: count, offset: offset, length: length); + final rangeCount = + _calculateRangeCount(count: count, offset: offset, length: length); + final args = + [offset, rangeCount, end].map(dartIdFor).map(remoteObjectFor).toList(); + // If this is a List, just call sublist. If it's a Map, get the entries, but + // avoid doing a toList on a large map using skip/take to get the section we + // want. To make those alternatives easier in JS, pass both count and end. + final expression = ''' + function (offset, count, end) { + const sdk = ${globalLoadStrategy.loadModuleSnippet}("dart_sdk"); + if (sdk.core.Map.is(this)) { + const entries = sdk.dart.dload(this, "entries"); + const skipped = sdk.dart.dsend(entries, "skip", [offset]) + const taken = sdk.dart.dsend(skipped, "take", [count]); + return sdk.dart.dsend(taken, "toList", []); + } else if (sdk.core.List.is(this)) { + return sdk.dart.dsendRepl(this, "sublist", [offset, end]); + } else { + return this; + } + } + '''; + return await jsCallFunctionOn(receiver, expression, args); + } + + static bool _isEmptyRange({ + required int length, + int? offset, + int? count, + }) { + if (count == 0) return true; + if (offset == null) return false; + return offset >= length; + } + + static bool _isSubRange({ + int? offset, + int? count, + }) { + if (offset == 0 && count == null) return false; + return offset != null || count != null; + } + + /// Returns true for objects we display for the user. + @override + bool isDisplayableObject(Object? object) => + object is Sentinel || + object is InstanceRef && + !isNativeJsObject(object) && + !isNativeJsError(object); + + /// Returns true for non-dart JavaScript objects. + bool isNativeJsObject(InstanceRef instanceRef) { + return _instanceHelper.metadataHelper + .isNativeJsObject(instanceRef.classRef); + } + + /// Returns true for JavaScript exceptions. + @override + bool isNativeJsError(InstanceRef instanceRef) { + return _instanceHelper.metadataHelper.isNativeJsError(instanceRef.classRef); + } + /// Request and cache s for all the scripts in the application. /// /// This populates [_scriptRefsById], [_scriptIdToLibraryId], diff --git a/dwds/lib/src/debugging/instance.dart b/dwds/lib/src/debugging/instance.dart index 8f43172b5..23c95b2ba 100644 --- a/dwds/lib/src/debugging/instance.dart +++ b/dwds/lib/src/debugging/instance.dart @@ -4,7 +4,7 @@ import 'dart:math'; -import 'package:dwds/src/debugging/debugger.dart'; +import 'package:dwds/src/debugging/inspector.dart'; import 'package:dwds/src/debugging/metadata/class.dart'; import 'package:dwds/src/debugging/metadata/function.dart'; import 'package:dwds/src/loaders/strategy.dart'; @@ -19,12 +19,13 @@ import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; /// Contains a set of methods for getting [Instance]s and [InstanceRef]s. class InstanceHelper extends Domain { final _logger = Logger('InstanceHelper'); + final ClassMetaDataHelper metadataHelper; - InstanceHelper(AppInspectorInterface appInspector, this.debugger) { + InstanceHelper(AppInspector appInspector) + : metadataHelper = ClassMetaDataHelper(appInspector) { inspector = appInspector; } - final Debugger debugger; static final InstanceRef kNullInstanceRef = _primitiveInstanceRef(InstanceKind.kNull, null); @@ -39,19 +40,21 @@ class InstanceHelper extends Domain { kind: kind, classRef: classRef, id: dartIdFor(remoteObject?.value), - )..valueAsString = '${remoteObject?.value}'; + valueAsString: '${remoteObject?.value}', + ); } /// Creates an [Instance] for a primitive [RemoteObject]. - Instance? _primitiveInstance(String kind, RemoteObject? remote) { - final objectId = remote?.objectId; + 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 = '${remote?.value}'; + valueAsString: '${remoteObject?.value}', + ); } Instance? _stringInstanceFor( @@ -77,12 +80,12 @@ class InstanceHelper extends Domain { kind: InstanceKind.kString, classRef: classRefForString, id: createId(), - ) - ..valueAsString = preview - ..valueAsStringIsTruncated = truncated - ..length = fullString.length - ..count = (truncated ? preview.length : null) - ..offset = (truncated ? offset : null); + valueAsString: preview, + valueAsStringIsTruncated: truncated, + length: fullString.length, + count: (truncated ? preview.length : null), + offset: (truncated ? offset : null), + ); } Instance? _closureInstanceFor(RemoteObject remoteObject) { @@ -114,79 +117,66 @@ class InstanceHelper extends Domain { final objectId = remoteObject?.objectId; if (remoteObject == null || objectId == null) return null; - // TODO: This is checking the JS object ID for the dart pattern we use for - // VM objects, which seems wrong (and, we catch 'string' types above). - if (isStringId(objectId)) { - return _stringInstanceFor(remoteObject, offset, count); - } - - final metaData = await ClassMetaData.metaDataFor( - remoteObject, - inspector, - ); + final metaData = await metadataHelper.metaDataFor(remoteObject); final classRef = metaData?.classRef; if (metaData == null || classRef == null) return null; - if (metaData.isFunction) { - return _closureInstanceFor(remoteObject); - } - if (metaData.isSystemList) { - return await _listInstanceFor( - classRef, - remoteObject, - offset: offset, - count: count, - length: metaData.length, - ); - } else if (metaData.isSystemMap) { - return await _mapInstanceFor( - classRef, - remoteObject, - offset: offset, - count: count, - length: metaData.length, - ); - } else if (metaData.isRecord) { - return await _recordInstanceFor( - classRef, - remoteObject, - offset: offset, - count: count, - length: metaData.length, - ); - } else if (metaData.isRecordType) { - return await _recordTypeInstanceFor( - classRef, - remoteObject, - offset: offset, - count: count, - length: metaData.length, - ); - } else if (metaData.isSet) { - return await _setInstanceFor( - classRef, - remoteObject, - offset: offset, - count: count, - length: metaData.length, - ); - } else if (metaData.isNativeError) { - return await _plainInstanceFor( - classRefForNativeJsError, - remoteObject, - offset: offset, - count: count, - length: metaData.length, - ); - } else { - return await _plainInstanceFor( - classRef, - remoteObject, - offset: offset, - count: count, - length: metaData.length, - ); + 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: + default: + return await _plainInstanceFor( + metaData, + remoteObject, + offset: offset, + count: count, + ); } } @@ -239,35 +229,36 @@ class InstanceHelper extends Domain { /// Create a plain instance of [classRef] from [remoteObject] and the JS /// properties [properties]. Future _plainInstanceFor( - ClassRef classRef, + ClassMetaData metaData, RemoteObject remoteObject, { int? offset, int? count, - int? length, }) async { final objectId = remoteObject.objectId; if (objectId == null) return null; - final properties = await debugger.getProperties( + final properties = await inspector.getProperties( objectId, offset: offset, count: count, - length: length, + length: metaData.length, ); final dartProperties = await _dartFieldsFor(properties, remoteObject); var boundFields = await Future.wait( - dartProperties.map>((p) => _fieldFor(p, classRef)), + dartProperties + .map>((p) => _fieldFor(p, metaData.classRef)), ); boundFields = boundFields - .where((bv) => isDisplayableObject(bv.value)) + .where((bv) => inspector.isDisplayableObject(bv.value)) .toList() ..sort(_compareBoundFields); final result = Instance( kind: InstanceKind.kPlainInstance, id: objectId, identityHashCode: remoteObject.objectId.hashCode, - classRef: classRef, - )..fields = boundFields; + classRef: metaData.classRef, + fields: boundFields, + ); return result; } @@ -338,11 +329,10 @@ class InstanceHelper extends Domain { /// [length] is the expected length of the whole object, read from /// the [ClassMetaData]. Future _mapInstanceFor( - ClassRef classRef, + ClassMetaData metaData, RemoteObject remoteObject, { int? offset, int? count, - int? length, }) async { final objectId = remoteObject.objectId; if (objectId == null) return null; @@ -353,18 +343,18 @@ class InstanceHelper extends Domain { final rangeCount = _calculateRangeCount( count: count, elementCount: associations.length, - length: length, + length: metaData.length, ); return Instance( identityHashCode: remoteObject.objectId.hashCode, kind: InstanceKind.kMap, id: objectId, - classRef: classRef, - ) - ..length = length - ..offset = offset - ..count = rangeCount - ..associations = associations; + classRef: metaData.classRef, + length: metaData.length, + offset: offset, + count: rangeCount, + associations: associations, + ); } /// Create a List instance of [classRef] from [remoteObject]. @@ -377,11 +367,10 @@ class InstanceHelper extends Domain { /// [length] is the expected length of the whole object, read from /// the [ClassMetaData]. Future _listInstanceFor( - ClassRef classRef, + ClassMetaData metaData, RemoteObject remoteObject, { int? offset, int? count, - int? length, }) async { final objectId = remoteObject.objectId; if (objectId == null) return null; @@ -390,23 +379,23 @@ class InstanceHelper extends Domain { remoteObject, offset: offset, count: count, - length: length, + length: metaData.length, ); final rangeCount = _calculateRangeCount( count: count, elementCount: elements.length, - length: length, + length: metaData.length, ); return Instance( identityHashCode: remoteObject.objectId.hashCode, kind: InstanceKind.kList, id: objectId, - classRef: classRef, - ) - ..length = length - ..elements = elements - ..offset = offset - ..count = rangeCount; + classRef: metaData.classRef, + length: metaData.length, + elements: elements, + offset: offset, + count: rangeCount, + ); } /// The elements for a Dart List. @@ -424,7 +413,7 @@ class InstanceHelper extends Domain { int? count, int? length, }) async { - final properties = await debugger.getProperties( + final properties = await inspector.getProperties( list.objectId!, offset: offset, count: count, @@ -588,11 +577,10 @@ class InstanceHelper extends Domain { /// [length] is the expected length of the whole object, read from /// the [ClassMetaData]. Future _recordInstanceFor( - ClassRef classRef, + ClassMetaData metaData, RemoteObject remoteObject, { int? offset, int? count, - int? length, }) async { final objectId = remoteObject.objectId; if (objectId == null) return null; @@ -602,18 +590,18 @@ class InstanceHelper extends Domain { final rangeCount = _calculateRangeCount( count: count, elementCount: fields.length, - length: length, + length: metaData.length, ); return Instance( identityHashCode: remoteObject.objectId.hashCode, kind: InstanceKind.kRecord, id: objectId, - classRef: classRef, - ) - ..length = length - ..offset = offset - ..count = rangeCount - ..fields = fields; + classRef: metaData.classRef, + length: metaData.length, + offset: offset, + count: rangeCount, + fields: fields, + ); } /// Create a RecordType instance with class [classRef] from [remoteObject]. @@ -626,32 +614,35 @@ class InstanceHelper extends Domain { /// [length] is the expected length of the whole object, read from /// the [ClassMetaData]. Future _recordTypeInstanceFor( - ClassRef classRef, + ClassMetaData metaData, RemoteObject remoteObject, { int? offset, int? count, - int? length, }) 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 fields = await _recordTypeFields( + remoteObject, + offset: offset, + count: count, + ); final rangeCount = _calculateRangeCount( count: count, elementCount: fields.length, - length: length, + length: metaData.length, ); return Instance( identityHashCode: remoteObject.objectId.hashCode, kind: InstanceKind.kRecordType, id: objectId, - classRef: classRef, - ) - ..length = length - ..offset = offset - ..count = rangeCount - ..fields = fields; + classRef: metaData.classRef, + length: metaData.length, + offset: offset, + count: rangeCount, + fields: fields, + ); } /// The field types for a Dart RecordType. @@ -672,11 +663,12 @@ class InstanceHelper extends Domain { final expression = ''' function() { var sdkUtils = ${globalLoadStrategy.loadModuleSnippet}('dart_sdk').dart; - var shape = sdkUtils.dloadRepl(this, "shape"); + var type = sdkUtils.dloadRepl(this, "_type"); + var shape = sdkUtils.dloadRepl(type, "shape"); var positionalCount = sdkUtils.dloadRepl(shape, "positionals"); var named = sdkUtils.dloadRepl(shape, "named"); named = named == null? null: sdkUtils.dsendRepl(named, "toList", []); - var types = sdkUtils.dloadRepl(this, "types"); + var types = sdkUtils.dloadRepl(type, "types"); types = types.map(t => sdkUtils.wrapType(t)); types = sdkUtils.dsendRepl(types, "toList", []); @@ -700,12 +692,12 @@ class InstanceHelper extends Domain { } Future _setInstanceFor( - ClassRef classRef, + ClassMetaData metaData, RemoteObject remoteObject, { int? offset, int? count, - int? length, }) async { + final length = metaData.length; final objectId = remoteObject.objectId; if (objectId == null) return null; @@ -731,10 +723,11 @@ class InstanceHelper extends Domain { identityHashCode: remoteObject.objectId.hashCode, kind: InstanceKind.kSet, id: objectId, - classRef: classRef, - ) - ..length = length - ..elements = elements; + classRef: metaData.classRef, + length: length, + elements: elements, + ); + if (offset != null && offset > 0) { setInstance.offset = offset; } @@ -745,6 +738,78 @@ class InstanceHelper extends Domain { 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 _typeFields(metaData.classRef, remoteObject); + 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, + ); + } + + /// 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> _typeFields( + ClassRef classRef, + RemoteObject type, + ) async { + // Present the type as an instance of `core.Type` class and + // hide the internal implementation. + final expression = ''' + function() { + var sdkUtils = ${globalLoadStrategy.loadModuleSnippet}('dart_sdk').dart; + var hashCode = sdkUtils.dloadRepl(this, "hashCode"); + var runtimeType = sdkUtils.dloadRepl(this, "runtimeType"); + + return { + hashCode: hashCode, + runtimeType: runtimeType + }; + } + '''; + + final result = await inspector.jsCallFunctionOn(type, expression, []); + final hashCodeObject = await inspector.loadField(result, 'hashCode'); + final runtimeTypeObject = await inspector.loadField(result, 'runtimeType'); + + final properties = [ + Property({'name': 'hashCode', 'value': hashCodeObject}), + Property({'name': 'runtimeType', 'value': runtimeTypeObject}), + ]; + + final boundFields = await Future.wait( + properties.map>((p) => _fieldFor(p, classRef)), + ); + return boundFields; + } + /// 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. @@ -849,9 +914,9 @@ class InstanceHelper extends Domain { id: dartIdFor(remoteObject.value), classRef: classRefForString, kind: InstanceKind.kString, - ) - ..valueAsString = stringValue - ..length = stringValue?.length; + valueAsString: stringValue, + length: stringValue?.length, + ); case 'number': return _primitiveInstanceRef(InstanceKind.kDouble, remoteObject); case 'boolean': @@ -863,82 +928,33 @@ class InstanceHelper extends Domain { if (objectId == null) { return _primitiveInstanceRef(InstanceKind.kNull, remoteObject); } - final metaData = await ClassMetaData.metaDataFor( - remoteObject, - inspector, - ); + final metaData = await metadataHelper.metaDataFor(remoteObject); if (metaData == null) return null; - if (metaData.isSystemList) { - return InstanceRef( - kind: InstanceKind.kList, - id: objectId, - identityHashCode: remoteObject.objectId.hashCode, - classRef: metaData.classRef, - )..length = metaData.length; - } - if (metaData.isSystemMap) { - return InstanceRef( - kind: InstanceKind.kMap, - id: objectId, - identityHashCode: remoteObject.objectId.hashCode, - classRef: metaData.classRef, - )..length = metaData.length; - } - if (metaData.isRecord) { - return InstanceRef( - kind: InstanceKind.kRecord, - id: objectId, - identityHashCode: remoteObject.objectId.hashCode, - classRef: metaData.classRef, - )..length = metaData.length; - } - if (metaData.isRecordType) { - return InstanceRef( - kind: InstanceKind.kRecordType, - id: objectId, - identityHashCode: remoteObject.objectId.hashCode, - classRef: metaData.classRef, - )..length = metaData.length; - } - if (metaData.isSet) { - return InstanceRef( - kind: InstanceKind.kSet, - id: objectId, - identityHashCode: remoteObject.objectId.hashCode, - classRef: metaData.classRef, - )..length = metaData.length; - } - if (metaData.isNativeError) { - return InstanceRef( - kind: InstanceKind.kPlainInstance, - id: objectId, - identityHashCode: remoteObject.objectId.hashCode, - classRef: classRefForNativeJsError, - )..length = metaData.length; - } + return InstanceRef( - kind: InstanceKind.kPlainInstance, + kind: metaData.kind, id: objectId, - identityHashCode: remoteObject.objectId.hashCode, + identityHashCode: objectId.hashCode, classRef: metaData.classRef, + length: metaData.length, + name: metaData.typeName, ); case 'function': - final functionMetaData = await FunctionMetaData.metaDataFor( - inspector.remoteDebugger, - remoteObject, - ); final objectId = remoteObject.objectId; if (objectId == null) { return _primitiveInstanceRef(InstanceKind.kNull, remoteObject); } + final functionMetaData = await FunctionMetaData.metaDataFor( + inspector.remoteDebugger, + remoteObject, + ); return InstanceRef( kind: InstanceKind.kClosure, id: objectId, - identityHashCode: remoteObject.objectId.hashCode, + identityHashCode: objectId.hashCode, classRef: classRefForClosure, - ) // TODO(grouma) - fill this in properly. - ..closureFunction = FuncRef( + closureFunction: FuncRef( name: functionMetaData.name, id: createId(), // TODO(alanknight): The right ClassRef @@ -948,8 +964,9 @@ class InstanceHelper extends Domain { // TODO(annagrin): get information about getters and setters from symbols. // https://github.com/dart-lang/sdk/issues/46723 implicit: false, - ) - ..closureContext = (ContextRef(length: 0, id: createId())); + ), + closureContext: ContextRef(length: 0, id: createId()), + ); default: // Return null for an unsupported type. This is likely a JS construct. return null; diff --git a/dwds/lib/src/debugging/libraries.dart b/dwds/lib/src/debugging/libraries.dart index 5b9b96851..b50eef16d 100644 --- a/dwds/lib/src/debugging/libraries.dart +++ b/dwds/lib/src/debugging/libraries.dart @@ -120,9 +120,12 @@ class LibraryHelper extends Domain { List>.from(jsonValues['classes'] ?? []); for (final classDescriptor in classDescriptors) { final classMetaData = ClassMetaData( - jsName: classDescriptor['name'] as Object?, - libraryId: libraryRef.id, - dartName: classDescriptor['dartName'] as Object?, + jsName: classDescriptor['name'], + runtimeKind: RuntimeObjectKind.type, + classRef: classRefFor( + libraryRef.id, + classDescriptor['dartName'], + ), ); classRefs.add(classMetaData.classRef); } diff --git a/dwds/lib/src/debugging/metadata/class.dart b/dwds/lib/src/debugging/metadata/class.dart index 1d0c861a8..7cde9989d 100644 --- a/dwds/lib/src/debugging/metadata/class.dart +++ b/dwds/lib/src/debugging/metadata/class.dart @@ -10,8 +10,6 @@ import 'package:vm_service/vm_service.dart'; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; const _dartCoreLibrary = 'dart:core'; -const _dartRuntimeLibrary = 'dart:_runtime'; -const _dartInterceptorsLibrary = 'dart:_interceptors'; /// A hard-coded ClassRef for the Closure class. final classRefForClosure = classRefFor(_dartCoreLibrary, InstanceKind.kClosure); @@ -19,48 +17,9 @@ final classRefForClosure = classRefFor(_dartCoreLibrary, InstanceKind.kClosure); /// A hard-coded ClassRef for the String class. final classRefForString = classRefFor(_dartCoreLibrary, InstanceKind.kString); -/// A hard-coded ClassRef for the Record class. -final classRefForRecord = classRefFor(_dartCoreLibrary, InstanceKind.kRecord); - -/// A hard-coded ClassRef for the RecordType class. -final classRefForRecordType = - classRefFor(_dartRuntimeLibrary, InstanceKind.kRecordType); - /// A hard-coded ClassRef for a (non-existent) class called Unknown. final classRefForUnknown = classRefFor(_dartCoreLibrary, 'Unknown'); -/// A hard-coded ClassRef for a JS exception. -/// -/// Exceptions are instances of NativeError and its subtypes. -/// We detect their common base type in class metadata and replace their -/// classRef by hard-coded reference in instances and instance refs. -/// -/// TODO(annagrin): this breaks on name changes for JS types. -/// https://github.com/dart-lang/sdk/issues/51583 -final classRefForNativeJsError = - classRefFor(_dartInterceptorsLibrary, 'NativeError'); - -/// Returns true for non-dart JavaScript classes. -/// -/// TODO(annagrin): this breaks on name changes for JS types. -/// https://github.com/dart-lang/sdk/issues/51583 -bool isNativeJsObjectRef(ClassRef? classRef) { - final className = classRef?.name; - final libraryUri = classRef?.library?.uri; - // Non-dart JS objects are all instances of JavaScriptObject - // and its subtypes with names that end with 'JavaScriptObject'. - return className != null && - libraryUri == _dartInterceptorsLibrary && - className.endsWith('JavaScriptObject'); -} - -/// A hard-coded LibraryRef for a a dart:core library. -final libraryRefForCore = LibraryRef( - id: _dartCoreLibrary, - name: _dartCoreLibrary, - uri: _dartCoreLibrary, -); - /// Returns a [LibraryRef] for the provided library ID and class name. LibraryRef libraryRefFor(String libraryId) => LibraryRef( id: libraryId, @@ -69,17 +28,59 @@ LibraryRef libraryRefFor(String libraryId) => LibraryRef( ); /// Returns a [ClassRef] for the provided library ID and class name. -ClassRef classRefFor(String libraryId, String? name) => ClassRef( - id: classIdFor(libraryId, name), - name: name, - library: libraryRefFor(libraryId), - ); +ClassRef classRefFor(Object? libraryId, Object? dartName) { + final library = libraryId as String? ?? _dartCoreLibrary; + final name = dartName as String?; + return ClassRef( + id: classIdFor(library, name), + name: name, + library: libraryRefFor(library), + ); +} String classIdFor(String libraryId, String? name) => 'classes|$libraryId|$name'; +String classMetaDataIdFor(String library, String? jsName) => '$library:$jsName'; + +/// DDC runtime object kind. +/// +/// Object kinds are determined using DDC runtime API and +/// are used to translate from JavaScript objects to their +/// vm service protocol representation. +enum RuntimeObjectKind { + object, + set, + list, + map, + function, + record, + type, + recordType, + nativeError, + nativeObject; + + // TODO(annagrin): Update when built-in parsing is available. + // We can also implement a faster parser if needed. + // https://github.com/dart-lang/language/issues/2348 + static final parse = values.byName; + + String toInstanceKind() { + return switch (this) { + object || nativeObject || nativeError => InstanceKind.kPlainInstance, + set => InstanceKind.kSet, + list => InstanceKind.kList, + map => InstanceKind.kMap, + function => InstanceKind.kClosure, + record => InstanceKind.kRecord, + type => InstanceKind.kType, + recordType => InstanceKind.kRecordType, + }; + } +} /// Meta data for a remote Dart class in Chrome. class ClassMetaData { - static final _logger = Logger('ClassMetadata'); + /// Runtime object kind. + final RuntimeObjectKind runtimeKind; /// Class id. /// @@ -92,50 +93,40 @@ class ClassMetaData { /// example, 'Number', 'JSArray', 'Object'. final String? jsName; + /// Type name for Type instances. + /// + /// For example, 'int', 'String', 'MyClass', 'List'. + final String? typeName; + /// The length of the object, if applicable. final int? length; /// The dart type name for the object. /// /// For example, 'int', 'List', 'Null' - final String? dartName; + String? get dartName => classRef.name; /// Class ref for the class metadata. final ClassRef classRef; + /// Instance kind for vm service protocol. + String get kind => runtimeKind.toInstanceKind(); + factory ClassMetaData({ Object? jsName, - Object? libraryId, - Object? dartName, + Object? typeName, Object? length, - bool isFunction = false, - bool isRecord = false, - bool isRecordType = false, - bool isNativeError = false, + required RuntimeObjectKind runtimeKind, + required ClassRef classRef, }) { - final jName = jsName as String?; - final dName = dartName as String?; - final library = libraryId as String? ?? _dartCoreLibrary; - final id = '$library:$jName'; - - var classRef = classRefFor(library, dName); - if (isRecord) { - classRef = classRefForRecord; - } - if (isRecordType) { - classRef = classRefForRecordType; - } - + final id = classMetaDataIdFor(classRef.library!.id!, jsName as String?); return ClassMetaData._( id, classRef, - jName, - dName, + jsName, + typeName as String?, int.tryParse('$length'), - isFunction, - isRecord, - isRecordType, - isNativeError, + runtimeKind, ); } @@ -143,77 +134,125 @@ class ClassMetaData { this.id, this.classRef, this.jsName, - this.dartName, + this.typeName, this.length, - this.isFunction, - this.isRecord, - this.isRecordType, - this.isNativeError, + this.runtimeKind, ); +} + +/// Metadata helper for objects and class refs. +/// +/// Allows to get runtime metadata from DDC runtime +/// and provides functionality to detect some of the +/// runtime kinds of objects. +class ClassMetaDataHelper { + static final _logger = Logger('ClassMetadata'); + + final AppInspectorInterface _inspector; + + /// Runtime object kinds for class refs. + final _runtimeObjectKinds = {}; + + ClassMetaDataHelper(this._inspector); /// Returns the [ClassMetaData] for the Chrome [remoteObject]. /// /// Returns null if the [remoteObject] is not a Dart class. - static Future metaDataFor( - RemoteObject remoteObject, - AppInspectorInterface inspector, - ) async { + Future metaDataFor(RemoteObject remoteObject) async { try { + /// TODO(annagrin): this breaks on changes to internal + /// type representation in DDC. Replace by runtime API. + /// https://github.com/dart-lang/sdk/issues/51583 final evalExpression = ''' function(arg) { const sdk = ${globalLoadStrategy.loadModuleSnippet}('dart_sdk'); const dart = sdk.dart; const core = sdk.core; const interceptors = sdk._interceptors; - const classObject = dart.getReifiedType(arg); - const isFunction = classObject instanceof dart.AbstractFunctionType; - const isRecord = classObject instanceof dart.RecordType; - const isRecordType = dart.is(arg, dart.RecordType); - const isNativeError = dart.is(arg, interceptors.NativeError); - const result = {}; - var name = isFunction ? 'Function' : classObject.name; + const reifiedType = dart.getReifiedType(arg); + const name = reifiedType.name; + const result = {}; result['name'] = name; - result['libraryId'] = dart.getLibraryUri(classObject); - result['dartName'] = dart.typeName(classObject); - result['isFunction'] = isFunction; - result['isRecord'] = isRecord; - result['isRecordType'] = isRecordType; - result['isNativeError'] = isNativeError; + result['libraryId'] = dart.getLibraryUri(reifiedType); + result['dartName'] = dart.typeName(reifiedType); result['length'] = arg['length']; + result['runtimeKind'] = '${RuntimeObjectKind.object.name}'; - if (isRecord) { + if (name == '_HashSet') { + result['runtimeKind'] = '${RuntimeObjectKind.set.name}'; + } + else if (name == 'JSArray') { + result['runtimeKind'] = '${RuntimeObjectKind.list.name}'; + } + else if (name == 'LinkedMap' || name == 'IdentityMap') { + result['runtimeKind'] = '${RuntimeObjectKind.map.name}'; + } + else if (reifiedType instanceof dart.AbstractFunctionType) { + result['runtimeKind'] = '${RuntimeObjectKind.function.name}'; + result['name'] = 'Function'; + } + else if (reifiedType instanceof dart.RecordType) { + result['runtimeKind'] = '${RuntimeObjectKind.record.name}'; + result['libraryId'] = 'dart:core'; result['name'] = 'Record'; - var shape = classObject.shape; + result['dartName'] = 'Record'; + var shape = reifiedType.shape; var positionalCount = shape.positionals; var namedCount = shape.named == null ? 0 : shape.named.length; result['length'] = positionalCount + namedCount; } - - if (isRecordType) { - result['name'] = 'RecordType'; - result['length'] = arg.types.length; + else if (arg instanceof dart._Type) { + var object = dart.dloadRepl(arg, "_type"); + if (object instanceof dart.RecordType) { + result['libraryId'] = 'dart:_runtime'; + result['name'] = 'RecordType'; + result['dartName'] = 'RecordType'; + result['runtimeKind'] = '${RuntimeObjectKind.recordType.name}'; + result['length'] = object.types.length; + } + else if (dart.is(object, core.Type)) { + result['libraryId'] = 'dart:core'; + result['name'] = 'Type'; + result['dartName'] = 'Type'; + result['runtimeKind'] = '${RuntimeObjectKind.type.name}'; + result['typeName'] = dart.dsendRepl(arg, "toString", []); + result['length'] = object['length']; + } + } + else if (dart.is(arg, interceptors.NativeError)) { + result['runtimeKind'] = '${RuntimeObjectKind.nativeError.name}'; + } + else if (dart.is(arg, interceptors.JavaScriptObject)) { + result['runtimeKind'] = '${RuntimeObjectKind.nativeObject.name}'; } return result; } '''; - final result = await inspector.jsCallFunctionOn( + final result = await _inspector.jsCallFunctionOn( remoteObject, evalExpression, [remoteObject], returnByValue: true, ); final metadata = result.value as Map; + final jsName = metadata['name']; + final typeName = metadata['typeName']; + final dartName = metadata['dartName']; + final library = metadata['libraryId']; + final runtimeKind = RuntimeObjectKind.parse(metadata['runtimeKind']); + final length = metadata['length']; + + final classRef = classRefFor(library, dartName); + _addRuntimeObjectKind(classRef, runtimeKind); + return ClassMetaData( - jsName: metadata['name'], - libraryId: metadata['libraryId'], - dartName: metadata['dartName'], - isFunction: metadata['isFunction'], - isRecord: metadata['isRecord'], - isRecordType: metadata['isRecordType'], - isNativeError: metadata['isNativeError'], - length: metadata['length'], + jsName: jsName, + typeName: typeName, + length: length, + runtimeKind: runtimeKind, + classRef: classRef, ); } on ChromeDebugException catch (e, s) { _logger.fine( @@ -225,31 +264,29 @@ class ClassMetaData { } } - /// TODO(annagrin): convert fields and getters below to kinds. - - /// True if this class refers to system maps, which are treated specially. - /// - /// Classes that implement Map or inherit from MapBase are still treated as - /// plain objects. - // TODO(alanknight): It may be that IdentityMap should not be treated as a - // system map. - bool get isSystemMap => jsName == 'LinkedMap' || jsName == 'IdentityMap'; - - /// True if this class refers to system Lists, which are treated specially. - bool get isSystemList => jsName == 'JSArray'; - - bool get isSet => jsName == '_HashSet'; - - /// True if this class refers to a function type. - bool isFunction; - - /// True if this class refers to a Record type. - bool isRecord; + // Stores runtime object kind for class refs. + void _addRuntimeObjectKind( + ClassRef classRef, + RuntimeObjectKind runtimeKind, + ) { + final id = classRef.id; + if (id == null) { + throw StateError('No classRef id for $classRef'); + } + _runtimeObjectKinds[id] = runtimeKind; + } - /// True if this class refers to a RecordType type. - bool isRecordType; + /// Returns true for non-dart JavaScript classes. + bool isNativeJsObject(ClassRef? classRef) { + final id = classRef?.id; + return id != null && + _runtimeObjectKinds[id] == RuntimeObjectKind.nativeObject; + } - /// True is this class refers to a native JS type. - /// i.e. inherits from NativeError. - bool isNativeError; + /// Returns true for non-dart JavaScript classes. + bool isNativeJsError(ClassRef? classRef) { + final id = classRef?.id; + return id != null && + _runtimeObjectKinds[id] == RuntimeObjectKind.nativeError; + } } diff --git a/dwds/lib/src/services/batched_expression_evaluator.dart b/dwds/lib/src/services/batched_expression_evaluator.dart index cadd0c784..b50e1cdaf 100644 --- a/dwds/lib/src/services/batched_expression_evaluator.dart +++ b/dwds/lib/src/services/batched_expression_evaluator.dart @@ -28,19 +28,19 @@ class EvaluateRequest { class BatchedExpressionEvaluator extends ExpressionEvaluator { final _logger = Logger('BatchedExpressionEvaluator'); - final Debugger _debugger; + final AppInspectorInterface _inspector; final _requestController = BatchedStreamController(delay: 200); bool _closed = false; BatchedExpressionEvaluator( String entrypoint, - AppInspectorInterface inspector, - this._debugger, + this._inspector, + Debugger debugger, Locations locations, Modules modules, ExpressionCompiler compiler, - ) : super(entrypoint, inspector, _debugger, locations, modules, compiler) { + ) : super(entrypoint, _inspector, debugger, locations, modules, compiler) { _requestController.stream.listen(_processRequest); } @@ -148,7 +148,7 @@ class BatchedExpressionEvaluator extends ExpressionEvaluator { request.completer.complete(error); } else { safeUnawaited( - _debugger + _inspector .getProperties( listId, offset: i, diff --git a/dwds/lib/src/services/chrome_proxy_service.dart b/dwds/lib/src/services/chrome_proxy_service.dart index 398dfb87a..105cca1bf 100644 --- a/dwds/lib/src/services/chrome_proxy_service.dart +++ b/dwds/lib/src/services/chrome_proxy_service.dart @@ -262,7 +262,6 @@ class ChromeProxyService implements VmServiceInterface { debugger, executionContext, ); - debugger.updateInspector(inspector); final compiler = _compiler; _expressionEvaluator = compiler == null diff --git a/dwds/lib/src/services/expression_evaluator.dart b/dwds/lib/src/services/expression_evaluator.dart index 1e1be6fad..19b45ad3e 100644 --- a/dwds/lib/src/services/expression_evaluator.dart +++ b/dwds/lib/src/services/expression_evaluator.dart @@ -350,7 +350,7 @@ class ExpressionEvaluator { for (var scope in scopeChain) { final objectId = scope.object.objectId; if (objectId != null) { - final scopeProperties = await _debugger.getProperties(objectId); + final scopeProperties = await _inspector.getProperties(objectId); collectVariables(scopeProperties); } } diff --git a/dwds/lib/src/utilities/domain.dart b/dwds/lib/src/utilities/domain.dart index 4d1a896ad..699bd586b 100644 --- a/dwds/lib/src/utilities/domain.dart +++ b/dwds/lib/src/utilities/domain.dart @@ -4,6 +4,7 @@ import 'package:dwds/src/connections/app_connection.dart'; import 'package:dwds/src/debugging/remote_debugger.dart'; +import 'package:dwds/src/utilities/objects.dart'; import 'package:vm_service/vm_service.dart'; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; @@ -91,6 +92,22 @@ abstract class AppInspectorInterface { /// All the scripts in the isolate. Future getScripts(); + /// Calls the Chrome Runtime.getProperties API for the object with [objectId]. + /// + /// Note that the property names are JS names, e.g. + /// Symbol(DartClass.actualName) and will need to be converted. For a system + /// List or Map, [offset] and/or [count] can be provided to indicate a desired + /// range of entries. They will be ignored if there is no [length]. + Future> getProperties( + String objectId, { + int? offset, + int? count, + int? length, + }); + + bool isDisplayableObject(Object? object); + bool isNativeJsError(InstanceRef instanceRef); + /// Return the VM SourceReport for the given parameters. /// /// Currently this implements the 'PossibleBreakpoints' report kind. diff --git a/dwds/test/debugger_test.dart b/dwds/test/debugger_test.dart index 58aa29a43..bac31b50f 100644 --- a/dwds/test/debugger_test.dart +++ b/dwds/test/debugger_test.dart @@ -98,7 +98,7 @@ void main() async { skipLists, root, ); - inspector = FakeInspector(fakeIsolate: simpleIsolate); + inspector = FakeInspector(webkitDebugger, fakeIsolate: simpleIsolate); debugger.updateInspector(inspector); }); @@ -168,7 +168,7 @@ void main() async { setUp(() { // We need to provide an Isolate so that the code doesn't bail out on a null // check before it has a chance to throw. - inspector = FakeInspector(fakeIsolate: simpleIsolate); + inspector = FakeInspector(webkitDebugger, fakeIsolate: simpleIsolate); debugger.updateInspector(inspector); }); @@ -176,7 +176,7 @@ void main() async { setUp(() { // We need to provide an Isolate so that the code doesn't bail out on a null // check before it has a chance to throw. - inspector = FakeInspector(fakeIsolate: simpleIsolate); + inspector = FakeInspector(webkitDebugger, fakeIsolate: simpleIsolate); debugger.updateInspector(inspector); }); diff --git a/dwds/test/evaluate_common.dart b/dwds/test/evaluate_common.dart index 7f0ee77b1..aefb331f6 100644 --- a/dwds/test/evaluate_common.dart +++ b/dwds/test/evaluate_common.dart @@ -284,32 +284,25 @@ void testAll({ final instanceRef = result as InstanceRef; // Type - var instance = await getInstance(instanceRef); + final instance = await getInstance(instanceRef); for (var field in instance.fields!) { final name = field.decl!.name; + final fieldInstance = + await getInstance(field.value as InstanceRef); - // Type. (i.e. Type._type) - instance = await getInstance(field.value as InstanceRef); - for (var field in instance.fields!) { - final nestedName = field.decl!.name; - - // Type.. (i.e Type._type._subtypeCache) - instance = await getInstance(field.value as InstanceRef); - - expect( - instance, - isA().having( - (instance) => instance.classRef!.name, - 'Type.$name.$nestedName: classRef.name', - isNot( - isIn([ - 'NativeJavaScriptObject', - 'JavaScriptObject', - ]), - ), + expect( + fieldInstance, + isA().having( + (i) => i.classRef!.name, + 'Type.$name: classRef.name', + isNot( + isIn([ + 'NativeJavaScriptObject', + 'JavaScriptObject', + ]), ), - ); - } + ), + ); } }); }); diff --git a/dwds/test/expression_evaluator_test.dart b/dwds/test/expression_evaluator_test.dart index 908a1741b..e934d8366 100644 --- a/dwds/test/expression_evaluator_test.dart +++ b/dwds/test/expression_evaluator_test.dart @@ -58,7 +58,8 @@ void main() async { skipLists, root, ); - final inspector = FakeInspector(fakeIsolate: simpleIsolate); + final inspector = + FakeInspector(webkitDebugger, fakeIsolate: simpleIsolate); debugger.updateInspector(inspector); _evaluator = ExpressionEvaluator( diff --git a/dwds/test/fixtures/fakes.dart b/dwds/test/fixtures/fakes.dart index fdac99b1a..e51ecc501 100644 --- a/dwds/test/fixtures/fakes.dart +++ b/dwds/test/fixtures/fakes.dart @@ -6,11 +6,9 @@ import 'dart:async'; import 'package:dwds/asset_reader.dart'; import 'package:dwds/expression_compiler.dart'; -import 'package:dwds/src/debugging/classes.dart'; import 'package:dwds/src/debugging/execution_context.dart'; import 'package:dwds/src/debugging/inspector.dart'; import 'package:dwds/src/debugging/instance.dart'; -import 'package:dwds/src/debugging/libraries.dart'; import 'package:dwds/src/debugging/metadata/provider.dart'; import 'package:dwds/src/debugging/modules.dart'; import 'package:dwds/src/debugging/remote_debugger.dart'; @@ -18,6 +16,7 @@ import 'package:dwds/src/debugging/webkit_debugger.dart'; import 'package:dwds/src/handlers/socket_connections.dart'; import 'package:dwds/src/loaders/require.dart'; import 'package:dwds/src/loaders/strategy.dart'; +import 'package:dwds/src/utilities/objects.dart'; import 'package:shelf/shelf.dart' as shelf; import 'package:vm_service/vm_service.dart'; @@ -46,7 +45,8 @@ Isolate get simpleIsolate => Isolate( ); class FakeInspector implements AppInspector { - FakeInspector({required this.fakeIsolate}); + final WebkitDebugger _remoteDebugger; + FakeInspector(this._remoteDebugger, {required this.fakeIsolate}); Isolate fakeIsolate; @@ -63,12 +63,7 @@ class FakeInspector implements AppInspector { RemoteObject({'type': 'string', 'value': 'true'}); @override - Future initialize( - LibraryHelper libraryHelper, - ClassHelper classHelper, - InstanceHelper instanceHelper, - ) async => - {}; + Future initialize() async => {}; @override Future instanceRefFor(Object value) async => @@ -98,6 +93,32 @@ class FakeInspector implements AppInspector { name: fakeIsolate.name, isSystemIsolate: fakeIsolate.isSystemIsolate, ); + + @override + Future> getProperties( + String objectId, { + int? offset, + int? count, + int? length, + }) async { + final response = await _remoteDebugger.sendCommand( + 'Runtime.getProperties', + params: { + 'objectId': objectId, + 'ownProperties': true, + }, + ); + final result = response.result?['result']; + return result + .map((each) => Property(each as Map)) + .toList(); + } + + @override + bool isDisplayableObject(Object? object) => true; + + @override + bool isNativeJsError(InstanceRef instanceRef) => false; } class FakeSseConnection implements SseSocketConnection { diff --git a/dwds/test/inspector_test.dart b/dwds/test/inspector_test.dart index dd2f3fa87..6e66a547c 100644 --- a/dwds/test/inspector_test.dart +++ b/dwds/test/inspector_test.dart @@ -6,7 +6,6 @@ @Timeout(Duration(minutes: 2)) import 'package:dwds/dwds.dart'; -import 'package:dwds/src/debugging/debugger.dart'; import 'package:dwds/src/debugging/inspector.dart'; import 'package:dwds/src/loaders/strategy.dart'; import 'package:dwds/src/utilities/conversions.dart'; @@ -26,13 +25,11 @@ void main() { TestContext(TestProject.testScopesWithSoundNullSafety, provider); late AppInspector inspector; - late Debugger debugger; setUpAll(() async { await context.setUp(); final service = context.service; inspector = service.inspector; - debugger = await service.debuggerFuture; }); tearDownAll(() async { @@ -159,7 +156,7 @@ void main() { test('properties', () async { final remoteObject = await libraryPublicFinal(); - final properties = await debugger.getProperties(remoteObject.objectId!); + final properties = await inspector.getProperties(remoteObject.objectId!); final names = properties.map((p) => p.name).where((x) => x != '__proto__').toList(); final expected = [ diff --git a/dwds/test/instances/instance_inspection_common.dart b/dwds/test/instances/instance_inspection_common.dart index c4c61c388..64a6d03ee 100644 --- a/dwds/test/instances/instance_inspection_common.dart +++ b/dwds/test/instances/instance_inspection_common.dart @@ -5,7 +5,6 @@ @TestOn('vm') @Timeout(Duration(minutes: 2)) -import 'package:dwds/src/debugging/metadata/class.dart'; import 'package:test/test.dart'; import 'package:vm_service/vm_service.dart'; @@ -65,16 +64,12 @@ class TestInspector { expect(result, isA()); final instance = result as Instance; - // TODO(annagrin): we sometimes get mismatching reference - // and instance kinds from chrome. Investigate. - if (instanceRef.kind != InstanceKind.kClosure) { - expect( - instance.kind, - instanceRef.kind, - reason: 'object $instanceId with ref kind ${instanceRef.kind} ' - 'has an instance kind ${instance.kind}', - ); - } + expect( + instance.kind, + instanceRef.kind, + reason: 'object $instanceId with ref kind ${instanceRef.kind} ' + 'has an instance kind ${instance.kind}', + ); final fields = instance.fields; final associations = instance.associations; @@ -161,41 +156,41 @@ class TestInspector { return instances; } - Future getUnwrappedTypeInstanceRef( + Future getDisplayedRef( String isolateId, - InstanceRef ref, - ) async { - final typeClassId = ref.classRef!.id!; - - // `o.runtimeType` is an instance of `Type`. - expect(await service.getObject(isolateId, typeClassId), matchTypeClass); - - // Get `o.runtimeType._type`. - return (await getFields(isolateId, ref, depth: 1) - as Map)['_type'] as InstanceRef; - } + String instanceId, + ) async => + await service.invoke(isolateId, instanceId, 'toString', []) + as InstanceRef; - Future getUnwrappedTypeInstance( + Future> getDisplayedFields( String isolateId, InstanceRef ref, ) async { - final typeInstanceRef = await getUnwrappedTypeInstanceRef(isolateId, ref); - return await service.getObject(isolateId, typeInstanceRef.id!) as Instance; + final fieldRefs = + await getFields(isolateId, ref, depth: 1) as Map; + + Future toStringValue(InstanceRef ref) async => + ref.valueAsString ?? + (await getDisplayedRef(isolateId, ref.id!)).valueAsString; + + final fields = await Future.wait(fieldRefs.values.map(toStringValue)); + return fields.toList(); } - Future> getFieldTypes( + Future> getElements( String isolateId, - InstanceRef ref, + String instanceId, ) async { - final fieldTypeInstanceRefs = - await getFields(isolateId, ref, depth: 1) as Map; - - final fieldTypes = await Future.wait( - fieldTypeInstanceRefs.values.map( - (ref) async => await service.invoke(isolateId, ref.id!, 'toString', []), + final instance = await service.getObject(isolateId, instanceId) as Instance; + return Future.wait( + instance.fields!.map( + (e) async => await service.getObject( + isolateId, + (e.value as InstanceRef).id!, + ) as Instance, ), ); - return fieldTypes.map((ref) => (ref as InstanceRef).valueAsString).toList(); } } @@ -228,6 +223,16 @@ Matcher matchRecordTypeInstanceRef({required int length}) => isA() .having((e) => e.length, 'length', length) .having((e) => e.classRef!, 'classRef', matchRecordTypeClassRef); +Matcher matchTypeInstanceRef(String name) => isA() + .having((e) => e.kind, 'kind', InstanceKind.kType) + .having((e) => e.name, 'name', name) + .having((e) => e.classRef, 'classRef', matchTypeClassRef); + +Matcher matchPrimitiveInstanceRef({ + required String kind, +}) => + isA().having((e) => e.kind, 'kind', kind); + Matcher matchPrimitiveInstance({ required String kind, required dynamic value, @@ -236,52 +241,77 @@ Matcher matchPrimitiveInstance({ .having((e) => e.kind, 'kind', kind) .having(_getValue, 'value', value); -Matcher matchPlainInstance({required String type}) => isA() - .having((e) => e.kind, 'kind', InstanceKind.kPlainInstance) - .having((e) => e.classRef!.name, 'classRef.name', type); +Matcher matchPlainInstance({required libraryId, required String type}) => + isA() + .having((e) => e.kind, 'kind', InstanceKind.kPlainInstance) + .having( + (e) => e.classRef, + 'classRef', + matchClassRef(name: type, libraryId: libraryId), + ); Matcher matchListInstance({required String type}) => isA() .having((e) => e.kind, 'kind', InstanceKind.kList) - .having((e) => e.classRef!.name, 'classRef.name', type); + .having((e) => e.classRef, 'classRef', matchListClassRef(type)); Matcher matchMapInstance({required String type}) => isA() .having((e) => e.kind, 'kind', InstanceKind.kMap) - .having((e) => e.classRef!.name, 'classRef.name', type); + .having((e) => e.classRef, 'classRef', matchMapClassRef(type)); Matcher matchSetInstance({required String type}) => isA() .having((e) => e.kind, 'kind', InstanceKind.kSet) - .having((e) => e.classRef!.name, 'classRef.name', type); + .having((e) => e.classRef, 'classRef', matchSetClassRef(type)); Matcher matchRecordInstance({required int length}) => isA() .having((e) => e.kind, 'kind', InstanceKind.kRecord) .having((e) => e.length, 'length', length) - .having((e) => e.classRef!, 'classRef', matchRecordClassRef); + .having((e) => e.classRef, 'classRef', matchRecordClassRef); Matcher matchRecordTypeInstance({required int length}) => isA() .having((e) => e.kind, 'kind', InstanceKind.kRecordType) .having((e) => e.length, 'length', length) - .having((e) => e.classRef!, 'classRef', matchRecordTypeClassRef); + .having((e) => e.classRef, 'classRef', matchRecordTypeClassRef); -Matcher matchTypeInstance = isA() - .having((e) => e.kind, 'kind', InstanceKind.kPlainInstance) - .having((e) => e.classRef!.name, 'classRef.name', matchTypeClassRef); +Matcher matchTypeStringInstance(String name) => + matchPrimitiveInstance(kind: InstanceKind.kString, value: name); -Matcher matchRecordClass = matchClass(libraryId: 'dart:core', type: 'Record'); -Matcher matchRecordTypeClass = - matchClass(libraryId: 'dart:_runtime', type: 'RecordType'); -Matcher matchTypeClass = matchClass(libraryId: 'dart:_runtime', type: '_Type'); - -Matcher matchClass({required String libraryId, required String type}) => - isA() - .having((e) => e.name, 'name', type) - .having((e) => e.id, 'id', classIdFor(libraryId, type)); - -Matcher matchRecordClassRef = matchClassRef('Record'); -Matcher matchRecordTypeClassRef = matchClassRef('RecordType'); -Matcher matchTypeClassRef = matchClassRef('Type'); +Matcher matchTypeInstance(String name) => isA() + .having((e) => e.kind, 'kind', InstanceKind.kType) + .having((e) => e.name, 'name', name) + .having((e) => e.classRef, 'classRef', matchTypeClassRef); -Matcher matchClassRef(String type) => - isA().having((e) => e.name, 'class name', type); +Matcher matchRecordClass = + matchClass(name: _recordClass, libraryId: _dartCoreLibrary); +Matcher matchRecordTypeClass = + matchClass(name: _recordTypeClass, libraryId: _dartRuntimeLibrary); +Matcher matchTypeClass = + matchClass(name: _typeClass, libraryId: _dartCoreLibrary); + +Matcher matchClass({String? name, String? libraryId}) => isA() + .having((e) => e.name, 'name', name) + .having((e) => e.library, 'library', matchLibraryRef(libraryId)); + +Matcher matchRecordClassRef = + matchClassRef(name: _recordClass, libraryId: _dartCoreLibrary); +Matcher matchRecordTypeClassRef = + matchClassRef(name: _recordTypeClass, libraryId: _dartRuntimeLibrary); +Matcher matchTypeClassRef = + matchClassRef(name: _typeClass, libraryId: _dartCoreLibrary); +Matcher matchListClassRef(String type) => + matchClassRef(name: type, libraryId: _dartInterceptorsLibrary); +Matcher matchMapClassRef(String type) => + matchClassRef(name: type, libraryId: _dartJsHelperLibrary); +Matcher matchSetClassRef(String type) => + matchClassRef(name: type, libraryId: _dartCollectionLibrary); + +Matcher matchClassRef({String? name, String? libraryId}) => isA() + .having((e) => e.name, 'class name', name) + .having((e) => e.library, 'library', matchLibraryRef(libraryId)); + +Matcher matchLibraryRef(String? libraryId) => isA() + .having((e) => e.name, 'name', libraryId) + .having((e) => e.id, 'id', libraryId) + .having((e) => e.uri, 'uri', libraryId); Object? _getValue(InstanceRef instanceRef) { switch (instanceRef.kind) { @@ -296,3 +326,13 @@ Object? _getValue(InstanceRef instanceRef) { return null; } } + +final _dartCoreLibrary = 'dart:core'; +final _dartRuntimeLibrary = 'dart:_runtime'; +final _dartInterceptorsLibrary = 'dart:_interceptors'; +final _dartJsHelperLibrary = 'dart:_js_helper'; +final _dartCollectionLibrary = 'dart:collection'; + +final _recordClass = 'Record'; +final _recordTypeClass = 'RecordType'; +final _typeClass = 'Type'; diff --git a/dwds/test/instances/instance_inspection_test.dart b/dwds/test/instances/instance_inspection_test.dart index d627a6bcc..9be7dc107 100644 --- a/dwds/test/instances/instance_inspection_test.dart +++ b/dwds/test/instances/instance_inspection_test.dart @@ -106,7 +106,10 @@ Future _runTests({ final instanceId = instanceRef.id!; expect( await getObject(instanceId), - matchPlainInstance(type: 'MainClass'), + matchPlainInstance( + libraryId: 'org-dartlang-app:///web/main.dart', + type: 'MainClass', + ), ); expect(await getFields(instanceRef), {'_field': 1, 'field': 2}); diff --git a/dwds/test/instances/instance_test.dart b/dwds/test/instances/instance_test.dart index a20a8ba42..2fe80d7be 100644 --- a/dwds/test/instances/instance_test.dart +++ b/dwds/test/instances/instance_test.dart @@ -4,7 +4,6 @@ @Timeout(Duration(minutes: 2)) -import 'package:dwds/src/debugging/debugger.dart'; import 'package:dwds/src/debugging/inspector.dart'; import 'package:dwds/src/loaders/strategy.dart'; import 'package:test/test.dart'; @@ -26,14 +25,12 @@ void main() { TestContext(TestProject.testScopesWithSoundNullSafety, provider); late AppInspector inspector; - late Debugger debugger; setUpAll(() async { setCurrentLogWriter(debug: debug); await context.setUp(); final chromeProxyService = context.service; inspector = chromeProxyService.inspector; - debugger = await chromeProxyService.debuggerFuture; }); tearDownAll(() async { @@ -70,6 +67,7 @@ void main() { final classRef = ref.classRef!; expect(classRef.name, 'Null'); expect(classRef.id, 'classes|dart:core|Null'); + expect(inspector.isDisplayableObject(ref), isTrue); }); test('for a double', () async { @@ -81,6 +79,7 @@ void main() { final classRef = ref.classRef!; expect(classRef.name, 'Double'); expect(classRef.id, 'classes|dart:core|Double'); + expect(inspector.isDisplayableObject(ref), isTrue); }); test('for a class', () async { @@ -94,20 +93,22 @@ void main() { classRef.id, 'classes|org-dartlang-app:///example/scopes/main.dart' '|MyTestClass'); + expect(inspector.isDisplayableObject(ref), isTrue); }); test('for closure', () async { final remoteObject = await libraryPublicFinal(); - final properties = await debugger.getProperties(remoteObject.objectId!); + final properties = await inspector.getProperties(remoteObject.objectId!); final closure = properties.firstWhere((property) => property.name == 'closure'); - final instanceRef = await inspector.instanceRefFor(closure.value!); - final functionName = instanceRef!.closureFunction!.name; + final ref = await inspector.instanceRefFor(closure.value!); + final functionName = ref!.closureFunction!.name; // Older SDKs do not contain function names if (functionName != 'Closure') { expect(functionName, 'someFunction'); } - expect(instanceRef.kind, InstanceKind.kClosure); + expect(ref.kind, InstanceKind.kClosure); + expect(inspector.isDisplayableObject(ref), isTrue); }); test('for a list', () async { @@ -116,6 +117,7 @@ void main() { expect(ref!.length, greaterThan(0)); expect(ref.kind, InstanceKind.kList); expect(ref.classRef!.name, 'List'); + expect(inspector.isDisplayableObject(ref), isTrue); }); test('for map', () async { @@ -125,6 +127,7 @@ void main() { expect(ref!.length, 2); expect(ref.kind, InstanceKind.kMap); expect(ref.classRef!.name, 'LinkedMap'); + expect(inspector.isDisplayableObject(ref), isTrue); }); test('for an IdentityMap', () async { @@ -134,6 +137,7 @@ void main() { expect(ref!.length, 2); expect(ref.kind, InstanceKind.kMap); expect(ref.classRef!.name, 'IdentityMap'); + expect(inspector.isDisplayableObject(ref), isTrue); }); test('for a native JavaScript error', () async { @@ -142,6 +146,9 @@ void main() { final ref = await inspector.instanceRefFor(remoteObject); expect(ref!.kind, InstanceKind.kPlainInstance); expect(ref.classRef!.name, 'NativeError'); + expect(inspector.isDisplayableObject(ref), isFalse); + expect(inspector.isNativeJsError(ref), isTrue); + expect(inspector.isNativeJsObject(ref), isFalse); }); test('for a native JavaScript type error', () async { @@ -149,7 +156,21 @@ void main() { .jsEvaluate(interceptorsNewExpression('JSNoSuchMethodError')); final ref = await inspector.instanceRefFor(remoteObject); expect(ref!.kind, InstanceKind.kPlainInstance); - expect(ref.classRef!.name, 'NativeError'); + expect(ref.classRef!.name, 'JSNoSuchMethodError'); + expect(inspector.isDisplayableObject(ref), isFalse); + expect(inspector.isNativeJsError(ref), isTrue); + expect(inspector.isNativeJsObject(ref), isFalse); + }); + + test('for a native JavaScript object', () async { + final remoteObject = await inspector + .jsEvaluate(interceptorsNewExpression('LegacyJavaScriptObject')); + final ref = await inspector.instanceRefFor(remoteObject); + expect(ref!.kind, InstanceKind.kPlainInstance); + expect(ref.classRef!.name, 'LegacyJavaScriptObject'); + expect(inspector.isDisplayableObject(ref), isFalse); + expect(inspector.isNativeJsError(ref), isFalse); + expect(inspector.isNativeJsObject(ref), isTrue); }); }); @@ -181,16 +202,18 @@ void main() { expect(field.name, isNotNull); expect(field.decl!.declaredType, isNotNull); } + expect(inspector.isDisplayableObject(instance), isTrue); }); test('for closure', () async { final remoteObject = await libraryPublicFinal(); - final properties = await debugger.getProperties(remoteObject.objectId!); + final properties = await inspector.getProperties(remoteObject.objectId!); final closure = properties.firstWhere((property) => property.name == 'closure'); final instance = await inspector.instanceFor(closure.value!); expect(instance!.kind, InstanceKind.kClosure); expect(instance.classRef!.name, 'Closure'); + expect(inspector.isDisplayableObject(instance), isTrue); }); test('for a nested class', () async { @@ -202,6 +225,7 @@ void main() { final classRef = instance.classRef!; expect(classRef, isNotNull); expect(classRef.name, 'MyTestClass'); + expect(inspector.isDisplayableObject(instance), isTrue); }); test('for a list', () async { @@ -213,6 +237,7 @@ void main() { expect(classRef.name, 'List'); final first = instance.elements![0]; expect(first.valueAsString, 'library'); + expect(inspector.isDisplayableObject(instance), isTrue); }); test('for a map', () async { @@ -228,6 +253,7 @@ void main() { final second = instance.associations![1].value as InstanceRef; expect(second.kind, InstanceKind.kString); expect(second.valueAsString, 'something'); + expect(inspector.isDisplayableObject(instance), isTrue); }); test('for an identityMap', () async { @@ -239,6 +265,7 @@ void main() { expect(classRef.name, 'IdentityMap'); final first = instance.associations![0].value; expect(first.valueAsString, '1'); + expect(inspector.isDisplayableObject(instance), isTrue); }); test('for a class that implements List', () async { @@ -252,6 +279,40 @@ void main() { expect(instance.elements, isNull); final field = instance.fields!.first; expect(field.decl!.name, '_internal'); + expect(inspector.isDisplayableObject(instance), isTrue); + }); + + test('for a native JavaScript error', () async { + final remoteObject = + await inspector.jsEvaluate(interceptorsNewExpression('NativeError')); + final instance = await inspector.instanceFor(remoteObject); + expect(instance!.kind, InstanceKind.kPlainInstance); + expect(instance.classRef!.name, 'NativeError'); + expect(inspector.isDisplayableObject(instance), isFalse); + expect(inspector.isNativeJsError(instance), isTrue); + expect(inspector.isNativeJsObject(instance), isFalse); + }); + + test('for a native JavaScript type error', () async { + final remoteObject = await inspector + .jsEvaluate(interceptorsNewExpression('JSNoSuchMethodError')); + final instance = await inspector.instanceFor(remoteObject); + expect(instance!.kind, InstanceKind.kPlainInstance); + expect(instance.classRef!.name, 'JSNoSuchMethodError'); + expect(inspector.isDisplayableObject(instance), isFalse); + expect(inspector.isNativeJsError(instance), isTrue); + expect(inspector.isNativeJsObject(instance), isFalse); + }); + + test('for a native JavaScript object', () async { + final remoteObject = await inspector + .jsEvaluate(interceptorsNewExpression('LegacyJavaScriptObject')); + final instance = await inspector.instanceFor(remoteObject); + expect(instance!.kind, InstanceKind.kPlainInstance); + expect(instance.classRef!.name, 'LegacyJavaScriptObject'); + expect(inspector.isDisplayableObject(instance), isFalse); + expect(inspector.isNativeJsError(instance), isFalse); + expect(inspector.isNativeJsObject(instance), isTrue); }); }); } diff --git a/dwds/test/instances/record_type_inspection_test.dart b/dwds/test/instances/record_type_inspection_test.dart index 78c946cf8..d3e6254a0 100644 --- a/dwds/test/instances/record_type_inspection_test.dart +++ b/dwds/test/instances/record_type_inspection_test.dart @@ -57,13 +57,11 @@ Future _runTests({ getInstanceRef(frame, expression) => testInspector.getInstanceRef(isolateId, frame, expression); - getUnwrappedTypeInstanceRef(ref) => - testInspector.getUnwrappedTypeInstanceRef(isolateId, ref); + getDisplayedFields(InstanceRef ref) => + testInspector.getDisplayedFields(isolateId, ref); - getUnwrappedTypeInstance(ref) => - testInspector.getUnwrappedTypeInstance(isolateId, ref); - - getFieldTypes(InstanceRef ref) => testInspector.getFieldTypes(isolateId, ref); + getElements(String instanceId) => + testInspector.getElements(isolateId, instanceId); group('$compilationMode |', () { setUpAll(() async { @@ -97,20 +95,29 @@ Future _runTests({ test('simple record type', () async { await onBreakPoint('printSimpleLocalRecord', (event) async { final frame = event.topFrame!.index!; - final typeInstanceRef = - await getInstanceRef(frame, 'record.runtimeType'); - - final ref = await getUnwrappedTypeInstanceRef(typeInstanceRef); - final instance = await getUnwrappedTypeInstance(typeInstanceRef); + final instanceRef = await getInstanceRef(frame, 'record.runtimeType'); + final instanceId = instanceRef.id!; - expect(ref, matchRecordTypeInstanceRef(length: 2)); - expect(instance, matchRecordTypeInstance(length: 2)); + expect(instanceRef, matchRecordTypeInstanceRef(length: 2)); + expect(await getObject(instanceId), matchRecordTypeInstance(length: 2)); - final classId = instance.classRef!.id; + final classId = instanceRef.classRef!.id; expect(await getObject(classId), matchRecordTypeClass); + }); + }); + + test('simple record type elements', () async { + await onBreakPoint('printSimpleLocalRecord', (event) async { + final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, 'record.runtimeType'); + final instanceId = instanceRef.id!; expect( - await getFieldTypes(ref), + await getElements(instanceId), + [matchTypeInstance('bool'), matchTypeInstance('int')], + ); + expect( + await getDisplayedFields(instanceRef), ['bool', 'int'], ); }); @@ -136,26 +143,39 @@ Future _runTests({ test('complex record type', () async { await onBreakPoint('printComplexLocalRecord', (event) async { final frame = event.topFrame!.index!; - final typeInstanceRef = - await getInstanceRef(frame, 'record.runtimeType'); + final instanceRef = await getInstanceRef(frame, 'record.runtimeType'); + final instanceId = instanceRef.id!; - final ref = await getUnwrappedTypeInstanceRef(typeInstanceRef); - final instance = await getUnwrappedTypeInstance(typeInstanceRef); + expect(instanceRef, matchRecordTypeInstanceRef(length: 3)); + expect(await getObject(instanceId), matchRecordTypeInstance(length: 3)); - expect(ref, matchRecordTypeInstanceRef(length: 3)); - expect(instance, matchRecordTypeInstance(length: 3)); - - final classId = instance.classRef!.id; + final classId = instanceRef.classRef!.id; expect(await getObject(classId), matchRecordTypeClass); + }); + }); + test('complex record type elements', () async { + await onBreakPoint('printComplexLocalRecord', (event) async { + final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, 'record.runtimeType'); + final instanceId = instanceRef.id!; + + expect( + await getElements(instanceId), + [ + matchTypeInstance('bool'), + matchTypeInstance('int'), + matchTypeInstance('IdentityMap'), + ], + ); expect( - await getFieldTypes(ref), + await getDisplayedFields(instanceRef), ['bool', 'int', 'IdentityMap'], ); }); }); - test('complex records display', () async { + test('complex record type display', () async { await onBreakPoint('printComplexLocalRecord', (event) async { final frame = event.topFrame!.index!; final typeStringRef = @@ -175,20 +195,34 @@ Future _runTests({ test('complex record type with named fields ', () async { await onBreakPoint('printComplexNamedLocalRecord', (event) async { final frame = event.topFrame!.index!; - final typeInstanceRef = - await getInstanceRef(frame, 'record.runtimeType'); - - final ref = await getUnwrappedTypeInstanceRef(typeInstanceRef); - final instance = await getUnwrappedTypeInstance(typeInstanceRef); + final instanceRef = await getInstanceRef(frame, 'record.runtimeType'); + final instanceId = instanceRef.id!; - expect(ref, matchRecordTypeInstanceRef(length: 3)); - expect(instance, matchRecordTypeInstance(length: 3)); + expect(instanceRef, matchRecordTypeInstanceRef(length: 3)); + expect(await getObject(instanceId), matchRecordTypeInstance(length: 3)); - final classId = instance.classRef!.id; + final classId = instanceRef.classRef!.id; expect(await getObject(classId), matchRecordTypeClass); + }); + }); + + test('complex record type with named fields elements', () async { + await onBreakPoint('printComplexNamedLocalRecord', (event) async { + final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, 'record.runtimeType'); + final instanceId = instanceRef.id!; expect( - await getFieldTypes(ref), + await getElements(instanceId), + [ + matchTypeInstance('bool'), + matchTypeInstance('int'), + matchTypeInstance('IdentityMap'), + ], + ); + + expect( + await getDisplayedFields(instanceRef), ['bool', 'int', 'IdentityMap'], ); }); @@ -211,25 +245,43 @@ Future _runTests({ }); }); - test('nested record type display', () async { + test('nested record type', () async { await onBreakPoint('printNestedLocalRecord', (event) async { final frame = event.topFrame!.index!; - final typeInstanceRef = - await getInstanceRef(frame, 'record.runtimeType'); + final instanceRef = await getInstanceRef(frame, 'record.runtimeType'); + final instanceId = instanceRef.id!; - final ref = await getUnwrappedTypeInstanceRef(typeInstanceRef); - final instance = await getUnwrappedTypeInstance(typeInstanceRef); + expect(instanceRef, matchRecordTypeInstanceRef(length: 2)); + expect(await getObject(instanceId), matchRecordTypeInstance(length: 2)); - expect(ref, matchRecordTypeInstanceRef(length: 2)); - expect(instance, matchRecordTypeInstance(length: 2)); - - final classId = instance.classRef!.id; + final classId = instanceRef.classRef!.id; expect(await getObject(classId), matchRecordTypeClass); + }); + }); + test('nested record type elements', () async { + await onBreakPoint('printNestedLocalRecord', (event) async { + final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, 'record.runtimeType'); + final instanceId = instanceRef.id!; + + final elements = await getElements(instanceId); + expect( + elements, + [matchTypeInstance('bool'), matchRecordTypeInstance(length: 2)], + ); + expect( + await getElements(elements[1].id!), + [matchTypeInstance('bool'), matchTypeInstance('int')], + ); expect( - await getFieldTypes(ref), + await getDisplayedFields(instanceRef), ['bool', '(bool, int)'], ); + expect( + await getDisplayedFields(elements[1]), + ['bool', 'int'], + ); }); }); @@ -251,36 +303,54 @@ Future _runTests({ }); test('nested record type with named fields', () async { - await onBreakPoint('printNestedLocalRecord', (event) async { + await onBreakPoint('printNestedNamedLocalRecord', (event) async { final frame = event.topFrame!.index!; - final typeInstanceRef = - await getInstanceRef(frame, 'record.runtimeType'); - - final ref = await getUnwrappedTypeInstanceRef(typeInstanceRef); - final instance = await getUnwrappedTypeInstance(typeInstanceRef); + final instanceRef = await getInstanceRef(frame, 'record.runtimeType'); + final instanceId = instanceRef.id!; + final instance = await getObject(instanceId); - expect(ref, matchRecordTypeInstanceRef(length: 2)); + expect(instanceRef, matchRecordTypeInstanceRef(length: 2)); expect(instance, matchRecordTypeInstance(length: 2)); - final classId = instance.classRef!.id; + final classId = instanceRef.classRef!.id; expect(await getObject(classId), matchRecordTypeClass); + }); + }); + + test('nested record type with named fields elements', () async { + await onBreakPoint('printNestedNamedLocalRecord', (event) async { + final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, 'record.runtimeType'); + final instanceId = instanceRef.id!; + final elements = await getElements(instanceId); expect( - await getFieldTypes(ref), + elements, + [matchTypeInstance('bool'), matchRecordTypeInstance(length: 2)], + ); + expect( + await getElements(elements[1].id!), + [matchTypeInstance('bool'), matchTypeInstance('int')], + ); + expect( + await getDisplayedFields(instanceRef), ['bool', '(bool, int)'], ); + expect( + await getDisplayedFields(elements[1]), + ['bool', 'int'], + ); }); }); test('nested record type with named fields display', () async { await onBreakPoint('printNestedNamedLocalRecord', (event) async { final frame = event.topFrame!.index!; - final typeInstanceRef = - await getInstanceRef(frame, 'record.runtimeType'); - final typeInstance = await getObject(typeInstanceRef.id!); - final typeClassId = typeInstance.classRef!.id; + final instanceRef = await getInstanceRef(frame, 'record.runtimeType'); + final instance = await getObject(instanceRef.id!); + final typeClassId = instance.classRef!.id; - expect(await getObject(typeClassId), matchTypeClass); + expect(await getObject(typeClassId), matchRecordTypeClass); final typeStringRef = await getInstanceRef(frame, 'record.runtimeType.toString()'); diff --git a/dwds/test/instances/type_inspection_test.dart b/dwds/test/instances/type_inspection_test.dart new file mode 100644 index 000000000..af3a9b625 --- /dev/null +++ b/dwds/test/instances/type_inspection_test.dart @@ -0,0 +1,245 @@ +// Copyright (c) 2023, 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. + +@TestOn('vm') +@Timeout(Duration(minutes: 2)) + +import 'package:test/test.dart'; +import 'package:test_common/logging.dart'; +import 'package:test_common/test_sdk_configuration.dart'; +import 'package:vm_service/vm_service.dart'; + +import '../fixtures/context.dart'; +import '../fixtures/project.dart'; +import 'instance_inspection_common.dart'; + +void main() async { + // Enable verbose logging for debugging. + final debug = false; + + final provider = TestSdkConfigurationProvider(verbose: debug); + tearDownAll(provider.dispose); + + for (var compilationMode in CompilationMode.values) { + await _runTests( + provider: provider, + compilationMode: compilationMode, + debug: debug, + ); + } +} + +Future _runTests({ + required TestSdkConfigurationProvider provider, + required CompilationMode compilationMode, + required bool debug, +}) async { + final context = + TestContext(TestProject.testExperimentWithSoundNullSafety, provider); + final testInspector = TestInspector(context); + + late VmServiceInterface service; + late Stream stream; + late String isolateId; + late ScriptRef mainScript; + + onBreakPoint(breakPointId, body) => testInspector.onBreakPoint( + stream, + isolateId, + mainScript, + breakPointId, + body, + ); + + getObject(instanceId) => service.getObject(isolateId, instanceId); + + getDisplayedFields(instanceRef) => + testInspector.getDisplayedFields(isolateId, instanceRef); + + getInstanceRef(frame, expression) => + testInspector.getInstanceRef(isolateId, frame, expression); + + getFields(instanceRef, {offset, count, depth = -1}) => + testInspector.getFields( + isolateId, + instanceRef, + offset: offset, + count: count, + depth: depth, + ); + + getElements(String instanceId) => + testInspector.getElements(isolateId, instanceId); + + final matchTypeObject = { + 'hashCode': matchPrimitiveInstanceRef(kind: InstanceKind.kDouble), + 'runtimeType': matchTypeInstanceRef('Type'), + }; + + final matchDisplayedTypeObject = [ + matches('[0-9]*'), + 'Type', + ]; + + group('$compilationMode |', () { + setUpAll(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + compilationMode: compilationMode, + enableExpressionEvaluation: true, + verboseCompiler: debug, + experiments: ['records', 'patterns'], + ); + service = context.debugConnection.vmService; + + final vm = await service.getVM(); + isolateId = vm.isolates!.first.id!; + final scripts = await service.getScripts(isolateId); + + await service.streamListen('Debug'); + stream = service.onEvent('Debug'); + + mainScript = scripts.scripts! + .firstWhere((each) => each.uri!.contains('main.dart')); + }); + + tearDownAll(() async { + await context.tearDown(); + }); + + setUp(() => setCurrentLogWriter(debug: debug)); + tearDown(() => service.resume(isolateId)); + + test('String type', () async { + await onBreakPoint('printSimpleLocalRecord', (event) async { + final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, "'1'.runtimeType"); + expect(instanceRef, matchTypeInstanceRef('String')); + + final instanceId = instanceRef.id!; + final instance = await getObject(instanceId); + expect(instance, matchTypeInstance('String')); + + final classId = instanceRef.classRef!.id; + expect(await getObject(classId), matchTypeClass); + expect(await getFields(instanceRef, depth: 1), matchTypeObject); + expect(await getDisplayedFields(instanceRef), matchDisplayedTypeObject); + }); + }); + + test('int type', () async { + await onBreakPoint('printSimpleLocalRecord', (event) async { + final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, '1.runtimeType'); + expect(instanceRef, matchTypeInstanceRef('int')); + + final instanceId = instanceRef.id!; + final instance = await getObject(instanceId); + expect(instance, matchTypeInstance('int')); + + final classId = instanceRef.classRef!.id; + expect(await getObject(classId), matchTypeClass); + expect(await getFields(instanceRef, depth: 1), matchTypeObject); + expect(await getDisplayedFields(instanceRef), matchDisplayedTypeObject); + }); + }); + + test('list type', () async { + await onBreakPoint('printSimpleLocalRecord', (event) async { + final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, '[].runtimeType'); + expect(instanceRef, matchTypeInstanceRef('List')); + + final instanceId = instanceRef.id!; + final instance = await getObject(instanceId); + expect(instance, matchTypeInstance('List')); + + final classId = instanceRef.classRef!.id; + expect(await getObject(classId), matchTypeClass); + expect(await getFields(instanceRef, depth: 1), matchTypeObject); + expect(await getDisplayedFields(instanceRef), matchDisplayedTypeObject); + }); + }); + + test('map type', () async { + await onBreakPoint('printSimpleLocalRecord', (event) async { + final frame = event.topFrame!.index!; + final instanceRef = + await getInstanceRef(frame, '{}.runtimeType'); + expect(instanceRef, matchTypeInstanceRef('IdentityMap')); + + final instanceId = instanceRef.id!; + final instance = await getObject(instanceId); + expect(instance, matchTypeInstance('IdentityMap')); + + final classId = instanceRef.classRef!.id; + expect(await getObject(classId), matchTypeClass); + expect(await getFields(instanceRef, depth: 1), matchTypeObject); + expect(await getDisplayedFields(instanceRef), matchDisplayedTypeObject); + }); + }); + + test('set type', () async { + await onBreakPoint('printSimpleLocalRecord', (event) async { + final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, '{}.runtimeType'); + expect(instanceRef, matchTypeInstanceRef('_IdentityHashSet')); + + final instanceId = instanceRef.id!; + final instance = await getObject(instanceId); + expect(instance, matchTypeInstance('_IdentityHashSet')); + + final classId = instanceRef.classRef!.id; + expect(await getObject(classId), matchTypeClass); + expect(await getFields(instanceRef, depth: 1), matchTypeObject); + expect(await getDisplayedFields(instanceRef), matchDisplayedTypeObject); + }); + }); + + test('record type', () async { + await onBreakPoint('printSimpleLocalRecord', (event) async { + final frame = event.topFrame!.index!; + final instanceRef = await getInstanceRef(frame, "(0,'a').runtimeType"); + expect(instanceRef, matchRecordTypeInstanceRef(length: 2)); + + final instanceId = instanceRef.id!; + final instance = await getObject(instanceId); + expect(instance, matchRecordTypeInstance(length: 2)); + expect( + await getElements(instanceId), + [matchTypeInstance('int'), matchTypeInstance('String')], + ); + + final classId = instanceRef.classRef!.id; + expect(await getObject(classId), matchRecordTypeClass); + expect( + await getFields(instanceRef, depth: 2), + {1: matchTypeObject, 2: matchTypeObject}, + ); + expect( + await getDisplayedFields(instanceRef), + ['int', 'String'], + ); + }); + }); + + test('class type', () async { + await onBreakPoint('printSimpleLocalRecord', (event) async { + final frame = event.topFrame!.index!; + final instanceRef = + await getInstanceRef(frame, "Uri.file('').runtimeType"); + expect(instanceRef, matchTypeInstanceRef('_Uri')); + + final instanceId = instanceRef.id!; + final instance = await getObject(instanceId); + expect(instance, matchTypeInstance('_Uri')); + + final classId = instanceRef.classRef!.id; + expect(await getObject(classId), matchTypeClass); + expect(await getFields(instanceRef, depth: 1), matchTypeObject); + expect(await getDisplayedFields(instanceRef), matchDisplayedTypeObject); + }); + }); + }); +} diff --git a/dwds/test/metadata/class_test.dart b/dwds/test/metadata/class_test.dart index 49b4e4b96..5b44890f4 100644 --- a/dwds/test/metadata/class_test.dart +++ b/dwds/test/metadata/class_test.dart @@ -9,16 +9,22 @@ import 'package:test/test.dart'; void main() { test('Gracefully handles invalid length objects', () async { - var metadata = ClassMetaData(length: null); + createMetadata(dynamic length) => ClassMetaData( + length: length, + runtimeKind: RuntimeObjectKind.object, + classRef: classRefForUnknown, + ); + + var metadata = createMetadata(null); expect(metadata.length, isNull); - metadata = ClassMetaData(length: {}); + metadata = createMetadata({}); expect(metadata.length, isNull); - metadata = ClassMetaData(length: '{}'); + metadata = createMetadata('{}'); expect(metadata.length, isNull); - metadata = ClassMetaData(length: 0); + metadata = createMetadata(0); expect(metadata.length, equals(0)); }); }