// Copyright 2017 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. import 'package:meta/meta.dart'; import 'print.dart'; /// The various priority levels used to filter which diagnostics are shown and /// omitted. /// /// Trees of Flutter diagnostics can be very large so filtering the diagnostics /// shown matters. Typically filtering to only show diagnostics with at least /// level [debug] is appropriate. enum DiagnosticLevel { /// Diagnostics that should not be shown. /// /// If a user chooses to display [hidden] diagnostics, they should not expect /// the diagnostics to be formatted consistently with other diagnostics and /// they should expect them to sometimes be misleading. For example, /// [FlagProperty] and [ObjectFlagProperty] have uglier formatting when the /// property `value` does does not match a value with a custom flag /// description. An example of a misleading diagnostic is a diagnostic for /// a property that has no effect because some other property of the object is /// set in a way that causes the hidden property to have no effect. hidden, /// A diagnostic that is likely to be low value but where the diagnostic /// display is just as high quality as a diagnostic with a higher level. /// /// Use this level for diagnostic properties that match their default value /// and other cases where showing a diagnostic would not add much value such /// as an [IterableProperty] where the value is empty. fine, /// Diagnostics that should only be shown when performing fine grained /// debugging of an object. /// /// Unlike a [fine] diagnostic, these diagnostics provide important /// information about the object that is likely to be needed to debug. Used by /// properties that are important but where the property value is too verbose /// (e.g. 300+ characters long) to show with a higher diagnostic level. debug, /// Interesting diagnostics that should be typically shown. info, /// Very important diagnostics that indicate problematic property values. /// /// For example, use if you would write the property description /// message in ALL CAPS. warning, /// Diagnostics that indicate errors or unexpected conditions. /// /// For example, use for property values where computing the value throws an /// exception. error, /// Special level indicating that no diagnostics should be shown. /// /// Do not specify this level for diagnostics. This level is only used to /// filter which diagnostics are shown. off, } /// Styles for displaying a node in a [DiagnosticsNode] tree. /// /// See also: /// /// * [DiagnosticsNode.toStringDeep], which dumps text art trees for these /// styles. enum DiagnosticsTreeStyle { /// Sparse style for displaying trees. /// /// See also: /// /// * [RenderObject], which uses this style. sparse, /// Connects a node to its parent with a dashed line. /// /// See also: /// /// * [RenderSliverMultiBoxAdaptor], which uses this style to distinguish /// offstage children from onstage children. offstage, /// Slightly more compact version of the [sparse] style. /// /// See also: /// /// * [Element], which uses this style. dense, /// Style that enables transitioning from nodes of one style to children of /// another. /// /// See also: /// /// * [RenderParagraph], which uses this style to display a [TextSpan] child /// in a way that is compatible with the [DiagnosticsTreeStyle.sparse] /// style of the [RenderObject] tree. transition, /// Render the tree just using whitespace without connecting parents to /// children using lines. /// /// See also: /// /// * [SliverGeometry], which uses this style. whitespace, /// Render the tree on a single line without showing children. singleLine, } /// Configuration specifying how a particular [DiagnosticsTreeStyle] should be /// rendered as text art. /// /// See also: /// /// * [sparseTextConfiguration], which is a typical style. /// * [transitionTextConfiguration], which is an example of a complex tree style. /// * [DiagnosticsNode.toStringDeep], for code using [TextTreeConfiguration] /// to render text art for arbitrary trees of [DiagnosticsNode] objects. class TextTreeConfiguration { /// Create a configuration object describing how to render a tree as text. /// /// All of the arguments must not be null. TextTreeConfiguration({ @required this.prefixLineOne, @required this.prefixOtherLines, @required this.prefixLastChildLineOne, @required this.prefixOtherLinesRootNode, @required this.linkCharacter, @required this.propertyPrefixIfChildren, @required this.propertyPrefixNoChildren, this.lineBreak = '\n', this.lineBreakProperties = true, this.afterName = ':', this.afterDescriptionIfBody = '', this.beforeProperties = '', this.afterProperties = '', this.propertySeparator = '', this.bodyIndent = '', this.footer = '', this.showChildren = true, this.addBlankLineIfNoChildren = true, this.isNameOnOwnLine = false, this.isBlankLineBetweenPropertiesAndChildren = true, }) : assert(prefixLineOne != null), assert(prefixOtherLines != null), assert(prefixLastChildLineOne != null), assert(prefixOtherLinesRootNode != null), assert(linkCharacter != null), assert(propertyPrefixIfChildren != null), assert(propertyPrefixNoChildren != null), assert(lineBreak != null), assert(lineBreakProperties != null), assert(afterName != null), assert(afterDescriptionIfBody != null), assert(beforeProperties != null), assert(afterProperties != null), assert(propertySeparator != null), assert(bodyIndent != null), assert(footer != null), assert(showChildren != null), assert(addBlankLineIfNoChildren != null), assert(isNameOnOwnLine != null), assert(isBlankLineBetweenPropertiesAndChildren != null), childLinkSpace = ' ' * linkCharacter.length; /// Prefix to add to the first line to display a child with this style. final String prefixLineOne; /// Prefix to add to other lines to display a child with this style. /// /// [prefixOtherLines] should typically be one character shorter than /// [prefixLineOne] as final String prefixOtherLines; /// Prefix to add to the first line to display the last child of a node with /// this style. final String prefixLastChildLineOne; /// Additional prefix to add to other lines of a node if this is the root node /// of the tree. final String prefixOtherLinesRootNode; /// Prefix to add before each property if the node as children. /// /// Plays a similar role to [linkCharacter] except that some configurations /// intentionally use a different line style than the [linkCharacter]. final String propertyPrefixIfChildren; /// Prefix to add before each property if the node does not have children. /// /// This string is typically a whitespace string the same length as /// [propertyPrefixIfChildren] but can have a different length. final String propertyPrefixNoChildren; /// Character to use to draw line linking parent to child. /// /// The first child does not require a line but all subsequent children do /// with the line drawn immediately before the left edge of the previous /// sibling. final String linkCharacter; /// Whitespace to draw instead of the childLink character if this node is the /// last child of its parent so no link line is required. final String childLinkSpace; /// Character(s) to use to separate lines. /// /// Typically leave set at the default value of '\n' unless this style needs /// to treat lines differently as is the case for /// [singleLineTextConfiguration]. final String lineBreak; /// Whether to place line breaks between properties or to leave all /// properties on one line. final bool lineBreakProperties; /// Text added immediately after the name of the node. /// /// See [transitionTextConfiguration] for an example of using a value other /// than ':' to achieve a custom line art style. final String afterName; /// Text to add immediately after the description line of a node with /// properties and/or children. final String afterDescriptionIfBody; /// Optional string to add before the properties of a node. /// /// Only displayed if the node has properties. /// See [singleLineTextConfiguration] for an example of using this field /// to enclose the property list with parenthesis. final String beforeProperties; /// Optional string to add after the properties of a node. /// /// See documentation for [beforeProperties]. final String afterProperties; /// Property separator to add between properties. /// /// See [singleLineTextConfiguration] for an example of using this field /// to render properties as a comma separated list. final String propertySeparator; /// Prefix to add to all lines of the body of the tree node. /// /// The body is all content in the node other than the name and description. final String bodyIndent; /// Whether the children of a node should be shown. /// /// See [singleLineTextConfiguration] for an example of using this field to /// hide all children of a node. final bool showChildren; /// Whether to add a blank line at the end of the output for a node if it has /// no children. /// /// See [denseTextConfiguration] for an example of setting this to false. final bool addBlankLineIfNoChildren; /// Whether the name should be displayed on the same line as the description. final bool isNameOnOwnLine; /// Footer to add as its own line at the end of a non-root node. /// /// See [transitionTextConfiguration] for an example of using footer to draw a box /// around the node. [footer] is indented the same amount as [prefixOtherLines]. final String footer; /// Add a blank line between properties and children if both are present. final bool isBlankLineBetweenPropertiesAndChildren; } /// Default text tree configuration. /// /// Example: /// ``` /// <root_name>: <root_description> /// │ <property1> /// │ <property2> /// │ ... /// │ <propertyN> /// ├─<child_name>: <child_description> /// │ │ <property1> /// │ │ <property2> /// │ │ ... /// │ │ <propertyN> /// │ │ /// │ └─<child_name>: <child_description> /// │ <property1> /// │ <property2> /// │ ... /// │ <propertyN> /// │ /// └─<child_name>: <child_description>' /// <property1> /// <property2> /// ... /// <propertyN> /// ``` /// /// See also: /// /// * [DiagnosticsTreeStyle.sparse] final TextTreeConfiguration sparseTextConfiguration = new TextTreeConfiguration( prefixLineOne: '├─', prefixOtherLines: ' ', prefixLastChildLineOne: '└─', linkCharacter: '│', propertyPrefixIfChildren: '│ ', propertyPrefixNoChildren: ' ', prefixOtherLinesRootNode: ' ', ); /// Identical to [sparseTextConfiguration] except that the lines connecting /// parent to children are dashed. /// /// Example: /// ``` /// <root_name>: <root_description> /// │ <property1> /// │ <property2> /// │ ... /// │ <propertyN> /// ├─<normal_child_name>: <child_description> /// ╎ │ <property1> /// ╎ │ <property2> /// ╎ │ ... /// ╎ │ <propertyN> /// ╎ │ /// ╎ └─<child_name>: <child_description> /// ╎ <property1> /// ╎ <property2> /// ╎ ... /// ╎ <propertyN> /// ╎ /// ╎╌<dashed_child_name>: <child_description> /// ╎ │ <property1> /// ╎ │ <property2> /// ╎ │ ... /// ╎ │ <propertyN> /// ╎ │ /// ╎ └─<child_name>: <child_description> /// ╎ <property1> /// ╎ <property2> /// ╎ ... /// ╎ <propertyN> /// ╎ /// └╌<dashed_child_name>: <child_description>' /// <property1> /// <property2> /// ... /// <propertyN> /// ``` /// /// See also: /// /// * [DiagnosticsTreeStyle.offstage], uses this style for ASCII art display. final TextTreeConfiguration dashedTextConfiguration = new TextTreeConfiguration( prefixLineOne: '╎╌', prefixLastChildLineOne: '└╌', prefixOtherLines: ' ', linkCharacter: '╎', // Intentionally not set as a dashed line as that would make the properties // look like they were disabled. propertyPrefixIfChildren: '│ ', propertyPrefixNoChildren: ' ', prefixOtherLinesRootNode: ' ', ); /// Dense text tree configuration that minimizes horizontal whitespace. /// /// Example: /// ``` /// <root_name>: <root_description>(<property1>; <property2> <propertyN>) /// ├<child_name>: <child_description>(<property1>, <property2>, <propertyN>) /// └<child_name>: <child_description>(<property1>, <property2>, <propertyN>) /// ``` /// /// See also: /// /// * [DiagnosticsTreeStyle.dense] final TextTreeConfiguration denseTextConfiguration = new TextTreeConfiguration( propertySeparator: ', ', beforeProperties: '(', afterProperties: ')', lineBreakProperties: false, prefixLineOne: '├', prefixOtherLines: '', prefixLastChildLineOne: '└', linkCharacter: '│', propertyPrefixIfChildren: '│', propertyPrefixNoChildren: ' ', prefixOtherLinesRootNode: '', addBlankLineIfNoChildren: false, isBlankLineBetweenPropertiesAndChildren: false, ); /// Configuration that draws a box around a leaf node. /// /// Used by leaf nodes such as [TextSpan] to draw a clear border around the /// contents of a node. /// /// Example: /// ``` /// <parent_node> /// ╞═╦══ <name> ═══ /// │ ║ <description>: /// │ ║ <body> /// │ ║ ... /// │ ╚═══════════ /// ╘═╦══ <name> ═══ /// ║ <description>: /// ║ <body> /// ║ ... /// ╚═══════════ /// ``` /// /// /// See also: /// /// * [DiagnosticsTreeStyle.transition] final TextTreeConfiguration transitionTextConfiguration = new TextTreeConfiguration( prefixLineOne: '╞═╦══ ', prefixLastChildLineOne: '╘═╦══ ', prefixOtherLines: ' ║ ', footer: ' ╚═══════════\n', linkCharacter: '│', // Subtree boundaries are clear due to the border around the node so omit the // property prefix. propertyPrefixIfChildren: '', propertyPrefixNoChildren: '', prefixOtherLinesRootNode: '', afterName: ' ═══', // Add a colon after the description if the node has a body to make the // connection between the description and the body clearer. afterDescriptionIfBody: ':', // Members are indented an extra two spaces to disambiguate as the description // is placed within the box instead of along side the name as is the case for // other styles. bodyIndent: ' ', isNameOnOwnLine: true, // No need to add a blank line as the footer makes the boundary of this // subtree unambiguous. addBlankLineIfNoChildren: false, isBlankLineBetweenPropertiesAndChildren: false, ); /// Whitespace only configuration where children are consistently indented /// two spaces. /// /// Use this style for displaying properties with structured values or for /// displaying children within a [transitionTextConfiguration] as using a style that /// draws line art would be visually distracting for those cases. /// /// Example: /// ``` /// <parent_node> /// <name>: <description>: /// <properties> /// <children> /// <name>: <description>: /// <properties> /// <children> ///``` /// /// See also: /// /// * [DiagnosticsTreeStyle.whitespace] final TextTreeConfiguration whitespaceTextConfiguration = new TextTreeConfiguration( prefixLineOne: '', prefixLastChildLineOne: '', prefixOtherLines: ' ', prefixOtherLinesRootNode: ' ', bodyIndent: '', propertyPrefixIfChildren: '', propertyPrefixNoChildren: '', linkCharacter: ' ', addBlankLineIfNoChildren: false, // Add a colon after the description and before the properties to link the // properties to the description line. afterDescriptionIfBody: ':', isBlankLineBetweenPropertiesAndChildren: false, ); /// Render a node as a single line omitting children. /// /// Example: /// `<name>: <description>(<property1>, <property2>, ..., <propertyN>)` /// /// See also: /// /// * [DiagnosticsTreeStyle.singleLine] final TextTreeConfiguration singleLineTextConfiguration = new TextTreeConfiguration( propertySeparator: ', ', beforeProperties: '(', afterProperties: ')', prefixLineOne: '', prefixOtherLines: '', prefixLastChildLineOne: '', lineBreak: '', lineBreakProperties: false, addBlankLineIfNoChildren: false, showChildren: false, propertyPrefixIfChildren: '', propertyPrefixNoChildren: '', linkCharacter: '', prefixOtherLinesRootNode: '', ); /// Builder that builds a String with specified prefixes for the first and /// subsequent lines. /// /// Allows for the incremental building of strings using `write*()` methods. /// The strings are concatenated into a single string with the first line /// prefixed by [prefixLineOne] and subsequent lines prefixed by /// [prefixOtherLines]. class _PrefixedStringBuilder { _PrefixedStringBuilder(this.prefixLineOne, this.prefixOtherLines); /// Prefix to add to the first line. final String prefixLineOne; /// Prefix to add to subsequent lines. /// /// The prefix can be modified while the string is being built in which case /// subsequent lines will be added with the modified prefix. String prefixOtherLines; final StringBuffer _buffer = new StringBuffer(); bool _atLineStart = true; bool _hasMultipleLines = false; /// Whether the string being built already has more than 1 line. bool get hasMultipleLines => _hasMultipleLines; /// Write text ensuring the specified prefixes for the first and subsequent /// lines. void write(String s) { if (s.isEmpty) return; if (s == '\n') { // Edge case to avoid adding trailing whitespace when the caller did // not explicitly add trailing whitespace. if (_buffer.isEmpty) { _buffer.write(prefixLineOne.trimRight()); } else if (_atLineStart) { _buffer.write(prefixOtherLines.trimRight()); _hasMultipleLines = true; } _buffer.write('\n'); _atLineStart = true; return; } if (_buffer.isEmpty) { _buffer.write(prefixLineOne); } else if (_atLineStart) { _buffer.write(prefixOtherLines); _hasMultipleLines = true; } bool lineTerminated = false; if (s.endsWith('\n')) { s = s.substring(0, s.length - 1); lineTerminated = true; } final List<String> parts = s.split('\n'); _buffer.write(parts[0]); for (int i = 1; i < parts.length; ++i) { _buffer ..write('\n') ..write(prefixOtherLines) ..write(parts[i]); } if (lineTerminated) _buffer.write('\n'); _atLineStart = lineTerminated; } /// Write text assuming the text already obeys the specified prefixes for the /// first and subsequent lines. void writeRaw(String text) { if (text.isEmpty) return; _buffer.write(text); _atLineStart = text.endsWith('\n'); } /// Write a line assuming the line obeys the specified prefixes. Ensures that /// a newline is added if one is not present. /// The same as [writeRaw] except a newline is added at the end of [line] if /// one is not already present. /// /// A new line is not added if the input string already contains a newline. void writeRawLine(String line) { if (line.isEmpty) return; _buffer.write(line); if (!line.endsWith('\n')) _buffer.write('\n'); _atLineStart = true; } @override String toString() => _buffer.toString(); } class _NoDefaultValue { const _NoDefaultValue(); } /// Marker object indicating that a [DiagnosticsNode] has no default value. const _NoDefaultValue kNoDefaultValue = const _NoDefaultValue(); /// Defines diagnostics data for a [value]. /// /// [DiagnosticsNode] 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. abstract class DiagnosticsNode { /// Initializes the object. /// /// The [style], [showName], and [showSeparator] arguments must not /// be null. DiagnosticsNode({ @required this.name, this.style, this.showName = true, this.showSeparator = true, }) : assert(showName != null), assert(showSeparator != null), // A name ending with ':' indicates that the user forgot that the ':' will // be automatically added for them when generating descriptions of the // property. assert(name == null || !name.endsWith(':'), 'Names of diagnostic nodes must not end with colons.'); /// Diagnostics containing just a string `message` and not a concrete name or /// value. /// /// The [style] and [level] arguments must not be null. /// /// See also: /// /// * [MessageProperty], which is better suited to messages that are to be /// formatted like a property with a separate name and message. factory DiagnosticsNode.message( String message, { DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, DiagnosticLevel level = DiagnosticLevel.info, }) { assert(style != null); assert(level != null); return new DiagnosticsProperty<Null>( '', null, description: message, style: style, showName: false, level: level, ); } /// Label describing the [DiagnosticsNode], typically shown before a separator /// (see [showSeparator]). /// /// The name will be omitted if the [showName] property is false. final String name; /// 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 toDescription({ TextTreeConfiguration parentConfiguration }); /// 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. final bool showSeparator; /// Whether the diagnostic should be filtered due to its [level] being lower /// than `minLevel`. /// /// If `minLevel` is [DiagnosticLevel.hidden] no diagnostics will be filtered. /// If `minLevel` is [DiagnosticsLevel.off] all diagnostics will be filtered. bool isFiltered(DiagnosticLevel minLevel) => level.index < minLevel.index; /// 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 => 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. final bool showName; /// Description to show if the node has no displayed properties or children. String get emptyBodyDescription => null; /// The actual object this is diagnostics data for. Object get value; /// Hint for how the node should be displayed. final DiagnosticsTreeStyle style; /// Properties of this [DiagnosticsNode]. /// /// Properties and children are kept distinct even though they are both /// [List<DiagnosticsNode>] because they should be grouped differently. List<DiagnosticsNode> getProperties(); /// Children of this [DiagnosticsNode]. /// /// See also: /// /// * [getProperties] List<DiagnosticsNode> getChildren(); String get _separator => showSeparator ? ':' : ''; /// Serialize the node excluding its descendants to a JSON map. /// /// Subclasses should override if they have additional properties that are /// useful for the GUI tools that consume this JSON. /// /// See also: /// /// * [WidgetInspectorService], which forms the bridge between JSON returned /// by this method and interactive tree views in the Flutter IntelliJ /// plugin. @mustCallSuper Map<String, Object> toJsonMap() { final Map<String, Object> data = <String, Object>{ 'name': name, 'showSeparator': showSeparator, 'description': toDescription(), 'level': describeEnum(level), 'showName': showName, 'emptyBodyDescription': emptyBodyDescription, 'style': describeEnum(style), 'valueToString': value.toString(), 'type': runtimeType.toString(), 'hasChildren': getChildren().isNotEmpty, }; return data; } /// Returns a string representation of this diagnostic that is compatible with /// the style of the parent if the node is not the root. /// /// `parentConfiguration` specifies how the parent is rendered as text art. /// For example, if the parent places all properties on one line, the /// [toString] for each property should avoid line breaks if possible. /// /// `minLevel` specifies the minimum [DiagnosticLevel] for properties included /// in the output. @override String toString({ TextTreeConfiguration parentConfiguration, DiagnosticLevel minLevel = DiagnosticLevel.info, }) { assert(style != null); assert(minLevel != null); if (style == DiagnosticsTreeStyle.singleLine) return toStringDeep(parentConfiguration: parentConfiguration, minLevel: minLevel); final String description = toDescription(parentConfiguration: parentConfiguration); if (name == null || name.isEmpty || !showName) return description; return description.contains('\n') ? '$name$_separator\n$description' : '$name$_separator $description'; } /// Returns a configuration specifying how this object should be rendered /// as text art. @protected TextTreeConfiguration get textTreeConfiguration { assert(style != null); switch (style) { case DiagnosticsTreeStyle.dense: return denseTextConfiguration; case DiagnosticsTreeStyle.sparse: return sparseTextConfiguration; case DiagnosticsTreeStyle.offstage: return dashedTextConfiguration; case DiagnosticsTreeStyle.whitespace: return whitespaceTextConfiguration; case DiagnosticsTreeStyle.transition: return transitionTextConfiguration; case DiagnosticsTreeStyle.singleLine: return singleLineTextConfiguration; } return null; } /// Text configuration to use to connect this node to a `child`. /// /// The singleLine style is special cased because the connection from the /// parent to the child should be consistent with the parent's style as the /// single line style does not provide any meaningful style for how children /// should be connected to their parents. TextTreeConfiguration _childTextConfiguration( DiagnosticsNode child, TextTreeConfiguration textStyle, ) { return (child != null && child.style != DiagnosticsTreeStyle.singleLine) ? child.textTreeConfiguration : textStyle; } /// Returns a string representation of this node and its descendants. /// /// `prefixLineOne` will be added to the front of the first line of the /// output. `prefixOtherLines` will be added to the front of each other line. /// If `prefixOtherLines` is null, the `prefixLineOne` is used for every line. /// By default, there is no prefix. /// /// `minLevel` specifies the minimum [DiagnosticLevel] for properties included /// in the output. /// /// The [toStringDeep] method takes other arguments, but those are intended /// for internal use when recursing to the descendants, and so can be ignored. /// /// See also: /// /// * [toString], for a brief description of the [value] but not its children. /// * [toStringShallow], for a detailed description of the [value] but not its /// children. String toStringDeep({ String prefixLineOne = '', String prefixOtherLines, TextTreeConfiguration parentConfiguration, DiagnosticLevel minLevel = DiagnosticLevel.debug, }) { assert(minLevel != null); prefixOtherLines ??= prefixLineOne; final List<DiagnosticsNode> children = getChildren(); final TextTreeConfiguration config = textTreeConfiguration; if (prefixOtherLines.isEmpty) prefixOtherLines += config.prefixOtherLinesRootNode; final _PrefixedStringBuilder builder = new _PrefixedStringBuilder( prefixLineOne, prefixOtherLines, ); final String description = toDescription(parentConfiguration: parentConfiguration); if (description == null || description.isEmpty) { if (showName && name != null) builder.write(name); } else { if (name != null && name.isNotEmpty && showName) { builder.write(name); if (showSeparator) builder.write(config.afterName); builder.write( config.isNameOnOwnLine || description.contains('\n') ? '\n' : ' '); if (description.contains('\n') && style == DiagnosticsTreeStyle.singleLine) builder.prefixOtherLines += ' '; } builder.prefixOtherLines += children.isEmpty ? config.propertyPrefixNoChildren : config.propertyPrefixIfChildren; builder.write(description); } final List<DiagnosticsNode> properties = getProperties().where( (DiagnosticsNode n) => !n.isFiltered(minLevel) ).toList(); if (properties.isNotEmpty || children.isNotEmpty || emptyBodyDescription != null) builder.write(config.afterDescriptionIfBody); if (config.lineBreakProperties) builder.write(config.lineBreak); if (properties.isNotEmpty) builder.write(config.beforeProperties); builder.prefixOtherLines += config.bodyIndent; if (emptyBodyDescription != null && properties.isEmpty && children.isEmpty && prefixLineOne.isNotEmpty) { builder.write(emptyBodyDescription); if (config.lineBreakProperties) builder.write(config.lineBreak); } for (int i = 0; i < properties.length; ++i) { final DiagnosticsNode property = properties[i]; if (i > 0) builder.write(config.propertySeparator); const int kWrapWidth = 65; if (property.style != DiagnosticsTreeStyle.singleLine) { final TextTreeConfiguration propertyStyle = property.textTreeConfiguration; builder.writeRaw(property.toStringDeep( prefixLineOne: '${builder.prefixOtherLines}${propertyStyle.prefixLineOne}', prefixOtherLines: '${builder.prefixOtherLines}${propertyStyle.linkCharacter}${propertyStyle.prefixOtherLines}', parentConfiguration: config, minLevel: minLevel, )); continue; } assert(property.style == DiagnosticsTreeStyle.singleLine); final String message = property.toString(parentConfiguration: config, minLevel: minLevel); if (!config.lineBreakProperties || message.length < kWrapWidth) { builder.write(message); } else { // debugWordWrap doesn't handle line breaks within the text being // wrapped so we must call it on each line. final List<String> lines = message.split('\n'); for (int j = 0; j < lines.length; ++j) { final String line = lines[j]; if (j > 0) builder.write(config.lineBreak); builder.write(debugWordWrap(line, kWrapWidth, wrapIndent: ' ').join('\n')); } } if (config.lineBreakProperties) builder.write(config.lineBreak); } if (properties.isNotEmpty) builder.write(config.afterProperties); if (!config.lineBreakProperties) builder.write(config.lineBreak); final String prefixChildren = '$prefixOtherLines${config.bodyIndent}'; if (children.isEmpty && config.addBlankLineIfNoChildren && builder.hasMultipleLines) { final String prefix = prefixChildren.trimRight(); if (prefix.isNotEmpty) builder.writeRaw('$prefix${config.lineBreak}'); } if (children.isNotEmpty && config.showChildren) { if (config.isBlankLineBetweenPropertiesAndChildren && properties.isNotEmpty && children.first.textTreeConfiguration.isBlankLineBetweenPropertiesAndChildren) { builder.write(config.lineBreak); } for (int i = 0; i < children.length; i++) { final DiagnosticsNode child = children[i]; assert(child != null); final TextTreeConfiguration childConfig = _childTextConfiguration(child, config); if (i == children.length - 1) { final String lastChildPrefixLineOne = '$prefixChildren${childConfig.prefixLastChildLineOne}'; builder.writeRawLine(child.toStringDeep( prefixLineOne: lastChildPrefixLineOne, prefixOtherLines: '$prefixChildren${childConfig.childLinkSpace}${childConfig.prefixOtherLines}', parentConfiguration: config, minLevel: minLevel, )); if (childConfig.footer.isNotEmpty) builder.writeRaw('$prefixChildren${childConfig.childLinkSpace}${childConfig.footer}'); } else { final TextTreeConfiguration nextChildStyle = _childTextConfiguration(children[i + 1], config); final String childPrefixLineOne = '$prefixChildren${childConfig.prefixLineOne}'; final String childPrefixOtherLines ='$prefixChildren${nextChildStyle.linkCharacter}${childConfig.prefixOtherLines}'; builder.writeRawLine(child.toStringDeep( prefixLineOne: childPrefixLineOne, prefixOtherLines: childPrefixOtherLines, parentConfiguration: config, minLevel: minLevel, )); if (childConfig.footer.isNotEmpty) builder.writeRaw('$prefixChildren${nextChildStyle.linkCharacter}${childConfig.footer}'); } } } return builder.toString(); } } /// Debugging message displayed like a property. /// /// ## Sample code /// /// The following two properties are better expressed using this /// [MessageProperty] class, rather than [StringProperty], as the intent is to /// show a message with property style display rather than to describe the value /// of an actual property of the object: /// /// ```dart /// new MessageProperty('table size', '$columns\u00D7$rows') /// ``` /// ```dart /// new MessageProperty('usefulness ratio', 'no metrics collected yet (never painted)') /// ``` /// /// On the other hand, [StringProperty] is better suited when the property has a /// concrete value that is a string: /// /// ```dart /// new StringProperty('name', _name) /// ``` /// /// See also: /// /// * [DiagnosticsNode.message], which serves the same role for messages /// without a clear property name. /// * [StringProperty], which is a better fit for properties with string values. class MessageProperty extends DiagnosticsProperty<Null> { /// Create a diagnostics property that displays a message. /// /// Messages have no concrete [value] (so [value] will return null). The /// message is stored as the description. /// /// The [name], `message`, and [level] arguments must not be null. MessageProperty(String name, String message, { DiagnosticLevel level = DiagnosticLevel.info, }) : assert(name != null), assert(message != null), assert(level != null), super(name, null, description: message, level: level); } /// Property which encloses its string [value] in quotes. /// /// See also: /// /// * [MessageProperty], which is a better fit for showing a message /// instead of describing a property with a string value. class StringProperty extends DiagnosticsProperty<String> { /// Create a diagnostics property for strings. /// /// The [showName], [quoted], and [level] arguments must not be null. StringProperty(String name, String value, { String description, String tooltip, bool showName = true, Object defaultValue = kNoDefaultValue, this.quoted = true, String ifEmpty, DiagnosticLevel level = DiagnosticLevel.info, }) : assert(showName != null), assert(quoted != null), assert(level != null), super( name, value, description: description, defaultValue: defaultValue, tooltip: tooltip, showName: showName, ifEmpty: ifEmpty, level: level, ); /// Whether the value is enclosed in double quotes. final bool quoted; @override Map<String, Object> toJsonMap() { final Map<String, Object> json = super.toJsonMap(); json['quoted'] = quoted; return json; } @override String valueToString({ TextTreeConfiguration parentConfiguration }) { String text = _description ?? value; if (parentConfiguration != null && !parentConfiguration.lineBreakProperties && text != null) { // Escape linebreaks in multiline strings to avoid confusing output when // the parent of this node is trying to display all properties on the same // line. text = text.replaceAll('\n', '\\n'); } if (quoted && text != null) { // An empty value would not appear empty after being surrounded with // quotes so we have to handle this case separately. if (ifEmpty != null && text.isEmpty) return ifEmpty; return '"$text"'; } return text.toString(); } } abstract class _NumProperty<T extends num> extends DiagnosticsProperty<T> { _NumProperty(String name, T value, { String ifNull, this.unit, bool showName = true, Object defaultValue = kNoDefaultValue, String tooltip, DiagnosticLevel level = DiagnosticLevel.info, }) : super( name, value, ifNull: ifNull, showName: showName, defaultValue: defaultValue, tooltip: tooltip, level: level, ); _NumProperty.lazy(String name, ComputePropertyValueCallback<T> computeValue, { String ifNull, this.unit, bool showName = true, Object defaultValue = kNoDefaultValue, String tooltip, DiagnosticLevel level = DiagnosticLevel.info, }) : super.lazy( name, computeValue, ifNull: ifNull, showName: showName, defaultValue: defaultValue, tooltip: tooltip, level: level, ); @override Map<String, Object> toJsonMap() { final Map<String, Object> json = super.toJsonMap(); if (unit != null) json['unit'] = unit; json['numberToString'] = numberToString(); return json; } /// 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]. final String unit; /// String describing just the numeric [value] without a unit suffix. String numberToString(); @override String valueToString({ TextTreeConfiguration parentConfiguration }) { if (value == null) return value.toString(); return unit != null ? '${numberToString()}$unit' : numberToString(); } } /// Property describing a [double] [value] with an optional [unit] of measurement. /// /// Numeric formatting is optimized for debug message readability. class DoubleProperty extends _NumProperty<double> { /// If specified, [unit] describes the unit for the [value] (e.g. px). /// /// The [showName] and [level] arguments must not be null. DoubleProperty(String name, double value, { String ifNull, String unit, String tooltip, Object defaultValue = kNoDefaultValue, bool showName = true, DiagnosticLevel level = DiagnosticLevel.info, }) : assert(showName != null), assert(level != null), super( name, value, ifNull: ifNull, unit: unit, tooltip: tooltip, defaultValue: defaultValue, showName: showName, level: level, ); /// Property with a [value] that is computed only when needed. /// /// Use if computing the property [value] may throw an exception or is /// expensive. /// /// The [showName] and [level] arguments must not be null. DoubleProperty.lazy( String name, ComputePropertyValueCallback<double> computeValue, { String ifNull, bool showName = true, String unit, String tooltip, Object defaultValue = kNoDefaultValue, DiagnosticLevel level = DiagnosticLevel.info, }) : assert(showName != null), assert(level != null), super.lazy( name, computeValue, showName: showName, ifNull: ifNull, unit: unit, tooltip: tooltip, defaultValue: defaultValue, level: level, ); @override String numberToString() => value?.toStringAsFixed(1); } /// An int valued property with an optional unit the value is measured in. /// /// Examples of units include 'px' and 'ms'. class IntProperty extends _NumProperty<int> { /// Create a diagnostics property for integers. /// /// The [showName] and [level] arguments must not be null. IntProperty(String name, int value, { String ifNull, bool showName = true, String unit, Object defaultValue = kNoDefaultValue, DiagnosticLevel level = DiagnosticLevel.info, }) : assert(showName != null), assert(level != null), super( name, value, ifNull: ifNull, showName: showName, unit: unit, defaultValue: defaultValue, level: level, ); @override String numberToString() => value.toString(); } /// Property which clamps a [double] to between 0 and 1 and formats it as a /// percentage. class PercentProperty extends DoubleProperty { /// Create a diagnostics property for doubles that represent percentages or /// fractions. /// /// Setting [showName] to false is often reasonable for [PercentProperty] /// objects, as the fact that the property is shown as a percentage tends to /// be sufficient to disambiguate its meaning. /// /// The [showName] and [level] arguments must not be null. PercentProperty(String name, double fraction, { String ifNull, bool showName = true, String tooltip, String unit, DiagnosticLevel level = DiagnosticLevel.info, }) : assert(showName != null), assert(level != null), super( name, fraction, ifNull: ifNull, showName: showName, tooltip: tooltip, unit: unit, level: level, ); @override String valueToString({ TextTreeConfiguration parentConfiguration }) { if (value == null) return value.toString(); return unit != null ? '${numberToString()} $unit' : numberToString(); } @override String numberToString() { if (value == null) return value.toString(); return '${(value.clamp(0.0, 1.0) * 100.0).toStringAsFixed(1)}%'; } } /// Property where the description is either [ifTrue] or [ifFalse] depending on /// whether [value] is true or false. /// /// Using [FlagProperty] instead of [DiagnosticsProperty<bool>] can make /// diagnostics display more polished. For example, given a property named /// `visible` that is typically true, the following code will return 'hidden' /// when `visible` is false and nothing when visible is true, in contrast to /// `visible: true` or `visible: false`. /// /// ## Sample code /// /// ```dart /// new FlagProperty( /// 'visible', /// value: true, /// ifFalse: 'hidden', /// ) /// ``` /// /// [FlagProperty] should also be used instead of [DiagnosticsProperty<bool>] /// if showing the bool value would not clearly indicate the meaning of the /// property value. /// /// ```dart /// new FlagProperty( /// 'inherit', /// value: inherit, /// ifTrue: '<all styles inherited>', /// ifFalse: '<no style specified>', /// ) /// ``` /// /// See also: /// /// * [ObjectFlagProperty], which provides similar behavior describing whether /// a [value] is null. class FlagProperty extends DiagnosticsProperty<bool> { /// Constructs a FlagProperty with the given descriptions with the specified descriptions. /// /// [showName] defaults to false as typically [ifTrue] and [ifFalse] should /// be descriptions that make the property name redundant. /// /// The [showName] and [level] arguments must not be null. FlagProperty(String name, { @required bool value, this.ifTrue, this.ifFalse, bool showName = false, Object defaultValue, DiagnosticLevel level = DiagnosticLevel.info, }) : assert(showName != null), assert(level != null), assert(ifTrue != null || ifFalse != null), super( name, value, showName: showName, defaultValue: defaultValue, level: level, ); @override Map<String, Object> toJsonMap() { final Map<String, Object> json = super.toJsonMap(); if (ifTrue != null) json['ifTrue'] = ifTrue; if (ifFalse != null) json['ifFalse'] = ifFalse; return json; } /// 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]. final String 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]. final String ifFalse; @override String valueToString({ TextTreeConfiguration parentConfiguration }) { if (value == true) { if (ifTrue != null) return ifTrue; } else if (value == false) { if (ifFalse != null) return ifFalse; } return super.valueToString(parentConfiguration: parentConfiguration); } @override bool get showName { if (value == null || (value == true && ifTrue == null) || (value == false && ifFalse == null)) { // We are missing a description for the flag value so we need to show the // flag name. The property will have DiagnosticLevel.hidden for this case // so users will not see this the property in this case unless they are // displaying hidden properties. return true; } return super.showName; } @override DiagnosticLevel get level { if (value == true) { if (ifTrue == null) return DiagnosticLevel.hidden; } if (value == false) { if (ifFalse == null) return DiagnosticLevel.hidden; } return super.level; } } /// Property with an `Iterable<T>` [value] that can be displayed with /// different [DiagnosticsTreeStyle] for custom rendering. /// /// If [style] is [DiagnosticsTreeStyle.singleLine], the iterable is described /// as a comma separated list, otherwise the iterable is described as a line /// break separated list. class IterableProperty<T> extends DiagnosticsProperty<Iterable<T>> { /// Create a diagnostics property for iterables (e.g. lists). /// /// The [ifEmpty] argument is used to indicate how an iterable [value] with 0 /// elements is displayed. If [ifEmpty] equals null that indicates that an /// empty iterable [value] is not interesting to display similar to how /// [defaultValue] is used to indicate that a specific concrete value is not /// interesting to display. /// /// The [style], [showName], and [level] arguments must not be null. IterableProperty(String name, Iterable<T> value, { Object defaultValue = kNoDefaultValue, String ifNull, String ifEmpty = '[]', DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, bool showName = true, DiagnosticLevel level = DiagnosticLevel.info, }) : assert(style != null), assert(showName != null), assert(level != null), super( name, value, defaultValue: defaultValue, ifNull: ifNull, ifEmpty: ifEmpty, style: style, showName: showName, level: level, ); @override String valueToString({ TextTreeConfiguration parentConfiguration }) { if (value == null) return value.toString(); if (value.isEmpty) return ifEmpty ?? '[]'; if (parentConfiguration != null && !parentConfiguration.lineBreakProperties) { // Always display the value as a single line and enclose the iterable // value in brackets to avoid ambiguity. return '[${value.join(', ')}]'; } return value.join(style == DiagnosticsTreeStyle.singleLine ? ', ' : '\n'); } /// Priority level of the diagnostic used to control which diagnostics should /// be shown and filtered. /// /// If [ifEmpty] is null and the [value] is an empty [Iterable] then level /// [DiagnosticLevel.fine] is returned in a similar way to how an /// [ObjectFlagProperty] handles when [ifNull] is null and the [value] is /// null. @override DiagnosticLevel get level { if (ifEmpty == null && value != null && value.isEmpty && super.level != DiagnosticLevel.hidden) return DiagnosticLevel.fine; return super.level; } @override Map<String, Object> toJsonMap() { final Map<String, Object> json = super.toJsonMap(); if (value != null) { json['values'] = value.map<String>((T value) => value.toString()).toList(); } return json; } } /// An property than displays enum values tersely. /// /// The enum value is displayed with the class name stripped. For example: /// [HitTestBehavior.deferToChild] is shown as `deferToChild`. /// /// See also: /// /// * [DiagnosticsProperty] which documents named parameters common to all /// [DiagnosticsProperty] class EnumProperty<T> extends DiagnosticsProperty<T> { /// Create a diagnostics property that displays an enum. /// /// The [level] argument must also not be null. EnumProperty(String name, T value, { Object defaultValue = kNoDefaultValue, DiagnosticLevel level = DiagnosticLevel.info, }) : assert(level != null), super ( name, value, defaultValue: defaultValue, level: level, ); @override String valueToString({ TextTreeConfiguration parentConfiguration }) { if (value == null) return value.toString(); return describeEnum(value); } } /// A property where the important diagnostic information is primarily whether /// the [value] is present (non-null) or absent (null), rather than the actual /// value of the property itself. /// /// The [ifPresent] and [ifNull] strings describe the property [value] when it /// is non-null and null respectively. If one of [ifPresent] or [ifNull] is /// omitted, that is taken to mean that [level] should be /// [DiagnosticsLevel.hidden] when [value] is non-null or null respectively. /// /// This kind of diagnostics property is typically used for values mostly opaque /// values, like closures, where presenting the actual object is of dubious /// value but where reporting the presence or absence of the value is much more /// useful. /// /// See also: /// /// * [FlagProperty], which provides similar functionality describing whether /// a [value] is true or false. class ObjectFlagProperty<T> extends DiagnosticsProperty<T> { /// Create a diagnostics property for values that can be present (non-null) or /// absent (null), but for which the exact value's [Object.toString] /// representation is not very transparent (e.g. a callback). /// /// The [showName] and [level] arguments must not be null. Additionally, at /// least one of [ifPresent] and [ifNull] must not be null. ObjectFlagProperty(String name, T value, { this.ifPresent, String ifNull, bool showName = false, DiagnosticLevel level = DiagnosticLevel.info, }) : assert(ifPresent != null || ifNull != null), assert(showName != null), assert(level != null), super( name, value, showName: showName, ifNull: ifNull, level: level, ); /// Shorthand constructor to describe whether the property has a value. /// /// Only use if prefixing the property name with the word 'has' is a good /// flag name. /// /// The [name] and [level] arguments must not be null. ObjectFlagProperty.has( String name, T value, { DiagnosticLevel level = DiagnosticLevel.info, }) : assert(name != null), assert(level != null), ifPresent = 'has $name', super( name, value, showName: false, level: level, ); /// 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. final String ifPresent; @override String valueToString({ TextTreeConfiguration parentConfiguration }) { if (value != null) { if (ifPresent != null) return ifPresent; } else { if (ifNull != null) return ifNull; } return super.valueToString(parentConfiguration: parentConfiguration); } @override bool get showName { if ((value != null && ifPresent == null) || (value == null && ifNull == null)) { // We are missing a description for the flag value so we need to show the // flag name. The property will have DiagnosticLevel.hidden for this case // so users will not see this the property in this case unless they are // displaying hidden properties. return true; } return super.showName; } @override DiagnosticLevel get level { if (value != null) { if (ifPresent == null) return DiagnosticLevel.hidden; } else { if (ifNull == null) return DiagnosticLevel.hidden; } return super.level; } @override Map<String, Object> toJsonMap() { final Map<String, Object> json = super.toJsonMap(); if (ifPresent != null) json['ifPresent'] = ifPresent; return json; } } /// Signature for computing the value of a property. /// /// May throw exception if accessing the property would throw an exception /// and callers must handle that case gracefully. For example, accessing a /// property may trigger an assert that layout constraints were violated. typedef T ComputePropertyValueCallback<T>(); /// Property with a [value] of type [T]. /// /// If the default `value.toString()` does not provide an adequate description /// of the value, specify `description` defining a custom description. /// /// The [showSeparator] property indicates whether a separator should be placed /// between the property [name] and its [value]. class DiagnosticsProperty<T> extends DiagnosticsNode { /// Create a diagnostics property. /// /// The [showName], [showSeparator], [style], [missingIfNull], and [level] /// arguments must not be null. /// /// The [level] argument is just a suggestion and can be overridden if /// something else about the property causes it to have a lower or higher /// level. For example, if the property value is null and [missingIfNull] is /// true, [level] is raised to [DiagnosticLevel.warning]. DiagnosticsProperty( String name, T value, { String description, String ifNull, this.ifEmpty, bool showName = true, bool showSeparator = true, this.defaultValue = kNoDefaultValue, this.tooltip, this.missingIfNull = false, DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, DiagnosticLevel level = DiagnosticLevel.info, }) : assert(showName != null), assert(showSeparator != null), assert(style != null), assert(level != null), _description = description, _valueComputed = true, _value = value, _computeValue = null, ifNull = ifNull ?? (missingIfNull ? 'MISSING' : null), _defaultLevel = level, super( name: name, showName: showName, showSeparator: showSeparator, style: style, ); /// Property with a [value] that is computed only when needed. /// /// Use if computing the property [value] may throw an exception or is /// expensive. /// /// The [showName], [showSeparator], [style], [missingIfNull], and [level] /// arguments must not be null. /// /// The [level] argument is just a suggestion and can be overridden if /// if something else about the property causes it to have a lower or higher /// level. For example, if calling `computeValue` throws an exception, [level] /// will always return [DiagnosticLevel.error]. DiagnosticsProperty.lazy( String name, ComputePropertyValueCallback<T> computeValue, { String description, String ifNull, this.ifEmpty, bool showName = true, bool showSeparator = true, this.defaultValue = kNoDefaultValue, this.tooltip, this.missingIfNull = false, DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, DiagnosticLevel level = DiagnosticLevel.info, }) : assert(showName != null), assert(showSeparator != null), assert(defaultValue == kNoDefaultValue || defaultValue is T), assert(missingIfNull != null), assert(style != null), assert(level != null), _description = description, _valueComputed = false, _value = null, _computeValue = computeValue, _defaultLevel = level, ifNull = ifNull ?? (missingIfNull ? 'MISSING' : null), super( name: name, showName: showName, showSeparator: showSeparator, style: style, ); final String _description; @override Map<String, Object> toJsonMap() { final Map<String, Object> json = super.toJsonMap(); if (defaultValue != kNoDefaultValue) json['defaultValue'] = defaultValue.toString(); if (ifEmpty != null) json['ifEmpty'] = ifEmpty; if (ifNull != null) json['ifNull'] = ifNull; if (tooltip != null) json['tooltip'] = tooltip; json['missingIfNull'] = missingIfNull; if (exception != null) json['exception'] = exception.toString(); json['propertyType'] = propertyType.toString(); json['valueToString'] = valueToString(); json['defaultLevel'] = describeEnum(_defaultLevel); if (T is Diagnosticable) json['isDiagnosticableValue'] = true; return json; } /// Returns a string representation of the property value. /// /// Subclasses should override this method instead of [toDescription] to /// customize how property values are converted to strings. /// /// Overriding this method ensures that behavior controlling how property /// values are decorated to generate a nice [toDescription] are consistent /// across all implementations. Debugging tools may also choose to use /// [valueToString] directly instead of [toDescription]. /// /// `parentConfiguration` specifies how the parent is rendered as text art. /// For example, if the parent places all properties on one line, the value /// of the property should be displayed without line breaks if possible. String valueToString({ TextTreeConfiguration parentConfiguration }) { final T v = value; // DiagnosticableTree values are shown using the shorter toStringShort() // instead of the longer toString() because the toString() for a // DiagnosticableTree value is likely too large to be useful. return v is DiagnosticableTree ? v.toStringShort() : v.toString(); } @override String toDescription({ TextTreeConfiguration parentConfiguration }) { if (_description != null) return _addTooltip(_description); if (exception != null) return 'EXCEPTION (${exception.runtimeType})'; if (ifNull != null && value == null) return _addTooltip(ifNull); String result = valueToString(parentConfiguration: parentConfiguration); if (result.isEmpty && ifEmpty != null) result = ifEmpty; return _addTooltip(result); } /// If a [tooltip] is specified, add the tooltip it to the end of `text` /// enclosing it parenthesis to disambiguate the tooltip from the rest of /// the text. /// /// `text` must not be null. String _addTooltip(String text) { assert(text != null); return tooltip == null ? text : '$text ($tooltip)'; } /// Description if the property [value] is null. final String ifNull; /// Description if the property description would otherwise be empty. final String ifEmpty; /// 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. final String tooltip; /// Whether a [value] of null causes the property to have [level] /// [DiagnosticLevel.warning] warning that the property is missing a [value]. final bool missingIfNull; /// 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". Type get propertyType => T; /// Returns the value of the property either from cache or by invoking a /// [ComputePropertyValueCallback]. /// /// If an exception is thrown invoking the [ComputePropertyValueCallback], /// [value] returns null and the exception thrown can be found via the /// [exception] property. /// /// See also: /// /// * [valueToString], which converts the property value to a string. @override T get value { _maybeCacheValue(); return _value; } T _value; bool _valueComputed; Object _exception; /// Exception thrown if accessing the property [value] threw an exception. /// /// Returns null if computing the property value did not throw an exception. Object get exception { _maybeCacheValue(); return _exception; } void _maybeCacheValue() { if (_valueComputed) return; _valueComputed = true; assert(_computeValue != null); try { _value = _computeValue(); } catch (exception) { _exception = exception; _value = null; } } /// 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]. final Object defaultValue; DiagnosticLevel _defaultLevel; /// Priority level of the diagnostic used to control which diagnostics should /// be shown and filtered. /// /// The property level defaults to the value specified by the `level` /// constructor argument. The level is raised to [DiagnosticLevel.error] if /// an [exception] was thrown getting the property [value]. The level is /// raised to [DiagnosticLevel.warning] if the property [value] is null and /// the property is not allowed to be null due to [missingIfNull]. The /// priority level is lowered to [DiagnosticLevel.fine] if the property /// [value] equals [defaultValue]. @override DiagnosticLevel get level { if (_defaultLevel == DiagnosticLevel.hidden) return _defaultLevel; if (exception != null) return DiagnosticLevel.error; if (value == null && missingIfNull) return DiagnosticLevel.warning; // Use a low level when the value matches the default value. if (defaultValue != kNoDefaultValue && value == defaultValue) return DiagnosticLevel.fine; return _defaultLevel; } final ComputePropertyValueCallback<T> _computeValue; @override List<DiagnosticsNode> getProperties() => <DiagnosticsNode>[]; @override List<DiagnosticsNode> getChildren() => <DiagnosticsNode>[]; } /// [DiagnosticsNode] that lazily calls the associated [Diagnosticable] [value] /// to implement [getChildren] and [getProperties]. class DiagnosticableNode<T extends Diagnosticable> extends DiagnosticsNode { /// Create a diagnostics describing a [Diagnosticable] value. /// /// The [value] argument must not be null. DiagnosticableNode({ String name, @required this.value, @required DiagnosticsTreeStyle style, }) : assert(value != null), super( name: name, style: style, ); @override final T value; DiagnosticPropertiesBuilder _cachedBuilder; DiagnosticPropertiesBuilder get _builder { if (_cachedBuilder == null) { _cachedBuilder = new DiagnosticPropertiesBuilder(); value?.debugFillProperties(_cachedBuilder); } return _cachedBuilder; } @override DiagnosticsTreeStyle get style { return super.style ?? _builder.defaultDiagnosticsTreeStyle; } @override String get emptyBodyDescription => _builder.emptyBodyDescription; @override List<DiagnosticsNode> getProperties() => _builder.properties; @override List<DiagnosticsNode> getChildren() { return const<DiagnosticsNode>[]; } @override String toDescription({ TextTreeConfiguration parentConfiguration }) { return value.toStringShort(); } } /// [DiagnosticsNode] for an instance of [DiagnosticableTree]. class _DiagnosticableTreeNode extends DiagnosticableNode<DiagnosticableTree> { _DiagnosticableTreeNode({ String name, @required DiagnosticableTree value, @required DiagnosticsTreeStyle style, }) : super( name: name, value: value, style: style, ); @override List<DiagnosticsNode> getChildren() { if (value != null) return value.debugDescribeChildren(); return const <DiagnosticsNode>[]; } } /// Returns a 5 character long hexadecimal string generated from /// [Object.hashCode]'s 20 least-significant bits. String shortHash(Object object) { return object.hashCode.toUnsigned(20).toRadixString(16).padLeft(5, '0'); } /// Returns a summary of the runtime type and hash code of `object`. /// /// See also: /// /// * [Object.hashCode], a value used when placing an object in a [Map] or /// other similar data structure, and which is also used in debug output to /// distinguish instances of the same class (hash collisions are /// possible, but rare enough that its use in debug output is useful). /// * [Object.runtimeType], the [Type] of an object. String describeIdentity(Object object) => '${object.runtimeType}#${shortHash(object)}'; // This method exists as a workaround for https://github.com/dart-lang/sdk/issues/30021 /// Returns a short description of an enum value. /// /// Strips off the enum class name from the `enumEntry.toString()`. /// /// ## Sample code /// /// ```dart /// enum Day { /// monday, tuesday, wednesday, thursday, friday, saturday, sunday /// } /// /// void validateDescribeEnum() { /// assert(Day.monday.toString() == 'Day.monday'); /// assert(describeEnum(Day.monday) == 'monday'); /// } /// ``` String describeEnum(Object enumEntry) { final String description = enumEntry.toString(); final int indexOfDot = description.indexOf('.'); assert(indexOfDot != -1 && indexOfDot < description.length - 1); return description.substring(indexOfDot + 1); } /// Builder to accumulate properties and configuration used to assemble a /// [DiagnosticsNode] from a [Diagnosticable] object. class DiagnosticPropertiesBuilder { /// Add a property to the list of properties. void add(DiagnosticsNode property) { properties.add(property); } /// List of properties accumulated so far. final List<DiagnosticsNode> properties = <DiagnosticsNode>[]; /// Default style to use for the [DiagnosticsNode] if no style is specified. DiagnosticsTreeStyle defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.sparse; /// Description to show if the node has no displayed properties or children. String emptyBodyDescription; } // Examples can assume: // class ExampleSuperclass extends Diagnosticable { String message; double stepWidth; double scale; double paintExtent; double hitTestExtent; double paintExtend; double maxWidth; bool primary; double progress; int maxLines; Duration duration; int depth; dynamic boxShadow; dynamic style; bool hasSize; Matrix4 transform; Map<Listenable, VoidCallback> handles; Color color; bool obscureText; ImageRepeat repeat; Size size; Widget widget; bool isCurrent; bool keepAlive; TextAlign textAlign; } /// A base class for providing string and [DiagnosticsNode] debug /// representations describing the properties of an object. /// /// The string debug representation is generated from the intermediate /// [DiagnosticsNode] representation. The [DiagnosticsNode] representation is /// also used by debugging tools displaying interactive trees of objects and /// properties. /// /// See also: /// /// * [DiagnosticableTree], which extends this class to also describe the /// children of a tree structured object. /// * [Diagnosticable.debugFillProperties], which lists best practices /// for specifying the properties of a [DiagnosticNode]. The most common use /// case is to override [debugFillProperties] defining custom properties for /// a subclass of [TreeDiagnosticsMixin] using the existing /// [DiagnosticsProperty] subclasses. /// * [DiagnosticableTree.debugDescribeChildren], which lists best practices /// for describing the children of a [DiagnosticNode]. Typically the base /// class already describes the children of a node properly or a node has /// no children. /// * [DiagnosticsProperty], which should be used to create leaf diagnostic /// nodes without properties or children. There are many [DiagnosticProperty] /// subclasses to handle common use cases. abstract class Diagnosticable { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. const Diagnosticable(); /// A brief description of this object, usually just the [runtimeType] and the /// [hashCode]. /// /// See also: /// /// * [toString], for a detailed description of the object. String toStringShort() => describeIdentity(this); @override String toString({ DiagnosticLevel minLevel = DiagnosticLevel.debug }) { return toDiagnosticsNode(style: DiagnosticsTreeStyle.singleLine).toString(minLevel: minLevel); } /// Returns a debug representation of the object that is used by debugging /// tools and by [toStringDeep]. /// /// Leave [name] as null if there is not a meaningful description of the /// relationship between the this node and its parent. /// /// Typically the [style] argument is only specified to indicate an atypical /// relationship between the parent and the node. For example, pass /// [DiagnosticsTreeStyle.offstage] to indicate that a node is offstage. DiagnosticsNode toDiagnosticsNode({ String name, DiagnosticsTreeStyle style }) { return new DiagnosticableNode<Diagnosticable>( name: name, value: this, style: style, ); } /// Add additional properties associated with the node. /// /// Use the most specific [DiagnosticsProperty] existing subclass to describe /// each property instead of the [DiagnosticsProperty] base class. There are /// only a small number of [DiagnosticsProperty] subclasses each covering a /// common use case. Consider what values a property is relevant for users /// debugging as users debugging large trees are overloaded with information. /// Common named parameters in [DiagnosticsNode] subclasses help filter when /// and how properties are displayed. /// /// `defaultValue`, `showName`, `showSeparator`, and `level` keep string /// representations of diagnostics terse and hide properties when they are not /// very useful. /// /// * Use `defaultValue` any time the default value of a property is /// uninteresting. For example, specify a default value of null any time /// a property being null does not indicate an error. /// * Avoid specifying the `level` parameter unless the result you want /// cannot be achieved by using the `defaultValue` parameter or using /// the [ObjectFlagProperty] class to conditionally display the property /// as a flag. /// * Specify `showName` and `showSeparator` in rare cases where the string /// output would look clumsy if they were not set. /// ```dart /// new DiagnosticsProperty<Object>('child(3, 4)', null, ifNull: 'is null', showSeparator: false).toString() /// ``` /// Shows using `showSeparator` to get output `child(3, 4) is null` which /// is more polished than `child(3, 4): is null`. /// ```dart /// new DiagnosticsProperty<IconData>('icon', icon, ifNull: '<empty>', showName: false)).toString() /// ``` /// Shows using `showName` to omit the property name as in this context the /// property name does not add useful information. /// /// `ifNull`, `ifEmpty`, `unit`, and `tooltip` make property /// descriptions clearer. The examples in the code sample below illustrate /// good uses of all of these parameters. /// /// ## DiagnosticsProperty subclasses for primitive types /// /// * [StringProperty], which supports automatically enclosing a [String] /// value in quotes. /// * [DoubleProperty], which supports specifying a unit of measurement for /// a [double] value. /// * [PercentProperty], which clamps a [double] to between 0 and 1 and /// formats it as a percentage. /// * [IntProperty], which supports specifying a unit of measurement for an /// [int] value. /// * [FlagProperty], which formats a [bool] value as one or more flags. /// Depending on the use case it is better to format a bool as /// `DiagnosticsProperty<bool>` instead of using [FlagProperty] as the /// output is more verbose but unambiguous. /// /// ## Other important [DiagnosticsProperty] variants /// /// * [EnumProperty], which provides terse descriptions of enum values /// working around limitations of the `toString` implementation for Dart /// enum types. /// * [IterableProperty], which handles iterable values with display /// customizable depending on the [DiagnosticsTreeStyle] used. /// * [ObjectFlagProperty], which provides terse descriptions of whether a /// property value is present or not. For example, whether an `onClick` /// callback is specified or an animation is in progress. /// /// If none of these subclasses apply, use the [DiagnosticsProperty] /// constructor or in rare cases create your own [DiagnosticsProperty] /// subclass as in the case for [TransformProperty] which handles [Matrix4] /// that represent transforms. Generally any property value with a good /// `toString` method implementation works fine using [DiagnosticsProperty] /// directly. /// /// ## Sample code /// /// This example shows best practices for implementing [debugFillProperties] /// illustrating use of all common [DiagnosticsProperty] subclasses and all /// common [DiagnosticsProperty] parameters. /// /// ```dart /// class ExampleObject extends ExampleSuperclass { /// /// // ...various members and properties... /// /// @override /// void debugFillProperties(DiagnosticPropertiesBuilder properties) { /// // Always add properties from the base class first. /// super.debugFillProperties(properties); /// /// // Omit the property name 'message' when displaying this String property /// // as it would just add visual noise. /// properties.add(new StringProperty('message', message, showName: false)); /// /// properties.add(new DoubleProperty('stepWidth', stepWidth)); /// /// // A scale of 1.0 does nothing so should be hidden. /// properties.add(new DoubleProperty('scale', scale, defaultValue: 1.0)); /// /// // If the hitTestExtent matches the paintExtent, it is just set to its /// // default value so is not relevant. /// properties.add(new DoubleProperty('hitTestExtent', hitTestExtent, defaultValue: paintExtent)); /// /// // maxWidth of double.infinity indicates the width is unconstrained and /// // so maxWidth has no impact., /// properties.add(new DoubleProperty('maxWidth', maxWidth, defaultValue: double.infinity)); /// /// // Progress is a value between 0 and 1 or null. Showing it as a /// // percentage makes the meaning clear enough that the name can be /// // hidden. /// properties.add(new PercentProperty( /// 'progress', /// progress, /// showName: false, /// ifNull: '<indeterminate>', /// )); /// /// // Most text fields have maxLines set to 1. /// properties.add(new IntProperty('maxLines', maxLines, defaultValue: 1)); /// /// // Specify the unit as otherwise it would be unclear that time is in /// // milliseconds. /// properties.add(new IntProperty('duration', duration.inMilliseconds, unit: 'ms')); /// /// // Tooltip is used instead of unit for this case as a unit should be a /// // terse description appropriate to display directly after a number /// // without a space. /// properties.add(new DoubleProperty( /// 'device pixel ratio', /// ui.window.devicePixelRatio, /// tooltip: 'physical pixels per logical pixel', /// )); /// /// // Displaying the depth value would be distracting. Instead only display /// // if the depth value is missing. /// properties.add(new ObjectFlagProperty<int>('depth', depth, ifNull: 'no depth')); /// /// // bool flag that is only shown when the value is true. /// properties.add(new FlagProperty('using primary controller', value: primary)); /// /// properties.add(new FlagProperty( /// 'isCurrent', /// value: isCurrent, /// ifTrue: 'active', /// ifFalse: 'inactive', /// showName: false, /// )); /// /// properties.add(new DiagnosticsProperty<bool>('keepAlive', keepAlive)); /// /// // FlagProperty could have also been used in this case. /// // This option results in the text "obscureText: true" instead /// // of "obscureText" which is a bit more verbose but a bit clearer. /// properties.add(new DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false)); /// /// properties.add(new EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null)); /// properties.add(new EnumProperty<ImageRepeat>('repeat', repeat, defaultValue: ImageRepeat.noRepeat)); /// /// // Warn users when the widget is missing but do not show the value. /// properties.add(new ObjectFlagProperty<Widget>('widget', widget, ifNull: 'no widget')); /// /// properties.add(new IterableProperty<BoxShadow>( /// 'boxShadow', /// boxShadow, /// defaultValue: null, /// style: style, /// )); /// /// // Getting the value of size throws an exception unless hasSize is true. /// properties.add(new DiagnosticsProperty<Size>.lazy( /// 'size', /// () => size, /// description: '${ hasSize ? size : "MISSING" }', /// )); /// /// // If the `toString` method for the property value does not provide a /// // good terse description, write a DiagnosticsProperty subclass as in /// // the case of TransformProperty which displays a nice debugging view /// // of a Matrix4 that represents a transform. /// properties.add(new TransformProperty('transform', transform)); /// /// // If the value class has a good `toString` method, use /// // DiagnosticsProperty<YourValueType>. Specifying the value type ensures /// // that debugging tools always know the type of the field and so can /// // provide the right UI affordances. For example, in this case even /// // if color is null, a debugging tool still knows the value is a Color /// // and can display relevant color related UI. /// properties.add(new DiagnosticsProperty<Color>('color', color)); /// /// // Use a custom description to generate a more terse summary than the /// // `toString` method on the map class. /// properties.add(new DiagnosticsProperty<Map<Listenable, VoidCallback>>( /// 'handles', /// handles, /// description: handles != null ? /// '${handles.length} active client${ handles.length == 1 ? "" : "s" }' : /// null, /// ifNull: 'no notifications ever received', /// showName: false, /// )); /// } /// } /// ``` /// /// Used by [toDiagnosticsNode] and [toString]. @protected @mustCallSuper void debugFillProperties(DiagnosticPropertiesBuilder properties) { } } /// A base class for providing string and [DiagnosticsNode] debug /// representations describing the properties and children of an object. /// /// The string debug representation is generated from the intermediate /// [DiagnosticsNode] representation. The [DiagnosticsNode] representation is /// also used by debugging tools displaying interactive trees of objects and /// properties. /// /// See also: /// /// * [DiagnosticableTreeMixin], which provides a mixin that implements this /// class. /// * [Diagnosticable], which should be used instead of this class to provide /// diagnostics for objects without children. abstract class DiagnosticableTree extends Diagnosticable { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. const DiagnosticableTree(); /// Returns a one-line detailed description of the object. /// /// This description is often somewhat long. This includes the same /// information given by [toStringDeep], but does not recurse to any children. /// /// `joiner` specifies the string which is place between each part obtained /// from [debugFillProperties]. Passing a string such as `'\n '` will result /// in a multiline string that indents the properties of the object below its /// name (as per [toString]). /// /// `minLevel` specifies the minimum [DiagnosticLevel] for properties included /// in the output. /// /// See also: /// /// * [toString], for a brief description of the object. /// * [toStringDeep], for a description of the subtree rooted at this object. String toStringShallow({ String joiner = ', ', DiagnosticLevel minLevel = DiagnosticLevel.debug, }) { final StringBuffer result = new StringBuffer(); result.write(toString()); result.write(joiner); final DiagnosticPropertiesBuilder builder = new DiagnosticPropertiesBuilder(); debugFillProperties(builder); result.write( builder.properties.where((DiagnosticsNode n) => !n.isFiltered(minLevel)).join(joiner), ); return result.toString(); } /// Returns a string representation of this node and its descendants. /// /// `prefixLineOne` will be added to the front of the first line of the /// output. `prefixOtherLines` will be added to the front of each other line. /// If `prefixOtherLines` is null, the `prefixLineOne` is used for every line. /// By default, there is no prefix. /// /// `minLevel` specifies the minimum [DiagnosticLevel] for properties included /// in the output. /// /// The [toStringDeep] method takes other arguments, but those are intended /// for internal use when recursing to the descendants, and so can be ignored. /// /// See also: /// /// * [toString], for a brief description of the object but not its children. /// * [toStringShallow], for a detailed description of the object but not its /// children. String toStringDeep({ String prefixLineOne = '', String prefixOtherLines, DiagnosticLevel minLevel = DiagnosticLevel.debug, }) { return toDiagnosticsNode().toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines, minLevel: minLevel); } @override String toStringShort() => describeIdentity(this); @override DiagnosticsNode toDiagnosticsNode({ String name, DiagnosticsTreeStyle style }) { return new _DiagnosticableTreeNode( name: name, value: this, style: style, ); } /// Returns a list of [DiagnosticsNode] objects describing this node's /// children. /// /// Children that are offstage should be added with `style` set to /// [DiagnosticsTreeStyle.offstage] to indicate that they are offstage. /// /// The list must not contain any null entries. If there are explicit null /// children to report, consider [new DiagnosticsNode.message] or /// [DiagnosticsProperty<Object>] as possible [DiagnosticsNode] objects to /// provide. /// /// See also: /// /// * [RenderTable.debugDescribeChildren], which provides high quality custom /// descriptions for its child nodes. /// /// Used by [toStringDeep], [toDiagnosticsNode] and [toStringShallow]. @protected List<DiagnosticsNode> debugDescribeChildren() => const <DiagnosticsNode>[]; } /// A class that can be used as a mixin that helps dump string and /// [DiagnosticsNode] representations of trees. /// /// This class is identical to DiagnosticableTree except that it can be used as /// a mixin. abstract class DiagnosticableTreeMixin implements DiagnosticableTree { // This class is intended to be used as a mixin, and should not be // extended directly. factory DiagnosticableTreeMixin._() => null; @override String toString({ DiagnosticLevel minLevel = DiagnosticLevel.debug }) { return toDiagnosticsNode(style: DiagnosticsTreeStyle.singleLine).toString(minLevel: minLevel); } @override String toStringShallow({ String joiner = ', ', DiagnosticLevel minLevel = DiagnosticLevel.debug, }) { final StringBuffer result = new StringBuffer(); result.write(toStringShort()); result.write(joiner); final DiagnosticPropertiesBuilder builder = new DiagnosticPropertiesBuilder(); debugFillProperties(builder); result.write( builder.properties.where((DiagnosticsNode n) => !n.isFiltered(minLevel)).join(joiner), ); return result.toString(); } @override String toStringDeep({ String prefixLineOne = '', String prefixOtherLines, DiagnosticLevel minLevel = DiagnosticLevel.debug, }) { return toDiagnosticsNode().toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines, minLevel: minLevel); } @override String toStringShort() => describeIdentity(this); @override DiagnosticsNode toDiagnosticsNode({ String name, DiagnosticsTreeStyle style }) { return new _DiagnosticableTreeNode( name: name, value: this, style: style, ); } @override List<DiagnosticsNode> debugDescribeChildren() => const <DiagnosticsNode>[]; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { } }