f3bd910e10
For classes that implement Map, using MapFormatter can result in many, many custom format operations. Also, the presentation is not that useful. We get e.g. a list of indices that then have a name and value beside them, so it just duplicates the Object view, but with extra non-useful data. Change-Id: I74ed34521778bddae9cdad975339925d7c181bf3 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/100282 Commit-Queue: Alan Knight <alanknight@google.com> Reviewed-by: Jacob Richman <jacobr@google.com>
1017 lines
32 KiB
Dart
1017 lines
32 KiB
Dart
// Copyright (c) 2015, 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.
|
|
|
|
library dart._debugger;
|
|
|
|
import 'dart:_foreign_helper' show JS;
|
|
import 'dart:_interceptors' show JSArray;
|
|
import 'dart:_js_helper' show InternalMap;
|
|
import 'dart:_runtime' as dart;
|
|
import 'dart:core';
|
|
import 'dart:collection';
|
|
import 'dart:html' as html;
|
|
import 'dart:math';
|
|
|
|
part 'profile.dart';
|
|
|
|
/// JsonMLConfig object to pass to devtools to specify how an Object should
|
|
/// be displayed. skipDart signals that an object should not be formatted
|
|
/// by the Dart formatter. This is used to specify that an Object
|
|
/// should just be displayed using the regular JavaScript view instead of a
|
|
/// custom Dart view. For example, this is used to display the JavaScript view
|
|
/// of a Dart Function as a child of the regular Function object. keyToString
|
|
/// signals that a map key object should have its toString() displayed by
|
|
/// the Dart formatter.
|
|
///
|
|
/// We'd like this to be an enum, but we can't because it's a dev_compiler bug.
|
|
class JsonMLConfig {
|
|
const JsonMLConfig(this.name);
|
|
|
|
final String name;
|
|
static const none = JsonMLConfig("none");
|
|
static const skipDart = JsonMLConfig("skipDart");
|
|
static const keyToString = JsonMLConfig("keyToString");
|
|
static const asClass = JsonMLConfig("asClass");
|
|
static const asObject = JsonMLConfig("asObject");
|
|
static const asMap = JsonMLConfig("asMap");
|
|
toString() => "JsonMLConfig($name)";
|
|
}
|
|
|
|
int _maxSpanLength = 100;
|
|
var _devtoolsFormatter = JsonMLFormatter(DartFormatter());
|
|
|
|
/// We truncate a toString() longer than [maxStringLength].
|
|
int maxFormatterStringLength = 100;
|
|
|
|
String _typeof(object) => JS('String', 'typeof #', object);
|
|
|
|
List<String> getOwnPropertyNames(object) =>
|
|
JSArray<String>.of(dart.getOwnPropertyNames(object));
|
|
|
|
List getOwnPropertySymbols(object) =>
|
|
JS('List', 'Object.getOwnPropertySymbols(#)', object);
|
|
|
|
// TODO(jacobr): move this to dart:js and fully implement.
|
|
class JSNative {
|
|
// Name may be a String or a Symbol.
|
|
static getProperty(object, name) => JS('', '#[#]', object, name);
|
|
// Name may be a String or a Symbol.
|
|
static setProperty(object, name, value) =>
|
|
JS('', '#[#]=#', object, name, value);
|
|
}
|
|
|
|
void addMetadataChildren(object, Set<NameValuePair> ret) {
|
|
ret.add(NameValuePair(
|
|
name: "[[class]]",
|
|
value: dart.getReifiedType(object),
|
|
config: JsonMLConfig.asClass));
|
|
}
|
|
|
|
/// Add properties from a signature definition [sig] for [object].
|
|
/// Walk the prototype chain if [walkProtypeChain] is set.
|
|
/// Tag types on function typed properties of [object] if [tagTypes] is set.
|
|
///
|
|
void addPropertiesFromSignature(
|
|
sig, Set<NameValuePair> properties, object, bool walkPrototypeChain,
|
|
{tagTypes = false}) {
|
|
// Including these property names doesn't add any value and just clutters
|
|
// the debugger output.
|
|
// TODO(jacobr): consider adding runtimeType to this list.
|
|
var skippedNames = Set()..add('hashCode');
|
|
var objectPrototype = JS('', 'Object.prototype');
|
|
while (sig != null && !identical(sig, objectPrototype)) {
|
|
for (var symbol in getOwnPropertySymbols(sig)) {
|
|
var dartName = symbolName(symbol);
|
|
String dartXPrefix = 'dartx.';
|
|
if (dartName.startsWith(dartXPrefix)) {
|
|
dartName = dartName.substring(dartXPrefix.length);
|
|
}
|
|
if (skippedNames.contains(dartName)) continue;
|
|
var value = safeGetProperty(object, symbol);
|
|
// Tag the function with its runtime type.
|
|
if (tagTypes && _typeof(value) == 'function') {
|
|
dart.fn(value, JS('', '#[#]', sig, symbol));
|
|
}
|
|
properties.add(NameValuePair(name: dartName, value: value));
|
|
}
|
|
|
|
for (var name in getOwnPropertyNames(sig)) {
|
|
var value = safeGetProperty(object, name);
|
|
if (skippedNames.contains(name)) continue;
|
|
// Tag the function with its runtime type.
|
|
if (tagTypes && _typeof(value) == 'function') {
|
|
dart.fn(value, JS('', '#[#]', sig, name));
|
|
}
|
|
properties.add(NameValuePair(name: name, value: value));
|
|
}
|
|
|
|
if (!walkPrototypeChain) break;
|
|
|
|
sig = dart.getPrototypeOf(sig);
|
|
}
|
|
}
|
|
|
|
/// Sort properties sorting public names before private names.
|
|
List<NameValuePair> sortProperties(Iterable<NameValuePair> properties) {
|
|
var sortedProperties = properties.toList();
|
|
|
|
sortedProperties.sort((a, b) {
|
|
var aPrivate = a.name.startsWith('_');
|
|
var bPrivate = b.name.startsWith('_');
|
|
if (aPrivate != bPrivate) return aPrivate ? 1 : -1;
|
|
return a.name.compareTo(b.name);
|
|
});
|
|
return sortedProperties;
|
|
}
|
|
|
|
String getObjectTypeName(object) {
|
|
var reifiedType = dart.getReifiedType(object);
|
|
if (reifiedType == null) {
|
|
if (_typeof(object) == 'function') {
|
|
return '[[Raw JavaScript Function]]';
|
|
}
|
|
return '<Error getting type name>';
|
|
}
|
|
return getTypeName(reifiedType);
|
|
}
|
|
|
|
String getTypeName(type) {
|
|
// TODO(jacobr): it would be nice if there was a way we could distinguish
|
|
// between a List<dynamic> created from Dart and an Array passed in from
|
|
// JavaScript.
|
|
return dart.typeName(type);
|
|
}
|
|
|
|
String safePreview(object, config) {
|
|
try {
|
|
var preview = _devtoolsFormatter._simpleFormatter.preview(object, config);
|
|
if (preview != null) return preview;
|
|
return object.toString();
|
|
} catch (e) {
|
|
return '<Exception thrown> $e';
|
|
}
|
|
}
|
|
|
|
String symbolName(symbol) {
|
|
var name = symbol.toString();
|
|
assert(name.startsWith('Symbol('));
|
|
return name.substring('Symbol('.length, name.length - 1);
|
|
}
|
|
|
|
bool hasMethod(object, String name) {
|
|
try {
|
|
return dart.hasMethod(object, name);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// [JsonMLFormatter] consumes [NameValuePair] objects and
|
|
class NameValuePair {
|
|
NameValuePair(
|
|
{this.name,
|
|
this.value,
|
|
this.config = JsonMLConfig.none,
|
|
this.hideName = false});
|
|
|
|
// Define equality and hashCode so that NameValuePair can be used
|
|
// in a Set to dedupe entries with duplicate names.
|
|
bool operator ==(other) {
|
|
if (other is! NameValuePair) return false;
|
|
if (this.hideName || other.hideName) return identical(this, other);
|
|
return other.name == name;
|
|
}
|
|
|
|
int get hashCode => name.hashCode;
|
|
|
|
final String name;
|
|
final Object value;
|
|
final JsonMLConfig config;
|
|
final bool hideName;
|
|
|
|
String get displayName => hideName ? '' : name;
|
|
}
|
|
|
|
class MapEntry {
|
|
MapEntry({this.key, this.value});
|
|
|
|
final Object key;
|
|
final Object value;
|
|
}
|
|
|
|
class IterableSpan {
|
|
IterableSpan(this.start, this.end, this.iterable);
|
|
|
|
final int start;
|
|
final int end;
|
|
final Iterable iterable;
|
|
int get length => end - start;
|
|
|
|
/// Using length - .5, a list of length 10000 results in a
|
|
/// maxPowerOfSubsetSize of 1, so the list will be broken up into 100,
|
|
/// 100-length subsets. A list of length 10001 results in a
|
|
/// maxPowerOfSubsetSize of 2, so the list will be broken up into 1
|
|
/// 10000-length subset and 1 1-length subset.
|
|
int get maxPowerOfSubsetSize =>
|
|
(log(length - .5) / log(_maxSpanLength)).truncate();
|
|
int get subsetSize => pow(_maxSpanLength, maxPowerOfSubsetSize);
|
|
|
|
Map<int, dynamic> asMap() =>
|
|
iterable.skip(start).take(length).toList().asMap();
|
|
|
|
List<NameValuePair> children() {
|
|
var children = <NameValuePair>[];
|
|
if (length <= _maxSpanLength) {
|
|
asMap().forEach((i, element) {
|
|
children
|
|
.add(NameValuePair(name: (i + start).toString(), value: element));
|
|
});
|
|
} else {
|
|
for (var i = start; i < end; i += subsetSize) {
|
|
var subSpan = IterableSpan(i, min(end, subsetSize + i), iterable);
|
|
if (subSpan.length == 1) {
|
|
children.add(
|
|
NameValuePair(name: i.toString(), value: iterable.elementAt(i)));
|
|
} else {
|
|
children.add(NameValuePair(
|
|
name: '[${i}...${subSpan.end - 1}]',
|
|
value: subSpan,
|
|
hideName: true));
|
|
}
|
|
}
|
|
}
|
|
return children;
|
|
}
|
|
}
|
|
|
|
class Library {
|
|
Library(this.name, this.object);
|
|
|
|
final String name;
|
|
final Object object;
|
|
}
|
|
|
|
class NamedConstructor {
|
|
NamedConstructor(this.object);
|
|
|
|
final Object object;
|
|
}
|
|
|
|
class HeritageClause {
|
|
HeritageClause(this.name, this.types);
|
|
|
|
final String name;
|
|
final List types;
|
|
}
|
|
|
|
Object safeGetProperty(Object protoChain, Object name) {
|
|
try {
|
|
return JSNative.getProperty(protoChain, name);
|
|
} catch (e) {
|
|
return '<Exception thrown> $e';
|
|
}
|
|
}
|
|
|
|
safeProperties(object) => Map.fromIterable(
|
|
getOwnPropertyNames(object)
|
|
.where((each) => safeGetProperty(object, each) != null),
|
|
key: (name) => name,
|
|
value: (name) => safeGetProperty(object, name));
|
|
|
|
/// Class to simplify building the JsonML objects expected by the
|
|
/// Devtools Formatter API.
|
|
class JsonMLElement {
|
|
dynamic _attributes;
|
|
List _jsonML;
|
|
|
|
JsonMLElement(tagName) {
|
|
_attributes = JS('', '{}');
|
|
_jsonML = [tagName, _attributes];
|
|
}
|
|
|
|
appendChild(element) {
|
|
_jsonML.add(element.toJsonML());
|
|
}
|
|
|
|
JsonMLElement createChild(String tagName) {
|
|
var c = JsonMLElement(tagName);
|
|
_jsonML.add(c.toJsonML());
|
|
return c;
|
|
}
|
|
|
|
JsonMLElement createObjectTag(object) =>
|
|
createChild('object')..addAttribute('object', object);
|
|
|
|
void setStyle(String style) {
|
|
_attributes.style = style;
|
|
}
|
|
|
|
addStyle(String style) {
|
|
if (_attributes.style == null) {
|
|
_attributes.style = style;
|
|
} else {
|
|
_attributes.style += style;
|
|
}
|
|
}
|
|
|
|
addAttribute(key, value) {
|
|
JSNative.setProperty(_attributes, key, value);
|
|
}
|
|
|
|
createTextChild(String text) {
|
|
_jsonML.add(text);
|
|
}
|
|
|
|
toJsonML() => _jsonML;
|
|
}
|
|
|
|
/// Whether an object is a native JavaScript type where we should display the
|
|
/// JavaScript view of the object instead of the custom Dart specific render
|
|
/// of properties.
|
|
bool isNativeJavaScriptObject(object) {
|
|
var type = _typeof(object);
|
|
if (type != 'object' && type != 'function') return true;
|
|
|
|
// Consider all regular JS objects that do not represent Dart modules native
|
|
// JavaScript objects.
|
|
if (dart.isJsInterop(object) && dart.getModuleName(object) == null) {
|
|
return true;
|
|
}
|
|
|
|
// Treat Node objects as a native JavaScript type as the regular DOM render
|
|
// in devtools is superior to the dart specific view.
|
|
return object is html.Node;
|
|
}
|
|
|
|
/// Class implementing the Devtools Formatter API described by:
|
|
/// https://docs.google.com/document/d/1FTascZXT9cxfetuPRT2eXPQKXui4nWFivUnS_335T3U
|
|
/// Specifically, a formatter implements a header, hasBody, and body method.
|
|
/// This class renders the simple structured format objects [_simpleFormatter]
|
|
/// provides as JsonML.
|
|
class JsonMLFormatter {
|
|
// TODO(jacobr): define a SimpleFormatter base class that DartFormatter
|
|
// implements if we decide to use this class elsewhere. We specify that the
|
|
// type is DartFormatter here purely to get type checking benefits not because
|
|
// this class is really intended to only support instances of type
|
|
// DartFormatter.
|
|
DartFormatter _simpleFormatter;
|
|
|
|
bool customFormattersOn = false;
|
|
|
|
JsonMLFormatter(this._simpleFormatter);
|
|
|
|
void setMaxSpanLengthForTestingOnly(int spanLength) {
|
|
_maxSpanLength = spanLength;
|
|
}
|
|
|
|
header(object, config) {
|
|
customFormattersOn = true;
|
|
if (config == JsonMLConfig.skipDart || isNativeJavaScriptObject(object)) {
|
|
return null;
|
|
}
|
|
var c = _simpleFormatter.preview(object, config);
|
|
if (c == null) return null;
|
|
|
|
if (config == JsonMLConfig.keyToString) {
|
|
c = object.toString();
|
|
}
|
|
|
|
// Indicate this is a Dart Object by using a Dart background color.
|
|
// This is stylistically a bit ugly but it eases distinguishing Dart and
|
|
// JS objects.
|
|
var element = JsonMLElement('span')
|
|
..setStyle('background-color: #d9edf7;color: black')
|
|
..createTextChild(c);
|
|
return element.toJsonML();
|
|
}
|
|
|
|
bool hasBody(object, config) => _simpleFormatter.hasChildren(object, config);
|
|
|
|
body(object, config) {
|
|
var body = JsonMLElement('ol')
|
|
..setStyle('list-style-type: none;'
|
|
'padding-left: 0px;'
|
|
'margin-top: 0px;'
|
|
'margin-bottom: 0px;'
|
|
'margin-left: 12px;');
|
|
if (object is StackTrace) {
|
|
body.addStyle('background-color: thistle;color: rgb(196, 26, 22);');
|
|
}
|
|
var children = _simpleFormatter.children(object, config);
|
|
if (children == null) return body.toJsonML();
|
|
for (NameValuePair child in children) {
|
|
var li = body.createChild('li');
|
|
li.setStyle("padding-left: 13px;");
|
|
|
|
// The value is indented when it is on a different line from the name
|
|
// by setting right padding of the name to -13px and the padding of the
|
|
// value to 13px.
|
|
JsonMLElement nameSpan;
|
|
var valueStyle = '';
|
|
if (!child.hideName) {
|
|
nameSpan = JsonMLElement('span')
|
|
..createTextChild(
|
|
child.displayName.isNotEmpty ? '${child.displayName}: ' : '')
|
|
..setStyle(
|
|
'background-color: thistle; color: rgb(136, 19, 145); margin-right: -13px');
|
|
valueStyle = 'margin-left: 13px';
|
|
}
|
|
|
|
if (_typeof(child.value) == 'object' ||
|
|
_typeof(child.value) == 'function') {
|
|
var valueSpan = JsonMLElement('span')..setStyle(valueStyle);
|
|
valueSpan.createObjectTag(child.value)
|
|
..addAttribute('config', child.config);
|
|
if (nameSpan != null) {
|
|
li.appendChild(nameSpan);
|
|
}
|
|
li.appendChild(valueSpan);
|
|
} else {
|
|
var line = li.createChild('span');
|
|
if (nameSpan != null) {
|
|
line.appendChild(nameSpan);
|
|
}
|
|
line.appendChild(JsonMLElement('span')
|
|
..createTextChild(safePreview(child.value, child.config))
|
|
..setStyle(valueStyle));
|
|
}
|
|
}
|
|
return body.toJsonML();
|
|
}
|
|
}
|
|
|
|
abstract class Formatter {
|
|
bool accept(object, config);
|
|
String preview(object);
|
|
bool hasChildren(object);
|
|
List<NameValuePair> children(object);
|
|
}
|
|
|
|
class DartFormatter {
|
|
List<Formatter> _formatters;
|
|
|
|
DartFormatter() {
|
|
// The order of formatters matters as formatters earlier in the list take
|
|
// precedence.
|
|
_formatters = [
|
|
ObjectInternalsFormatter(),
|
|
ClassFormatter(),
|
|
TypeFormatter(),
|
|
NamedConstructorFormatter(),
|
|
MapFormatter(),
|
|
MapOverviewFormatter(),
|
|
IterableFormatter(),
|
|
IterableSpanFormatter(),
|
|
MapEntryFormatter(),
|
|
StackTraceFormatter(),
|
|
ErrorAndExceptionFormatter(),
|
|
FunctionFormatter(),
|
|
HeritageClauseFormatter(),
|
|
LibraryModuleFormatter(),
|
|
LibraryFormatter(),
|
|
ObjectFormatter(),
|
|
];
|
|
}
|
|
|
|
String preview(object, config) {
|
|
try {
|
|
if (object == null ||
|
|
object is num ||
|
|
object is String ||
|
|
isNativeJavaScriptObject(object)) {
|
|
return object.toString();
|
|
}
|
|
for (var formatter in _formatters) {
|
|
if (formatter.accept(object, config)) return formatter.preview(object);
|
|
}
|
|
} catch (e, trace) {
|
|
// Log formatter internal errors as unfortunately the devtools cannot
|
|
// be used to debug formatter errors.
|
|
html.window.console.error("Caught exception $e\n trace:\n$trace");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
bool hasChildren(object, config) {
|
|
if (object == null) return false;
|
|
try {
|
|
for (var formatter in _formatters) {
|
|
if (formatter.accept(object, config))
|
|
return formatter.hasChildren(object);
|
|
}
|
|
} catch (e, trace) {
|
|
// See comment for preview.
|
|
html.window.console
|
|
.error("[hasChildren] Caught exception $e\n trace:\n$trace");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
List<NameValuePair> children(object, config) {
|
|
try {
|
|
if (object != null) {
|
|
for (var formatter in _formatters) {
|
|
if (formatter.accept(object, config))
|
|
return formatter.children(object);
|
|
}
|
|
}
|
|
} catch (e, trace) {
|
|
// See comment for preview.
|
|
html.window.console.error("Caught exception $e\n trace:\n$trace");
|
|
}
|
|
return <NameValuePair>[];
|
|
}
|
|
}
|
|
|
|
/// Default formatter for Dart Objects.
|
|
class ObjectFormatter extends Formatter {
|
|
bool accept(object, config) => !isNativeJavaScriptObject(object);
|
|
|
|
String preview(object) {
|
|
var typeName = getObjectTypeName(object);
|
|
try {
|
|
// An explicit toString() call might not actually be a string. This way
|
|
// we're sure.
|
|
var toString = "$object";
|
|
if (toString.length > maxFormatterStringLength) {
|
|
toString = toString.substring(0, maxFormatterStringLength - 3) + "...";
|
|
}
|
|
// The default toString() will be "Instance of 'Foo'", in which case we
|
|
// don't need any further indication of the class.
|
|
if (toString.contains(typeName)) {
|
|
return toString;
|
|
} else {
|
|
// If there's no class indication, e.g. an Int64 that just prints as a
|
|
// number, then add the class name.
|
|
return "$toString ($typeName)";
|
|
}
|
|
} catch (e) {}
|
|
// We will only get here if there was an error getting the toString, in
|
|
// which case we just use the type name.
|
|
return typeName;
|
|
}
|
|
|
|
bool hasChildren(object) => true;
|
|
|
|
List<NameValuePair> children(object) {
|
|
var type = dart.getType(object);
|
|
var ret = LinkedHashSet<NameValuePair>();
|
|
// We use a Set rather than a List to avoid duplicates.
|
|
var fields = Set<NameValuePair>();
|
|
addPropertiesFromSignature(dart.getFields(type), fields, object, true);
|
|
var getters = Set<NameValuePair>();
|
|
addPropertiesFromSignature(dart.getGetters(type), getters, object, true);
|
|
ret.addAll(sortProperties(fields));
|
|
ret.addAll(sortProperties(getters));
|
|
addMetadataChildren(object, ret);
|
|
return ret.toList();
|
|
}
|
|
}
|
|
|
|
/// Show the object instance members and a reduced preview.
|
|
///
|
|
/// Used as a sub-entry to show the internals of objects that have a different
|
|
/// primary format. For example, a Map shows the key-value pairs, but this makes
|
|
/// the internals of the map visible for debugging.
|
|
class ObjectInternalsFormatter extends ObjectFormatter {
|
|
bool accept(object, config) =>
|
|
super.accept(object, config) && config == JsonMLConfig.asObject;
|
|
|
|
// A minimal preview because we expect a full preview is already shown in a
|
|
// parent formatter.
|
|
String preview(object) {
|
|
return getObjectTypeName(object);
|
|
}
|
|
}
|
|
|
|
/// Formatter for module Dart Library objects.
|
|
class LibraryModuleFormatter implements Formatter {
|
|
accept(object, config) => dart.getModuleName(object) != null;
|
|
|
|
bool hasChildren(object) => true;
|
|
|
|
String preview(object) {
|
|
var libraryNames = dart.getModuleName(object).split('/');
|
|
// Library names are received with a repeat directory name, so strip the
|
|
// last directory entry here to make the path cleaner. For example, the
|
|
// library "third_party/dart/utf/utf" shoud display as
|
|
// "third_party/dart/utf/".
|
|
if (libraryNames.length > 1 &&
|
|
libraryNames.last == libraryNames[libraryNames.length - 2]) {
|
|
libraryNames[libraryNames.length - 1] = '';
|
|
}
|
|
return 'Library Module: ${libraryNames.join('/')}';
|
|
}
|
|
|
|
List<NameValuePair> children(object) {
|
|
var children = LinkedHashSet<NameValuePair>();
|
|
for (var name in getOwnPropertyNames(object)) {
|
|
var value = safeGetProperty(object, name);
|
|
children.add(NameValuePair(
|
|
name: name, value: Library(name, value), hideName: true));
|
|
}
|
|
return children.toList();
|
|
}
|
|
}
|
|
|
|
class LibraryFormatter implements Formatter {
|
|
var genericParameters = HashMap<String, String>();
|
|
|
|
accept(object, config) => object is Library;
|
|
|
|
bool hasChildren(object) => true;
|
|
|
|
String preview(object) => object.name;
|
|
|
|
List<NameValuePair> children(object) {
|
|
// Maintain library member order rather than sorting members as is the
|
|
// case for class members.
|
|
var children = LinkedHashSet<NameValuePair>();
|
|
var objectProperties = safeProperties(object.object);
|
|
objectProperties.forEach((name, value) {
|
|
// Skip the generic constructors for each class as users are only
|
|
// interested in seeing the actual classes.
|
|
if (dart.getGenericTypeCtor(value) != null) return;
|
|
|
|
children.add(dart.isType(value)
|
|
? classChild(name, value)
|
|
: NameValuePair(name: name, value: value));
|
|
});
|
|
return children.toList();
|
|
}
|
|
|
|
classChild(String name, Object child) {
|
|
var typeName = getTypeName(child);
|
|
return NameValuePair(
|
|
name: typeName, value: child, config: JsonMLConfig.asClass);
|
|
}
|
|
}
|
|
|
|
/// Formatter for Dart Function objects.
|
|
/// Dart functions happen to be regular JavaScript Function objects but
|
|
/// we can distinguish them based on whether they have been tagged with
|
|
/// runtime type information.
|
|
class FunctionFormatter implements Formatter {
|
|
accept(object, config) {
|
|
if (_typeof(object) != 'function') return false;
|
|
return dart.getReifiedType(object) != null;
|
|
}
|
|
|
|
bool hasChildren(object) => true;
|
|
|
|
String preview(object) {
|
|
// The debugger can createa a preview of a FunctionType while it's being
|
|
// constructed (before argument types exist), so we need to catch errors.
|
|
try {
|
|
return dart.typeName(dart.getReifiedType(object));
|
|
} catch (e) {
|
|
return safePreview(object, JsonMLConfig.none);
|
|
}
|
|
}
|
|
|
|
List<NameValuePair> children(object) => <NameValuePair>[
|
|
NameValuePair(name: 'signature', value: preview(object)),
|
|
NameValuePair(
|
|
name: 'JavaScript Function',
|
|
value: object,
|
|
config: JsonMLConfig.skipDart)
|
|
];
|
|
}
|
|
|
|
/// Formatter for Objects that implement Map but are not system Maps.
|
|
///
|
|
/// This shows two sub-views, one for instance fields and one for
|
|
/// Map key/value pairs.
|
|
class MapOverviewFormatter implements Formatter {
|
|
// Because this comes after MapFormatter in the list, internal
|
|
// maps will be picked up by that formatter.
|
|
accept(object, config) => object is Map;
|
|
|
|
bool hasChildren(object) => true;
|
|
|
|
String preview(object) {
|
|
Map map = object;
|
|
try {
|
|
return '${getObjectTypeName(map)}';
|
|
} catch (e) {
|
|
return safePreview(object, JsonMLConfig.none);
|
|
}
|
|
}
|
|
|
|
List<NameValuePair> children(object) => [
|
|
NameValuePair(
|
|
name: "[[instance view]]",
|
|
value: object,
|
|
config: JsonMLConfig.asObject),
|
|
NameValuePair(
|
|
name: "[[entries]]", value: object, config: JsonMLConfig.asMap)
|
|
];
|
|
}
|
|
|
|
/// Formatter for Dart Map objects.
|
|
///
|
|
/// This is only used for internal maps, or when shown as [[entries]]
|
|
/// from MapOverViewFormatter.
|
|
class MapFormatter implements Formatter {
|
|
accept(object, config) =>
|
|
object is InternalMap || config == JsonMLConfig.asMap;
|
|
|
|
bool hasChildren(object) => true;
|
|
|
|
String preview(object) {
|
|
Map map = object;
|
|
try {
|
|
return '${getObjectTypeName(map)} length ${map.length}';
|
|
} catch (e) {
|
|
return safePreview(object, JsonMLConfig.none);
|
|
}
|
|
}
|
|
|
|
List<NameValuePair> children(object) {
|
|
// TODO(jacobr): be lazier about enumerating contents of Maps that are not
|
|
// the build in LinkedHashMap class.
|
|
// TODO(jacobr): handle large Maps better.
|
|
Map map = object;
|
|
var entries = LinkedHashSet<NameValuePair>();
|
|
map.forEach((key, value) {
|
|
var entryWrapper = MapEntry(key: key, value: value);
|
|
entries.add(
|
|
NameValuePair(name: entries.length.toString(), value: entryWrapper));
|
|
});
|
|
addMetadataChildren(object, entries);
|
|
return entries.toList();
|
|
}
|
|
}
|
|
|
|
/// Formatter for Dart Iterable objects including List and Set.
|
|
class IterableFormatter implements Formatter {
|
|
bool accept(object, config) => object is Iterable;
|
|
|
|
String preview(object) {
|
|
Iterable iterable = object;
|
|
try {
|
|
var length = iterable.length;
|
|
return '${getObjectTypeName(iterable)} length $length';
|
|
} catch (_) {
|
|
return '${getObjectTypeName(iterable)}';
|
|
}
|
|
}
|
|
|
|
bool hasChildren(object) => true;
|
|
|
|
List<NameValuePair> children(object) {
|
|
// TODO(jacobr): be lazier about enumerating contents of Iterables that
|
|
// are not the built in Set or List types.
|
|
// TODO(jacobr): handle large Iterables better.
|
|
// TODO(jacobr): consider only using numeric indices
|
|
var children = LinkedHashSet<NameValuePair>();
|
|
children.addAll(IterableSpan(0, object.length, object).children());
|
|
// TODO(jacobr): provide a link to show regular class properties here.
|
|
// required for subclasses of iterable, etc.
|
|
addMetadataChildren(object, children);
|
|
return children.toList();
|
|
}
|
|
}
|
|
|
|
class NamedConstructorFormatter implements Formatter {
|
|
accept(object, config) => object is NamedConstructor;
|
|
|
|
// TODO(bmilligan): Display the signature of the named constructor as the
|
|
// preview.
|
|
String preview(object) => 'Named Constructor';
|
|
|
|
bool hasChildren(object) => true;
|
|
|
|
List<NameValuePair> children(object) => <NameValuePair>[
|
|
NameValuePair(
|
|
name: 'JavaScript Function',
|
|
value: object,
|
|
config: JsonMLConfig.skipDart)
|
|
];
|
|
}
|
|
|
|
/// Formatter for synthetic MapEntry objects used to display contents of a Map
|
|
/// cleanly.
|
|
class MapEntryFormatter implements Formatter {
|
|
accept(object, config) => object is MapEntry;
|
|
|
|
String preview(object) {
|
|
MapEntry entry = object;
|
|
return '${safePreview(entry.key, JsonMLConfig.none)} => ${safePreview(entry.value, JsonMLConfig.none)}';
|
|
}
|
|
|
|
bool hasChildren(object) => true;
|
|
|
|
List<NameValuePair> children(object) => <NameValuePair>[
|
|
NameValuePair(
|
|
name: 'key', value: object.key, config: JsonMLConfig.keyToString),
|
|
NameValuePair(name: 'value', value: object.value)
|
|
];
|
|
}
|
|
|
|
/// Formatter for Dart Iterable objects including List and Set.
|
|
class HeritageClauseFormatter implements Formatter {
|
|
bool accept(object, config) => object is HeritageClause;
|
|
|
|
String preview(object) {
|
|
HeritageClause clause = object;
|
|
var typeNames = clause.types.map(getTypeName);
|
|
return '${clause.name} ${typeNames.join(", ")}';
|
|
}
|
|
|
|
bool hasChildren(object) => true;
|
|
|
|
List<NameValuePair> children(object) {
|
|
HeritageClause clause = object;
|
|
var children = <NameValuePair>[];
|
|
for (var type in clause.types) {
|
|
children.add(NameValuePair(value: type, config: JsonMLConfig.asClass));
|
|
}
|
|
return children;
|
|
}
|
|
}
|
|
|
|
/// Formatter for synthetic IterableSpan objects used to display contents of
|
|
/// an Iterable cleanly.
|
|
class IterableSpanFormatter implements Formatter {
|
|
accept(object, config) => object is IterableSpan;
|
|
|
|
String preview(object) {
|
|
return '[${object.start}...${object.end - 1}]';
|
|
}
|
|
|
|
bool hasChildren(object) => true;
|
|
|
|
List<NameValuePair> children(object) => object.children();
|
|
}
|
|
|
|
/// Formatter for Dart Errors and Exceptions.
|
|
class ErrorAndExceptionFormatter extends ObjectFormatter {
|
|
static final RegExp _pattern = RegExp(r'\d+\:\d+');
|
|
|
|
accept(object, config) => object is Error || object is Exception;
|
|
|
|
bool hasChildren(object) => true;
|
|
|
|
String preview(object) {
|
|
var trace = dart.stackTrace(object);
|
|
// TODO(vsm): Pull our stack mapping logic here. We should aim to
|
|
// provide the first meaningful stack frame.
|
|
var line = '$trace'.split('\n').firstWhere(
|
|
(l) =>
|
|
l.contains(_pattern) &&
|
|
!l.contains('dart:sdk') &&
|
|
!l.contains('dart_sdk'),
|
|
orElse: () => null);
|
|
return line != null ? '${object} at ${line}' : '${object}';
|
|
}
|
|
|
|
List<NameValuePair> children(object) {
|
|
var trace = dart.stackTrace(object);
|
|
var entries = LinkedHashSet<NameValuePair>();
|
|
entries.add(NameValuePair(name: 'stackTrace', value: trace));
|
|
addInstanceMembers(object, entries);
|
|
addMetadataChildren(object, entries);
|
|
return entries.toList();
|
|
}
|
|
|
|
// Add an ObjectFormatter view underneath.
|
|
void addInstanceMembers(object, Set<NameValuePair> ret) {
|
|
ret.add(NameValuePair(
|
|
name: "[[instance members]]",
|
|
value: object,
|
|
config: JsonMLConfig.asObject));
|
|
}
|
|
}
|
|
|
|
class StackTraceFormatter implements Formatter {
|
|
accept(object, config) => object is StackTrace;
|
|
|
|
String preview(object) => 'StackTrace';
|
|
|
|
bool hasChildren(object) => true;
|
|
|
|
// Using the stack_trace formatting would be ideal, but adding the
|
|
// dependency or re-writing the code is too messy, so each line of the
|
|
// StackTrace will be added as its own child.
|
|
List<NameValuePair> children(object) => object
|
|
.toString()
|
|
.split('\n')
|
|
.map((line) => NameValuePair(
|
|
value: line.replaceFirst(RegExp(r'^\s+at\s'), ''), hideName: true))
|
|
.toList();
|
|
}
|
|
|
|
class ClassFormatter implements Formatter {
|
|
accept(object, config) => config == JsonMLConfig.asClass;
|
|
|
|
String preview(type) {
|
|
var implements = dart.getImplements(type);
|
|
var typeName = getTypeName(type);
|
|
if (implements != null) {
|
|
var typeNames = implements().map(getTypeName);
|
|
return '${typeName} implements ${typeNames.join(", ")}';
|
|
} else {
|
|
return typeName;
|
|
}
|
|
}
|
|
|
|
bool hasChildren(object) => true;
|
|
|
|
List<NameValuePair> children(type) {
|
|
// TODO(jacobr): add other entries describing the class such as
|
|
// implemented interfaces, and methods.
|
|
var ret = LinkedHashSet<NameValuePair>();
|
|
|
|
var staticProperties = Set<NameValuePair>();
|
|
var staticMethods = Set<NameValuePair>();
|
|
// Static fields and properties.
|
|
addPropertiesFromSignature(
|
|
dart.getStaticFields(type), staticProperties, type, false);
|
|
addPropertiesFromSignature(
|
|
dart.getStaticGetters(type), staticProperties, type, false);
|
|
// static methods.
|
|
addPropertiesFromSignature(
|
|
dart.getStaticMethods(type), staticMethods, type, false);
|
|
|
|
if (staticProperties.isNotEmpty || staticMethods.isNotEmpty) {
|
|
ret
|
|
..add(NameValuePair(value: '[[Static members]]', hideName: true))
|
|
..addAll(sortProperties(staticProperties))
|
|
..addAll(sortProperties(staticMethods));
|
|
}
|
|
|
|
// instance methods.
|
|
var instanceMethods = Set<NameValuePair>();
|
|
// Instance methods are defined on the prototype not the constructor object.
|
|
addPropertiesFromSignature(dart.getMethods(type), instanceMethods,
|
|
JS('', '#.prototype', type), false,
|
|
tagTypes: true);
|
|
if (instanceMethods.isNotEmpty) {
|
|
ret
|
|
..add(NameValuePair(value: '[[Instance Methods]]', hideName: true))
|
|
..addAll(sortProperties(instanceMethods));
|
|
}
|
|
|
|
var mixin = dart.getMixin(type);
|
|
if (mixin != null) {
|
|
// TODO(jmesserly): this can only be one value.
|
|
ret.add(NameValuePair(
|
|
name: '[[Mixins]]', value: HeritageClause('mixins', [mixin])));
|
|
}
|
|
|
|
var baseProto = JS('', '#.__proto__', type);
|
|
if (baseProto != null && !dart.isJsInterop(baseProto)) {
|
|
ret.add(NameValuePair(
|
|
name: "[[base class]]",
|
|
value: baseProto,
|
|
config: JsonMLConfig.asClass));
|
|
}
|
|
|
|
// TODO(jacobr): add back fields for named constructors.
|
|
return ret.toList();
|
|
}
|
|
}
|
|
|
|
class TypeFormatter implements Formatter {
|
|
accept(object, config) => object is Type;
|
|
|
|
String preview(object) => object.toString();
|
|
|
|
bool hasChildren(object) => false;
|
|
|
|
List<NameValuePair> children(object) => [];
|
|
}
|
|
|
|
typedef String StackTraceMapper(String stackTrace);
|
|
|
|
/// Hook for other parts of the SDK To use to map JS stack traces to Dart
|
|
/// stack traces.
|
|
///
|
|
/// Raw JS stack traces are used if $dartStackTraceUtility has not been
|
|
/// specified.
|
|
StackTraceMapper get stackTraceMapper {
|
|
var _util = JS('', r'#.$dartStackTraceUtility', dart.global_);
|
|
return _util != null ? JS('!', '#.mapper', _util) : null;
|
|
}
|
|
|
|
/// This entry point is automatically invoked by the code generated by
|
|
/// Dart Dev Compiler
|
|
registerDevtoolsFormatter() {
|
|
JS('', '#.devtoolsFormatters = [#]', dart.global_, _devtoolsFormatter);
|
|
}
|
|
|
|
// These methods are exposed here for debugger tests.
|
|
//
|
|
// TODO(jmesserly): these are not exports because there is existing code that
|
|
// calls into them from JS. Currently `dartdevc` always resolves exports at
|
|
// compile time, so there is no need to make exports available at runtime by
|
|
// copying properties. For that reason we cannot use re-export.
|
|
//
|
|
// If these methods are only for tests, we should move them here, or change the
|
|
// tests to call the methods directly on dart:_runtime.
|
|
List<String> getModuleNames() => dart.getModuleNames();
|
|
getModuleLibraries(String name) => dart.getModuleLibraries(name);
|