blob: 55f2e8ce7cbc53bcd57fb2ec4322c458316fabb0 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library diagnostics_node;
import 'dart:async';
import 'package:vm_service/vm_service.dart';
import '../ui/fake_flutter/fake_flutter.dart';
import '../ui/icons.dart';
import '../utils.dart';
import 'flutter_widget.dart';
import 'inspector_service.dart';
Map<K, V> _invertMap<K, V>(Map<V, K> inverted) => Map.fromEntries(
inverted.entries.map((entry) => MapEntry(entry.value, entry.key)));
const Map<String, DiagnosticLevel> nameToDiagnosticLevel = {
'hidden': DiagnosticLevel.hidden,
'fine': DiagnosticLevel.fine,
'debug': DiagnosticLevel.debug,
'info': DiagnosticLevel.info,
'warning': DiagnosticLevel.warning,
'hint': DiagnosticLevel.hint,
'summary': DiagnosticLevel.summary,
'error': DiagnosticLevel.error,
'off': DiagnosticLevel.off,
};
final Map<DiagnosticLevel, String> diagnosticLevelToName =
_invertMap(nameToDiagnosticLevel);
const Map<String, DiagnosticsTreeStyle> nameToTreeStyle = {
'sparse': DiagnosticsTreeStyle.sparse,
'offstage': DiagnosticsTreeStyle.offstage,
'dense': DiagnosticsTreeStyle.dense,
'transition': DiagnosticsTreeStyle.transition,
'whitespace': DiagnosticsTreeStyle.whitespace,
'error': DiagnosticsTreeStyle.error,
'flat': DiagnosticsTreeStyle.flat,
'singleLine': DiagnosticsTreeStyle.singleLine,
'errorProperty': DiagnosticsTreeStyle.errorProperty,
'shallow': DiagnosticsTreeStyle.shallow,
'truncateChildren': DiagnosticsTreeStyle.truncateChildren,
};
final Map<DiagnosticsTreeStyle, String> treeStyleToName =
_invertMap(nameToTreeStyle);
/// Defines diagnostics data for a [value].
///
/// [RemoteDiagnosticsNode] provides a high quality multi-line string dump via
/// [toStringDeep]. The core members are the [name], [toDescription],
/// [getProperties], [value], and [getChildren]. All other members exist
/// typically to provide hints for how [toStringDeep] and debugging tools should
/// format output.
///
/// See also:
///
/// * DiagnosticsNode class defined at https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/foundation/diagnostics.dart
/// The difference is the class hierarchy is collapsed here as in
/// package:flutter the subclasses exist more to simplify creation
/// of Diagnostics than because the class hierarchy of Diagnostics is
/// important. If you need to determine the exact Diagnostic class on the
/// Dart side you can use the value of type. The raw Dart object value is
/// also available via the getValue() method.
class RemoteDiagnosticsNode extends DiagnosticableTree {
RemoteDiagnosticsNode(
this.json,
this.inspectorService,
this.isProperty,
this.parent,
);
static final CustomIconMaker iconMaker = CustomIconMaker();
/// This node's parent (if it's been set).
RemoteDiagnosticsNode parent;
Future<String> propertyDocFuture;
List<RemoteDiagnosticsNode> cachedProperties;
/// Service used to retrieve more detailed information about the value of
/// the property and its children and properties.
final FutureOr<ObjectGroup> inspectorService;
/// JSON describing the diagnostic node.
final Map<String, Object> json;
Future<Map<String, InstanceRef>> _valueProperties;
final bool isProperty;
@override
bool operator ==(dynamic other) {
if (other is! RemoteDiagnosticsNode) return false;
return dartDiagnosticRef == other.dartDiagnosticRef;
}
@override
int get hashCode => dartDiagnosticRef.hashCode;
/// Separator text to show between property names and values.
String get separator => showSeparator ? ':' : '';
/// Label describing the [RemoteDiagnosticsNode], typically shown before a separator
/// (see [showSeparator]).
///
/// The name should be omitted if the [showName] property is false.
String get name => getStringMember('name');
/// Whether to show a separator between [name] and description.
///
/// If false, name and description should be shown with no separation.
/// `:` is typically used as a separator when displaying as text.
bool get showSeparator => getBooleanMember('showSeparator', true);
/// Returns a description with a short summary of the node itself not
/// including children or properties.
///
/// `parentConfiguration` specifies how the parent is rendered as text art.
/// For example, if the parent does not line break between properties, the
/// description of a property should also be a single line if possible.
String get description => getStringMember('description');
/// Priority level of the diagnostic used to control which diagnostics should
/// be shown and filtered.
///
/// Typically this only makes sense to set to a different value than
/// [DiagnosticLevel.info] for diagnostics representing properties. Some
/// subclasses have a `level` argument to their constructor which influences
/// the value returned here but other factors also influence it. For example,
/// whether an exception is thrown computing a property value
/// [DiagnosticLevel.error] is returned.
DiagnosticLevel get level => getLevelMember('level', DiagnosticLevel.info);
/// Whether the name of the property should be shown when showing the default
/// view of the tree.
///
/// This could be set to false (hiding the name) if the value's description
/// will make the name self-evident.
bool get showName => getBooleanMember('showName', true);
/// Description to show if the node has no displayed properties or children.
String getEmptyBodyDescription() => getStringMember('emptyBodyDescription');
/// Hint for how the node should be displayed.
DiagnosticsTreeStyle get style {
return _styleOverride ??
getStyleMember('style', DiagnosticsTreeStyle.sparse);
}
DiagnosticsTreeStyle _styleOverride;
set style(DiagnosticsTreeStyle style) {
_styleOverride = style;
}
/// Dart class defining the diagnostic node.
/// For example, DiagnosticProperty<Color>, IntProperty, StringProperty, etc.
/// This should rarely be required except for cases where custom rendering is desired
/// of a specific Dart diagnostic class.
String get type => getStringMember('type');
/// Whether the description is enclosed in double quotes.
///
/// Only relevant for String properties.
bool get isQuoted => getBooleanMember('quoted', false);
bool get hasIsQuoted => json.containsKey('quoted');
/// Optional unit the [value] is measured in.
///
/// Unit must be acceptable to display immediately after a number with no
/// spaces. For example: 'physical pixels per logical pixel' should be a
/// [tooltip] not a [unit].
///
/// Only specified for Number properties.
String get unit => getStringMember('unit');
bool get hasUnit => json.containsKey('unit');
/// String describing just the numeric [value] without a unit suffix.
///
/// Only specified for Number properties.
String get numberToString => getStringMember('numberToString');
bool get hasNumberToString => json.containsKey('numberToString');
/// Description to use if the property [value] is true.
///
/// If not specified and [value] equals true the property's priority [level]
/// will be [DiagnosticLevel.hidden].
///
/// Only applies to Flag properties.
String get ifTrue => getStringMember('ifTrue');
bool get hasIfTrue => json.containsKey('ifTrue');
/// Description to use if the property value is false.
///
/// If not specified and [value] equals false, the property's priority [level]
/// will be [DiagnosticLevel.hidden].
///
/// Only applies to Flag properties.
String get ifFalse => getStringMember('ifFalse');
bool get hasIfFalse => json.containsKey('ifFalse');
/// Value as a List of strings.
///
/// The raw value can always be extracted with the regular observatory protocol.
///
/// Only applies to IterableProperty.
List<String> get values {
final List<Object> rawValues = json['values'];
if (rawValues == null) {
return null;
}
return rawValues.toList();
}
bool get hasValues => json.containsKey('values');
/// Description to use if the property [value] is not null.
///
/// If the property [value] is not null and [ifPresent] is null, the
/// [level] for the property is [DiagnosticsLevel.hidden] and the description
/// from superclass is used.
///
/// Only specified for ObjectFlagProperty.
String get ifPresent => getStringMember('ifPresent');
bool get hasIfPresent => json.containsKey('ifPresent');
/// If the [value] of the property equals [defaultValue] the priority [level]
/// of the property is downgraded to [DiagnosticLevel.fine] as the property
/// value is uninteresting.
///
/// This is the default value of the object represented as a String.
/// The actual Dart object representing the defaultValue can also be accessed via
/// the observatory protocol. We can add a convenience helper method to access it here
/// if there is a use case.
///
/// Typically you shouldn't need to worry about the default value as the underlying
/// machinery will generate appropriate description and priority level based on the
/// default value.
String get defaultValue => getStringMember('defaultValue');
/// Whether a property has a default value.
bool get hasDefaultValue => json.containsKey('defaultValue');
/// Description if the property description would otherwise be empty.
///
/// Consider showing the property value in gray in an IDE if the description matches
/// ifEmpty.
String get ifEmpty => getStringMember('ifEmpty');
/// Description if the property [value] is null.
String get ifNull => getStringMember('ifNull');
bool get allowWrap => getBooleanMember('allowWrap', true);
/// Optional tooltip typically describing the property.
///
/// Example tooltip: 'physical pixels per logical pixel'
///
/// If present, the tooltip is added in parenthesis after the raw value when
/// generating the string description.
String get tooltip => getStringMember('tooltip') ?? '';
bool get hasTooltip => json.containsKey('tooltip');
/// Whether a [value] of null causes the property to have [level]
/// [DiagnosticLevel.warning] warning that the property is missing a [value].
bool get missingIfNull => getBooleanMember('missingIfNull', false);
/// String representation of exception thrown if accessing the property
/// [value] threw an exception.
String get exception => getStringMember('exception');
/// Whether accessing the property throws an exception.
bool get hasException => json.containsKey('exception');
bool get hasCreationLocation {
return _creationLocation != null || json.containsKey('creationLocation');
}
/// Location id compatible with rebuild location tracking code.
int get locationId => JsonUtils.getIntMember(json, 'locationId');
set creationLocation(InspectorSourceLocation location) {
_creationLocation = location;
}
InspectorSourceLocation _creationLocation;
InspectorSourceLocation get creationLocation {
if (_creationLocation != null) {
return _creationLocation;
}
if (!hasCreationLocation) {
return null;
}
_creationLocation = InspectorSourceLocation(json['creationLocation'], null);
return _creationLocation;
}
/// String representation of the type of the property [value].
///
/// This is determined from the type argument `T` used to instantiate the
/// [DiagnosticsProperty] class. This means that the type is available even if
/// [value] is null, but it also means that the [propertyType] is only as
/// accurate as the type provided when invoking the constructor.
///
/// Generally, this is only useful for diagnostic tools that should display
/// null values in a manner consistent with the property type. For example, a
/// tool might display a null [Color] value as an empty rectangle instead of
/// the word "null".
String get propertyType => getStringMember('propertyType');
/// If the [value] of the property equals [defaultValue] the priority [level]
/// of the property is downgraded to [DiagnosticLevel.fine] as the property
/// value is uninteresting.
///
/// [defaultValue] has type [T] or is [kNoDefaultValue].
DiagnosticLevel get defaultLevel {
return getLevelMember('defaultLevel', DiagnosticLevel.info);
}
/// Whether the value of the property is a Diagnosticable value itself.
/// Optionally, properties that are themselves Diagnosticable should be
/// displayed as trees of diagnosticable properties and children.
///
/// TODO(jacobr): add helpers to get the properties and children of
/// this diagnosticable value even if getChildren and getProperties
/// would return null. This will allow showing nested data for properties
/// that don't show children by default in other debugging output but
/// could.
bool get isDiagnosticableValue {
return getBooleanMember('isDiagnosticableValue', false);
}
String getStringMember(String memberName) {
return JsonUtils.getStringMember(json, memberName);
}
bool getBooleanMember(String memberName, bool defaultValue) {
if (json[memberName] == null) {
return defaultValue;
}
return json[memberName];
}
DiagnosticLevel getLevelMember(
String memberName, DiagnosticLevel defaultValue) {
final String value = json[memberName];
if (value == null) {
return defaultValue;
}
final level = nameToDiagnosticLevel[value];
assert(level != null, 'Unabled to find level for $value');
return level ?? defaultValue;
}
DiagnosticsTreeStyle getStyleMember(
String memberName, DiagnosticsTreeStyle defaultValue) {
if (!json.containsKey(memberName)) {
return defaultValue;
}
final String value = json[memberName];
if (value == null) {
return defaultValue;
}
final style = nameToTreeStyle[value];
assert(style != null);
return style ?? defaultValue;
}
/// Returns a reference to the value the DiagnosticsNode object is describing.
InspectorInstanceRef get valueRef => InspectorInstanceRef(json['valueId']);
bool isEnumProperty() {
return type != null && type.startsWith('EnumProperty<');
}
/// Returns a list of raw Dart property values of the Dart value of this
/// property that are useful for custom display of the property value.
/// For example, get the red, green, and blue components of color.
///
/// Unfortunately we cannot just use the list of fields from the Observatory
/// Instance object for the Dart value because much of the relevant
/// information to display good visualizations of Flutter values is stored
/// in properties not in fields.
Future<Map<String, InstanceRef>> get valueProperties async {
if (_valueProperties == null) {
if (propertyType == null || valueRef?.id == null) {
_valueProperties = Future.value(null);
return _valueProperties;
}
if (isEnumProperty()) {
// Populate all the enum property values.
return (await inspectorService)?.getEnumPropertyValues(valueRef);
}
List<String> propertyNames;
// Add more cases here as visual displays for additional Dart objects
// are added.
switch (propertyType) {
case 'Color':
propertyNames = ['red', 'green', 'blue', 'alpha'];
break;
case 'IconData':
propertyNames = ['codePoint'];
break;
default:
_valueProperties = Future.value(null);
return _valueProperties;
}
_valueProperties = (await inspectorService)
?.getDartObjectProperties(valueRef, propertyNames);
}
return _valueProperties;
}
Map<String, Object> get valuePropertiesJson => json['valueProperties'];
bool get hasChildren {
// In the summary tree, json['hasChildren']==true when the node has details
// tree children so we need to first check whether the list of children for
// the node in the tree was specified. If there is an empty list of children
// that indicates the node should have no children in the tree while if the
// 'children' property is not specified it means we do not know whether
// there is a list of children and need to query the server to find out.
final List children = json['children'];
if (children != null) {
return children.isNotEmpty;
}
return getBooleanMember('hasChildren', false);
}
bool get isCreatedByLocalProject {
return getBooleanMember('createdByLocalProject', false);
}
/// Whether this node is being displayed as a full tree or a filtered tree.
bool get isSummaryTree => getBooleanMember('summaryTree', false);
/// Whether this node is being displayed as a full tree or a filtered tree.
bool get isStateful => getBooleanMember('stateful', false);
String get widgetRuntimeType => getStringMember('widgetRuntimeType');
/// Check whether children are already available.
bool get childrenReady {
return json.containsKey('children') || _children != null || !hasChildren;
}
Future<List<RemoteDiagnosticsNode>> get children {
_computeChildren();
return _childrenFuture;
}
List<RemoteDiagnosticsNode> get childrenNow {
_maybePopulateChildren();
return _children;
}
Future<void> _computeChildren() async {
_maybePopulateChildren();
if (!hasChildren || _children != null) {
return;
}
if (_childrenFuture != null) {
await _childrenFuture;
return;
}
_childrenFuture = _getChildrenHelper();
try {
_children = await _childrenFuture;
} finally {
_children ??= [];
}
}
Future<List<RemoteDiagnosticsNode>> _getChildrenHelper() async {
return (await inspectorService)?.getChildren(
dartDiagnosticRef,
isSummaryTree,
this,
);
}
void _maybePopulateChildren() {
if (!hasChildren || _children != null) {
return;
}
final List<Object> jsonArray = json['children'];
if (jsonArray?.isNotEmpty == true) {
final List<RemoteDiagnosticsNode> nodes = [];
for (Map<String, Object> element in jsonArray) {
final child =
RemoteDiagnosticsNode(element, inspectorService, false, parent);
child.parent = this;
nodes.add(child);
}
_children = nodes;
}
}
Future<List<RemoteDiagnosticsNode>> _childrenFuture;
List<RemoteDiagnosticsNode> _children;
/// Reference the actual Dart DiagnosticsNode object this object is referencing.
InspectorInstanceRef get dartDiagnosticRef {
return InspectorInstanceRef(json['objectId']);
}
/// Properties to show inline in the widget tree.
List<RemoteDiagnosticsNode> get inlineProperties {
if (cachedProperties == null) {
cachedProperties = [];
if (json.containsKey('properties')) {
final List<Object> jsonArray = json['properties'];
for (Map<String, Object> element in jsonArray) {
cachedProperties.add(
RemoteDiagnosticsNode(element, inspectorService, true, parent));
}
trackPropertiesMatchingParameters(cachedProperties);
}
}
return cachedProperties;
}
Future<List<RemoteDiagnosticsNode>> getProperties(
ObjectGroup objectGroup) async {
return trackPropertiesMatchingParameters(
await objectGroup.getProperties(dartDiagnosticRef));
}
List<RemoteDiagnosticsNode> trackPropertiesMatchingParameters(
List<RemoteDiagnosticsNode> nodes) {
// Map locations to property nodes where available.
final List<InspectorSourceLocation> parameterLocations =
creationLocation?.getParameterLocations();
if (parameterLocations != null) {
final Map<String, InspectorSourceLocation> names = {};
for (InspectorSourceLocation location in parameterLocations) {
final String name = location.getName();
if (name != null) {
names[name] = location;
}
}
for (RemoteDiagnosticsNode node in nodes) {
node.parent = this;
final String name = node.name;
if (name != null) {
final InspectorSourceLocation parameterLocation = names[name];
if (parameterLocation != null) {
node.creationLocation = parameterLocation;
}
}
}
}
return nodes;
}
Future<String> get propertyDoc {
propertyDocFuture ??= _createPropertyDocFuture();
return propertyDocFuture;
}
Future<String> _createPropertyDocFuture() async {
// TODO(jacobr): We need access to the analyzer to support this feature.
/*
if (parent != null) {
DartVmServiceValue vmValue = inspectorService.toDartVmServiceValueForSourceLocation(parent.getValueRef());
if (vmValue == null) {
return null;
}
return inspectorService.getPropertyLocation(vmValue.getInstanceRef(), getName())
.thenApplyAsync((XSourcePosition sourcePosition) -> {
if (sourcePosition != null) {
final VirtualFile file = sourcePosition.getFile();
final int offset = sourcePosition.getOffset();
final Project project = getProject(file);
if (project != null) {
final List<HoverInformation> hovers =
DartAnalysisServerService.getInstance(project).analysis_getHover(file, offset);
if (!hovers.isEmpty()) {
return hovers.get(0).getDartdoc();
}
}
}
return 'Unable to find property source';
});
});
}
*/
return Future.value('Unable to find property source');
}
FlutterWidget get widget {
return Catalog.instance?.getWidget(widgetRuntimeType);
}
Icon get icon {
if (isProperty) return null;
Icon icon = widget?.icon;
if (icon == null && widgetRuntimeType != null) {
icon ??= iconMaker.fromWidgetName(widgetRuntimeType);
}
return icon;
}
/// Returns true if two diagnostic nodes are indistinguishable from
/// the perspective of a user debugging.
///
/// In practice this means that all fields but the objectId and valueId
/// properties for the DiagnosticsNode objects are identical. The valueId
/// field may change even for properties that have not changed because in
/// some cases such as the 'created' property for an element, the property
/// value is created dynamically each time 'getProperties' is called.
bool identicalDisplay(RemoteDiagnosticsNode node) {
if (node == null) {
return false;
}
final entries = json.entries;
if (entries.length != node.json.entries.length) {
return false;
}
for (var entry in entries) {
final String key = entry.key;
if (key == 'objectId' || key == 'valueId') {
continue;
}
if (entry.value == node.json[key]) {
return false;
}
}
return true;
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
for (var property in inlineProperties) {
properties.add(DiagnosticsProperty(property.name, property));
}
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
final children = childrenNow;
if (children == null || children.isEmpty) return const <DiagnosticsNode>[];
final regularChildren = <DiagnosticsNode>[];
for (var child in children) {
regularChildren.add(child.toDiagnosticsNode());
}
return regularChildren;
}
@override
DiagnosticsNode toDiagnosticsNode({String name, DiagnosticsTreeStyle style}) {
return super.toDiagnosticsNode(
name: name ?? this.name,
style: style ?? DiagnosticsTreeStyle.sparse,
);
}
@override
String toStringShort() {
return description;
}
Future<void> setSelectionInspector(bool uiAlreadyUpdated) async {
await (await inspectorService)
?.setSelectionInspector(valueRef, uiAlreadyUpdated);
}
}
class InspectorSourceLocation {
InspectorSourceLocation(this.json, this.parent);
final Map<String, Object> json;
final InspectorSourceLocation parent;
String get path => JsonUtils.getStringMember(json, 'file');
String getFile() {
final fileName = path;
if (fileName == null) {
return parent != null ? parent.getFile() : null;
}
return fileName;
}
int getLine() => JsonUtils.getIntMember(json, 'line');
String getName() => JsonUtils.getStringMember(json, 'name');
int getColumn() => JsonUtils.getIntMember(json, 'column');
SourcePosition getXSourcePosition() {
final file = getFile();
if (file == null) {
return null;
}
final int line = getLine();
final int column = getColumn();
if (line < 0 || column < 0) {
return null;
}
return SourcePosition(file: file, line: line - 1, column: column - 1);
}
List<InspectorSourceLocation> getParameterLocations() {
if (json.containsKey('parameterLocations')) {
final List<Object> parametersJson = json['parameterLocations'];
final List<InspectorSourceLocation> ret = [];
for (int i = 0; i < parametersJson.length; ++i) {
ret.add(InspectorSourceLocation(parametersJson[i], this));
}
return ret;
}
return null;
}
}
class SourcePosition {
const SourcePosition({this.file, this.line, this.column});
final String file;
final int line;
final int column;
}