8b0b57844b
BUG= R=jacobr@google.com Review URL: https://codereview.chromium.org/2208533003 .
848 lines
27 KiB
Dart
848 lines
27 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:_runtime' as dart;
|
|
import 'dart:core';
|
|
import 'dart:collection';
|
|
import 'dart:html' as html;
|
|
import 'dart:math';
|
|
|
|
/// 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 = const JsonMLConfig("none");
|
|
static const skipDart = const JsonMLConfig("skipDart");
|
|
static const keyToString = const JsonMLConfig("keyToString");
|
|
static const asClass = const JsonMLConfig("asClass");
|
|
}
|
|
|
|
int _maxSpanLength = 100;
|
|
var _devtoolsFormatter = new JsonMLFormatter(new DartFormatter());
|
|
|
|
String _typeof(object) => JS('String', 'typeof #', object);
|
|
|
|
List<String> getOwnPropertyNames(object) => JS('List<String>',
|
|
'dart.list(Object.getOwnPropertyNames(#), #)', object, String);
|
|
|
|
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(new NameValuePair(
|
|
name: getTypeName(_getType(object)),
|
|
value: object,
|
|
config: JsonMLConfig.asClass));
|
|
}
|
|
|
|
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 type) {
|
|
var name = dart.typeName(type);
|
|
// Hack to cleanup names for List<dynamic>
|
|
// 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.
|
|
if (name == 'JSArray<dynamic>' || name == 'JSObject<Array>')
|
|
return 'List<dynamic>';
|
|
return name;
|
|
}
|
|
|
|
Object _getType(object) =>
|
|
object is Type ? object : dart.getReifiedType(object);
|
|
|
|
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.
|
|
operator ==(other) => other is NameValuePair && 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(
|
|
new NameValuePair(name: (i + start).toString(), value: element));
|
|
});
|
|
} else {
|
|
for (var i = start; i < end; i += subsetSize) {
|
|
var subSpan = new IterableSpan(i, min(end, subsetSize + i), iterable);
|
|
if (subSpan.length == 1) {
|
|
children.add(new NameValuePair(
|
|
name: i.toString(), value: iterable.elementAt(i)));
|
|
} else {
|
|
children.add(new 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) => new 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 = new 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);
|
|
// Treat Node objects as a native JavaScript type as the regular DOM render
|
|
// in devtools is superior to the dart specific view.
|
|
return (type != 'object' && type != 'function') ||
|
|
object is dart.JSObject ||
|
|
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 = new JsonMLElement('span')
|
|
..setStyle('background-color: #d9edf7;')
|
|
..createTextChild(c);
|
|
return element.toJsonML();
|
|
}
|
|
|
|
bool hasBody(object, config) => _simpleFormatter.hasChildren(object, config);
|
|
|
|
body(object, config) {
|
|
var body = new 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('color: rgb(196, 26, 22);');
|
|
}
|
|
var children = _simpleFormatter.children(object, config);
|
|
for (NameValuePair child in children) {
|
|
var li = body.createChild('li');
|
|
var nameSpan = new JsonMLElement('span')
|
|
..createTextChild(
|
|
child.displayName.isNotEmpty ? '${child.displayName}: ' : '')
|
|
..setStyle('color: rgb(136, 19, 145);');
|
|
if (_typeof(child.value) == 'object' ||
|
|
_typeof(child.value) == 'function') {
|
|
nameSpan.addStyle("padding-left: 13px;");
|
|
|
|
li.appendChild(nameSpan);
|
|
var objectTag = li.createObjectTag(child.value);
|
|
objectTag.addAttribute('config', child.config);
|
|
if (!_simpleFormatter.hasChildren(child.value, child.config)) {
|
|
li.setStyle("padding-left: 13px;");
|
|
}
|
|
} else {
|
|
li.setStyle("padding-left: 13px;");
|
|
li.createChild('span')
|
|
..appendChild(nameSpan)
|
|
..createTextChild(safePreview(child.value, child.config));
|
|
}
|
|
}
|
|
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 = [
|
|
new ClassFormatter(),
|
|
new NamedConstructorFormatter(),
|
|
new MapFormatter(),
|
|
new IterableFormatter(),
|
|
new IterableSpanFormatter(),
|
|
new MapEntryFormatter(),
|
|
new StackTraceFormatter(),
|
|
new FunctionFormatter(),
|
|
new HeritageClauseFormatter(),
|
|
new LibraryModuleFormatter(),
|
|
new LibraryFormatter(),
|
|
new 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 {
|
|
static Set<String> _customNames = new Set()
|
|
..add('constructor')
|
|
..add('prototype')
|
|
..add('__proto__');
|
|
bool accept(object, config) => !isNativeJavaScriptObject(object);
|
|
|
|
String preview(object) => getObjectTypeName(object);
|
|
|
|
bool hasChildren(object) => true;
|
|
|
|
List<NameValuePair> children(object) {
|
|
var properties = new LinkedHashSet<NameValuePair>();
|
|
// Set of property names used to avoid duplicates.
|
|
addMetadataChildren(object, properties);
|
|
|
|
var current = object;
|
|
|
|
var protoChain = <Object>[];
|
|
while (current != null &&
|
|
!isNativeJavaScriptObject(current) &&
|
|
JS("bool", "# !== Object.prototype", current)) {
|
|
protoChain.add(current);
|
|
current = safeGetProperty(current, '__proto__');
|
|
}
|
|
|
|
// We walk the prototype chain for symbol properties because they take
|
|
// priority and are accessed instead of Dart properties according to Dart
|
|
// calling conventions.
|
|
// TODO(jacobr): where possible use the data stored by dart.setSignature
|
|
// instead of walking the JavaScript object directly.
|
|
for (current in protoChain) {
|
|
for (var symbol in getOwnPropertySymbols(current)) {
|
|
var dartName = symbolName(symbol);
|
|
if (hasMethod(object, dartName)) {
|
|
continue;
|
|
}
|
|
// TODO(jacobr): find a cleaner solution than checking for dartx
|
|
String dartXPrefix = 'dartx.';
|
|
if (dartName.startsWith(dartXPrefix)) {
|
|
dartName = dartName.substring(dartXPrefix.length);
|
|
} else if (!dartName.startsWith('_')) {
|
|
// Dart method extension names should either be from dartx or should
|
|
// start with an _
|
|
continue;
|
|
}
|
|
var value = safeGetProperty(object, symbol);
|
|
properties.add(new NameValuePair(name: dartName, value: value));
|
|
}
|
|
}
|
|
|
|
for (current in protoChain) {
|
|
// TODO(jacobr): optionally distinguish properties and fields so that
|
|
// it is safe to expand untrusted objects without side effects.
|
|
var className = dart.getReifiedType(current).name;
|
|
for (var name in getOwnPropertyNames(current)) {
|
|
if (_customNames.contains(name) || name == className) continue;
|
|
if (hasMethod(object, name)) {
|
|
continue;
|
|
}
|
|
var value = safeGetProperty(object, name);
|
|
properties.add(new NameValuePair(name: name, value: value));
|
|
}
|
|
}
|
|
|
|
return properties.toList();
|
|
}
|
|
}
|
|
|
|
/// Formatter for module Dart Library objects.
|
|
class LibraryModuleFormatter implements Formatter {
|
|
accept(object, config) => dart.getDartLibraryName(object) != null;
|
|
|
|
bool hasChildren(object) => true;
|
|
|
|
String preview(object) {
|
|
var libraryNames = dart.getDartLibraryName(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[libraryNames.length - 1] = '';
|
|
}
|
|
return 'Library Module: ${libraryNames.join('/')}';
|
|
}
|
|
|
|
List<NameValuePair> children(object) {
|
|
var children = new LinkedHashSet<NameValuePair>();
|
|
for (var name in getOwnPropertyNames(object)) {
|
|
var value = safeGetProperty(object, name);
|
|
// Replace __ with / to make file paths more readable. Then
|
|
// 'src__result__error' becomes 'src/result/error'.
|
|
name = '${name.replaceAll("__", "/")}.dart';
|
|
children.add(new NameValuePair(
|
|
name: name, value: new Library(name, value), hideName: true));
|
|
}
|
|
return children.toList();
|
|
}
|
|
}
|
|
|
|
class LibraryFormatter implements Formatter {
|
|
var genericParameters = new HashMap<String, String>();
|
|
|
|
accept(object, config) => object is Library;
|
|
|
|
bool hasChildren(object) => true;
|
|
|
|
String preview(object) => object.name;
|
|
|
|
List<NameValuePair> children(object) {
|
|
var children = new LinkedHashSet<NameValuePair>();
|
|
var nonGenericProperties = new LinkedHashMap<String, Object>();
|
|
var objectProperties = safeProperties(object.object);
|
|
objectProperties.forEach((name, value) {
|
|
var genericTypeConstructor = dart.getGenericTypeCtor(value);
|
|
if (genericTypeConstructor != null) {
|
|
recordGenericParameters(name, genericTypeConstructor);
|
|
} else {
|
|
nonGenericProperties[name] = value;
|
|
}
|
|
});
|
|
nonGenericProperties.forEach((name, value) {
|
|
if (value is Type) {
|
|
children.add(classChild(name, value));
|
|
} else {
|
|
children.add(new NameValuePair(name: name, value: value));
|
|
}
|
|
});
|
|
return children.toList();
|
|
}
|
|
|
|
recordGenericParameters(String name, Object genericTypeConstructor) {
|
|
// Using JS toString() eliminates the leading metadata that is generated
|
|
// with the toString function provided in operations.dart.
|
|
// Splitting by => and taking the first element gives the list of
|
|
// arguments in the constructor.
|
|
genericParameters[name] =
|
|
JS('String', '#.toString()', genericTypeConstructor)
|
|
.split(' =>')
|
|
.first
|
|
.replaceAll(new RegExp(r'[(|)]'), '');
|
|
}
|
|
|
|
classChild(String name, Object child) {
|
|
var typeName = getTypeName(child);
|
|
// Generic class names are generated with a $ at the end, so the
|
|
// corresponding non-generic class can be identified by adding $.
|
|
var parameterName = '$name\$';
|
|
if (genericParameters.keys.contains(parameterName)) {
|
|
typeName = '$typeName<${genericParameters[parameterName]}>';
|
|
// TODO(bmilligan): Add a symbol to classes with generic types at their
|
|
// creation so they can be recognized independently by the debugger.
|
|
JSNative.setProperty(child, 'genericTypeName', typeName);
|
|
}
|
|
return new NameValuePair(name: typeName, value: child);
|
|
}
|
|
}
|
|
|
|
/// 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) {
|
|
return dart.typeName(dart.getReifiedType(object));
|
|
}
|
|
|
|
List<NameValuePair> children(object) => <NameValuePair>[
|
|
new NameValuePair(name: 'signature', value: preview(object)),
|
|
new NameValuePair(
|
|
name: 'JavaScript Function',
|
|
value: object,
|
|
config: JsonMLConfig.skipDart)
|
|
];
|
|
}
|
|
|
|
/// Formatter for Dart Map objects.
|
|
class MapFormatter implements Formatter {
|
|
accept(object, config) => object is Map;
|
|
|
|
bool hasChildren(object) => true;
|
|
|
|
String preview(object) {
|
|
Map map = object;
|
|
return '${getObjectTypeName(map)} length ${map.length}';
|
|
}
|
|
|
|
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 = new LinkedHashSet<NameValuePair>();
|
|
map.forEach((key, value) {
|
|
var entryWrapper = new MapEntry(key: key, value: value);
|
|
entries.add(new 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 = new LinkedHashSet<NameValuePair>();
|
|
children.addAll(new 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>[
|
|
new 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>[
|
|
new NameValuePair(
|
|
name: 'key', value: object.key, config: JsonMLConfig.keyToString),
|
|
new 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(new 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();
|
|
}
|
|
|
|
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) => new NameValuePair(
|
|
value: line.replaceFirst(new RegExp(r'^\s+at\s'), ''),
|
|
hideName: true))
|
|
.toList();
|
|
}
|
|
|
|
class ClassFormatter implements Formatter {
|
|
accept(object, config) => object is Type || config == JsonMLConfig.asClass;
|
|
|
|
String preview(object) {
|
|
var typeName = safeGetProperty(object, 'genericTypeName');
|
|
if (typeName != null) return typeName;
|
|
var type = _getType(object);
|
|
var implements = dart.getImplements(type);
|
|
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(object) {
|
|
// TODO(jacobr): add other entries describing the class such as
|
|
// links to the superclass, mixins, implemented interfaces, and methods.
|
|
var type = _getType(object);
|
|
var children = <NameValuePair>[];
|
|
var typeName = getTypeName(_getType(object));
|
|
var mixins = dart.getMixins(type);
|
|
if (mixins != null && mixins.isNotEmpty) {
|
|
children.add(new NameValuePair(
|
|
name: '[[Mixins]]', value: new HeritageClause('mixins', mixins)));
|
|
}
|
|
|
|
var hiddenProperties = ['length', 'name', 'prototype', 'genericTypeName'];
|
|
// Addition of NameValuePairs for static variables and named constructors.
|
|
for (var name in getOwnPropertyNames(object)) {
|
|
// TODO(bmilligan): Perform more principled checks to filter out spurious
|
|
// members.
|
|
if (hiddenProperties.contains(name)) continue;
|
|
var value = safeGetProperty(object, name);
|
|
if (value != null && dart.getIsNamedConstructor(value) != null) {
|
|
value = new NamedConstructor(value);
|
|
name = '${typeName}.$name';
|
|
}
|
|
children.add(new NameValuePair(name: name, value: value));
|
|
}
|
|
|
|
// TODO(bmilligan): Replace the hard coding of $identityHash.
|
|
var hiddenPrototypeProperties = ['constructor', 'new', r'$identityHash'];
|
|
// Addition of class methods.
|
|
var prototype = JS('var', '#["prototype"]', object);
|
|
if (prototype != null) {
|
|
for (var name in getOwnPropertyNames(prototype)) {
|
|
if (hiddenPrototypeProperties.contains(name)) continue;
|
|
// Simulate dart.bind by using dart.tag and tear off the function
|
|
// so it will be recognized by the FunctionFormatter.
|
|
var function = safeGetProperty(prototype, name);
|
|
var constructor = safeGetProperty(prototype, 'constructor');
|
|
var sigObj = dart.getMethodSig(constructor);
|
|
if (sigObj != null) {
|
|
var value = safeGetProperty(sigObj, name);
|
|
if (getTypeName(dart.getReifiedType(value)) != 'Null') {
|
|
dart.tag(function, value);
|
|
children.add(new NameValuePair(name: name, value: function));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return children;
|
|
}
|
|
}
|
|
|
|
/// This entry point is automatically invoked by the code generated by
|
|
/// Dart Dev Compiler
|
|
registerDevtoolsFormatter() {
|
|
var formatters = [_devtoolsFormatter];
|
|
JS('', 'dart.global.devtoolsFormatters = #', formatters);
|
|
}
|