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