// 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 'dart:math' as math; import 'package:meta/meta.dart'; import 'assertions.dart'; import 'debug.dart'; // Examples can assume: // int rows, columns; // String _name; // bool inherit; /// 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 provide a hint about best practices. /// /// For example, a diagnostic describing best practices for fixing an error. hint, /// Diagnostics that summarize other diagnostics present. /// /// For example, use this level for a short one or two line summary /// describing other diagnostics present. summary, /// 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, /// Style for displaying content describing an error. /// /// See also: /// /// * [FlutterError], which uses this style for the root node in a tree /// describing an error. error, /// Render the tree just using whitespace without connecting parents to /// children using lines. /// /// See also: /// /// * [SliverGeometry], which uses this style. whitespace, /// Render the tree without indenting children at all. /// /// See also: /// /// * [DiagnosticsStackTrace], which uses this style. flat, /// Render the tree on a single line without showing children. singleLine, /// Render the tree using a style appropriate for properties that are part /// of an error message. /// /// The name is placed on one line with the value and properties placed on /// the following line. /// /// See also: /// * [singleLine], which displays the same information but keeps the /// property and value on the same line. errorProperty, /// Render only the immediate properties of a node instead of the full tree. /// /// See also: /// /// * [DebugOverflowIndicator], which uses this style to display just the /// immediate children of a node. shallow, /// Render only the children of a node truncating before the tree becomes too /// large. truncateChildren, } /// 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.afterDescription = '', this.beforeProperties = '', this.afterProperties = '', this.mandatoryAfterProperties = '', this.propertySeparator = '', this.bodyIndent = '', this.footer = '', this.showChildren = true, this.addBlankLineIfNoChildren = true, this.isNameOnOwnLine = false, this.isBlankLineBetweenPropertiesAndChildren = true, this.beforeName = '', this.suffixLineOne = '', this.manditoryFooter = '', }) : 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(afterDescription != 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; /// Suffix to add to end of the first line to make its length match the footer. final String suffixLineOne; /// 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 before the name of the node. /// /// See [errorTextConfiguration] for an example of using this to achieve a /// custom line art style. final String beforeName; /// 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 if the node has a body. final String afterDescriptionIfBody; /// Text to add immediately after the description line of a node with /// properties and/or children. final String afterDescription; /// 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; /// Mandatory string to add after the properties of a node regardless of /// whether the node has any properties. /// /// See [headerLineTextConfiguration] for an example of using this field to /// add a colon at the end of the header line. final String mandatoryAfterProperties; /// 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; /// Footer to add even for root nodes. final String manditoryFooter; /// 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], uses this style for ASCII art display. final TextTreeConfiguration sparseTextConfiguration = 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 = 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], uses this style for ASCII art display. final TextTreeConfiguration denseTextConfiguration = 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], uses this style for ASCII art display. final TextTreeConfiguration transitionTextConfiguration = TextTreeConfiguration( prefixLineOne: '╞═╦══ ', prefixLastChildLineOne: '╘═╦══ ', prefixOtherLines: ' ║ ', footer: ' ╚═══════════', 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, ); /// Configuration that draws a box around a node ignoring the connection to the /// parents. /// /// If nested in a tree, this node is best displayed in the property box rather /// than as a traditional child. /// /// Used to draw a decorative box around detailed descriptions of an exception. /// /// Example: /// ``` /// ══╡ <name>: <description> ╞═════════════════════════════════════ /// <body> /// ... /// ├─<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>' /// ════════════════════════════════════════════════════════════════ /// ``` /// /// See also: /// /// * [DiagnosticsTreeStyle.error], uses this style for ASCII art display. final TextTreeConfiguration errorTextConfiguration = TextTreeConfiguration( prefixLineOne: '╞═╦', prefixLastChildLineOne: '╘═╦', prefixOtherLines: ' ║ ', footer: ' ╚═══════════', linkCharacter: '│', // Subtree boundaries are clear due to the border around the node so omit the // property prefix. propertyPrefixIfChildren: '', propertyPrefixNoChildren: '', prefixOtherLinesRootNode: '', beforeName: '══╡ ', suffixLineOne: ' ╞══', manditoryFooter: '═════', // 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], uses this style for ASCII art display. final TextTreeConfiguration whitespaceTextConfiguration = 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, ); /// Whitespace only configuration where children are not indented. /// /// Use this style when indentation is not needed to disambiguate parents from /// children as in the case of a [DiagnosticsStackTrace]. /// /// Example: /// ``` /// <parent_node> /// <name>: <description>: /// <properties> /// <children> /// <name>: <description>: /// <properties> /// <children> /// ``` /// /// See also: /// /// * [DiagnosticsTreeStyle.flat], uses this style for ASCII art display. final TextTreeConfiguration flatTextConfiguration = 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], uses this style for ASCII art display. final TextTreeConfiguration singleLineTextConfiguration = TextTreeConfiguration( propertySeparator: ', ', beforeProperties: '(', afterProperties: ')', prefixLineOne: '', prefixOtherLines: '', prefixLastChildLineOne: '', lineBreak: '', lineBreakProperties: false, addBlankLineIfNoChildren: false, showChildren: false, propertyPrefixIfChildren: ' ', propertyPrefixNoChildren: ' ', linkCharacter: '', prefixOtherLinesRootNode: '', ); /// Render the name on a line followed by the body and properties on the next /// line omitting the children. /// /// Example: /// ``` /// <name>: /// <description>(<property1>, <property2>, ..., <propertyN>) /// ``` /// /// See also: /// /// * [DiagnosticsTreeStyle.errorProperty], uses this style for ASCII art /// display. final TextTreeConfiguration errorPropertyTextConfiguration = TextTreeConfiguration( propertySeparator: ', ', beforeProperties: '(', afterProperties: ')', prefixLineOne: '', prefixOtherLines: '', prefixLastChildLineOne: '', lineBreak: '\n', lineBreakProperties: false, addBlankLineIfNoChildren: false, showChildren: false, propertyPrefixIfChildren: ' ', propertyPrefixNoChildren: ' ', linkCharacter: '', prefixOtherLinesRootNode: '', afterName: ':', isNameOnOwnLine: true, ); /// Render a node on multiple lines omitting children. /// /// Example: /// `<name>: <description> /// <property1> /// <property2> /// <propertyN>` /// /// See also: /// /// * [DiagnosticsTreeStyle.shallow] final TextTreeConfiguration shallowTextConfiguration = 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, showChildren: false, ); enum _WordWrapParseMode { inSpace, inWord, atBreak } /// 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({ @required this.prefixLineOne, @required String prefixOtherLines, this.wrapWidth}) : _prefixOtherLines = 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 get prefixOtherLines => _nextPrefixOtherLines ?? _prefixOtherLines; String _prefixOtherLines; set prefixOtherLines(String prefix) { _prefixOtherLines = prefix; _nextPrefixOtherLines = null; } String _nextPrefixOtherLines; void incrementPrefixOtherLines(String suffix, {@required bool updateCurrentLine}) { if (_currentLine.isEmpty || updateCurrentLine) { _prefixOtherLines = prefixOtherLines + suffix; _nextPrefixOtherLines = null; } else { _nextPrefixOtherLines = prefixOtherLines + suffix; } } final int wrapWidth; /// Buffer containing lines that have already been completely laid out. final StringBuffer _buffer = StringBuffer(); /// Buffer containing the current line that has not yet been wrapped. final StringBuffer _currentLine = StringBuffer(); /// List of pairs of integers indicating the start and end of each block of /// text within _currentLine that can be wrapped. final List<int> _wrappableRanges = <int>[]; /// Whether the string being built already has more than 1 line. bool get requiresMultipleLines => _numLines > 1 || (_numLines == 1 && _currentLine.isNotEmpty) || (_currentLine.length + _getCurrentPrefix(true).length > wrapWidth); bool get isCurrentLineEmpty => _currentLine.isEmpty; int _numLines = 0; void _finalizeLine(bool addTrailingLineBreak) { final bool firstLine = _buffer.isEmpty; final String text = _currentLine.toString(); _currentLine.clear(); if (_wrappableRanges.isEmpty) { // Fast path. There were no wrappable spans of text. _writeLine( text, includeLineBreak: addTrailingLineBreak, firstLine: firstLine, ); return; } final Iterable<String> lines = _wordWrapLine( text, _wrappableRanges, wrapWidth, startOffset: firstLine ? prefixLineOne.length : _prefixOtherLines.length, otherLineOffset: firstLine ? _prefixOtherLines.length : _prefixOtherLines.length, ); int i = 0; final int length = lines.length; for (String line in lines) { i++; _writeLine( line, includeLineBreak: addTrailingLineBreak || i < length, firstLine: firstLine, ); } _wrappableRanges.clear(); } /// Wraps the given string at the given width. /// /// Wrapping occurs at space characters (U+0020). /// /// This is not suitable for use with arbitrary Unicode text. For example, it /// doesn't implement UAX #14, can't handle ideographic text, doesn't hyphenate, /// and so forth. It is only intended for formatting error messages. /// /// This method wraps a sequence of text where only some spans of text can be /// used as wrap boundaries. static Iterable<String> _wordWrapLine(String message, List<int> wrapRanges, int width, { int startOffset = 0, int otherLineOffset = 0}) sync* { if (message.length + startOffset < width) { // Nothing to do. The line doesn't wrap. yield message; return; } int startForLengthCalculations = -startOffset; bool addPrefix = false; int index = 0; _WordWrapParseMode mode = _WordWrapParseMode.inSpace; int lastWordStart; int lastWordEnd; int start = 0; int currentChunk = 0; // This helper is called with increasing indexes. bool noWrap(int index) { while (true) { if (currentChunk >= wrapRanges.length) return true; if (index < wrapRanges[currentChunk + 1]) break; // Found nearest chunk. currentChunk+= 2; } return index < wrapRanges[currentChunk]; } while (true) { switch (mode) { case _WordWrapParseMode.inSpace: // at start of break point (or start of line); can't break until next break while ((index < message.length) && (message[index] == ' ')) index += 1; lastWordStart = index; mode = _WordWrapParseMode.inWord; break; case _WordWrapParseMode.inWord: // looking for a good break point. Treat all text while ((index < message.length) && (message[index] != ' ' || noWrap(index))) index += 1; mode = _WordWrapParseMode.atBreak; break; case _WordWrapParseMode.atBreak: // at start of break point if ((index - startForLengthCalculations > width) || (index == message.length)) { // we are over the width line, so break if ((index - startForLengthCalculations <= width) || (lastWordEnd == null)) { // we should use this point, because either it doesn't actually go over the // end (last line), or it does, but there was no earlier break point lastWordEnd = index; } final String line = message.substring(start, lastWordEnd); yield line; addPrefix = true; if (lastWordEnd >= message.length) return; // just yielded a line if (lastWordEnd == index) { // we broke at current position // eat all the wrappable spaces, then set our start point // Even if some of the spaces are not wrappable that is ok. while ((index < message.length) && (message[index] == ' ')) index += 1; start = index; mode = _WordWrapParseMode.inWord; } else { // we broke at the previous break point, and we're at the start of a new one assert(lastWordStart > lastWordEnd); start = lastWordStart; mode = _WordWrapParseMode.atBreak; } startForLengthCalculations = start - otherLineOffset; assert(addPrefix); lastWordEnd = null; } else { // save this break point, we're not yet over the line width lastWordEnd = index; // skip to the end of this break point mode = _WordWrapParseMode.inSpace; } break; } } } /// Write text ensuring the specified prefixes for the first and subsequent /// lines. /// /// If [allowWrap] is true, the text may be wrapped to stay within the /// allow `wrapWidth`. void write(String s, {bool allowWrap = false}) { if (s.isEmpty) return; final List<String> lines = s.split('\n'); for (int i = 0; i < lines.length; i += 1) { if (i > 0) { _finalizeLine(true); _updatePrefix(); } final String line = lines[i]; if (line.isNotEmpty) { if (allowWrap && wrapWidth != null) { final int wrapStart = _currentLine.length; final int wrapEnd = wrapStart + line.length; if (_wrappableRanges.isNotEmpty && _wrappableRanges.last == wrapStart) { // Extend last range. _wrappableRanges.last = wrapEnd; } else { _wrappableRanges..add(wrapStart)..add(wrapEnd); } } _currentLine.write(line); } } } void _updatePrefix() { if (_nextPrefixOtherLines != null) { _prefixOtherLines = _nextPrefixOtherLines; _nextPrefixOtherLines = null; } } void _writeLine( String line, { @required bool includeLineBreak, @required bool firstLine, }) { line = '${_getCurrentPrefix(firstLine)}$line'; _buffer.write(line.trimRight()); if (includeLineBreak) _buffer.write('\n'); _numLines++; } String _getCurrentPrefix(bool firstLine) { return _buffer.isEmpty ? prefixLineOne : (firstLine ? _prefixOtherLines : _prefixOtherLines); } /// Write lines assuming the lines obey the specified prefixes. Ensures that /// a newline is added if one is not present. void writeRawLines(String lines) { if (lines.isEmpty) return; if (_currentLine.isNotEmpty) { _finalizeLine(true); } assert (_currentLine.isEmpty); _buffer.write(lines); if (!lines.endsWith('\n')) _buffer.write('\n'); _numLines++; _updatePrefix(); } /// Finishes the current line with a stretched version of text. void writeStretched(String text, int targetLineLength) { write(text); final int currentLineLength = _currentLine.length + _getCurrentPrefix(_buffer.isEmpty).length; assert (_currentLine.length > 0); final int targetLength = targetLineLength - currentLineLength; if (targetLength > 0) { assert(text.isNotEmpty); final String lastChar = text[text.length - 1]; assert(lastChar != '\n'); _currentLine.write(lastChar * targetLength); } // Mark the entire line as not wrappable. _wrappableRanges.clear(); } String build() { if (_currentLine.isNotEmpty) _finalizeLine(false); return _buffer.toString(); } } class _NoDefaultValue { const _NoDefaultValue(); } /// Marker object indicating that a [DiagnosticsNode] has no default value. const _NoDefaultValue kNoDefaultValue = _NoDefaultValue(); bool _isSingleLine(DiagnosticsTreeStyle style) { return style == DiagnosticsTreeStyle.singleLine; } /// Renderer that creates ascii art representations of trees of /// [DiagnosticsNode] objects. /// /// See also: /// /// * [DiagnosticsNode.toStringDeep], which uses a [TextRender] to return a /// string representation of this node and its descendants. class TextTreeRenderer { /// Creates a [TextTreeRenderer] object with the given arguments specifying /// how the tree is rendered. /// /// Lines are wrapped to at the maximum of [wrapWidth] and the current indent /// plus [wrapWidthProperties] characters. This ensures that wrapping does not /// become too excessive when displaying very deep trees and that wrapping /// only occurs at the overall [wrapWidth] when the tree is not very indented. /// If [maxDescendentsTruncatableNode] is specified, [DiagnosticsNode] objects /// with `allowTruncate` set to `true` are truncated after including /// [maxDescendentsTruncatableNode] descendants of the node to be truncated. TextTreeRenderer({ DiagnosticLevel minLevel = DiagnosticLevel.debug, int wrapWidth = 100, int wrapWidthProperties = 65, int maxDescendentsTruncatableNode = -1, }) : assert(minLevel != null), _minLevel = minLevel, _wrapWidth = wrapWidth, _wrapWidthProperties = wrapWidthProperties, _maxDescendentsTruncatableNode = maxDescendentsTruncatableNode; final int _wrapWidth; final int _wrapWidthProperties; final DiagnosticLevel _minLevel; final int _maxDescendentsTruncatableNode; /// Text configuration to use to connect this node to a `child`. /// /// The singleLine styles are 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, ) { final DiagnosticsTreeStyle childStyle = child?.style; return (_isSingleLine(childStyle) || childStyle == DiagnosticsTreeStyle.errorProperty) ? textStyle : child.textTreeConfiguration; } /// Renders a [node] to a String. String render( DiagnosticsNode node, { String prefixLineOne = '', String prefixOtherLines, TextTreeConfiguration parentConfiguration, }) { final bool isSingleLine = _isSingleLine(node.style) && parentConfiguration?.lineBreakProperties != true; prefixOtherLines ??= prefixLineOne; if (node.linePrefix != null) { prefixLineOne += node.linePrefix; prefixOtherLines += node.linePrefix; } final TextTreeConfiguration config = node.textTreeConfiguration; if (prefixOtherLines.isEmpty) prefixOtherLines += config.prefixOtherLinesRootNode; if (node.style == DiagnosticsTreeStyle.truncateChildren) { // This style is different enough that it isn't worthwhile to reuse the // existing logic. final List<String> descendants = <String>[]; const int maxDepth = 5; int depth = 0; const int maxLines = 25; int lines = 0; void visitor(DiagnosticsNode node) { for (DiagnosticsNode child in node.getChildren()) { if (lines < maxLines) { depth += 1; descendants.add('$prefixOtherLines${" " * depth}$child'); if (depth < maxDepth) visitor(child); depth -= 1; } else if (lines == maxLines) { descendants.add('$prefixOtherLines ...(descendants list truncated after $lines lines)'); } lines += 1; } } visitor(node); final StringBuffer information = StringBuffer(prefixLineOne); if (lines > 1) { information.writeln('This ${node.name} had the following descendants (showing up to depth $maxDepth):'); } else if (descendants.length == 1) { information.writeln('This ${node.name} had the following child:'); } else { information.writeln('This ${node.name} has no descendants.'); } information.writeAll(descendants, '\n'); return information.toString(); } final _PrefixedStringBuilder builder = _PrefixedStringBuilder( prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines, wrapWidth: math.max(_wrapWidth, prefixOtherLines.length + _wrapWidthProperties), ); List<DiagnosticsNode> children = node.getChildren(); String description = node.toDescription(parentConfiguration: parentConfiguration); if (config.beforeName.isNotEmpty) { builder.write(config.beforeName); } final bool wrapName = !isSingleLine && node.allowNameWrap; final bool wrapDescription = !isSingleLine && node.allowWrap; final bool uppercaseTitle = node.style == DiagnosticsTreeStyle.error; String name = node.name; if (uppercaseTitle) { name = name?.toUpperCase(); } if (description == null || description.isEmpty) { if (node.showName && name != null) builder.write(name, allowWrap: wrapName); } else { bool includeName = false; if (name != null && name.isNotEmpty && node.showName) { includeName = true; builder.write(name, allowWrap: wrapName); if (node.showSeparator) builder.write(config.afterName, allowWrap: wrapName); builder.write( config.isNameOnOwnLine || description.contains('\n') ? '\n' : ' ', allowWrap: wrapName, ); } if (!isSingleLine && builder.requiresMultipleLines && !builder.isCurrentLineEmpty) { // Make sure there is a break between the current line and next one if // there is not one already. builder.write('\n'); } if (includeName) { builder.incrementPrefixOtherLines( children.isEmpty ? config.propertyPrefixNoChildren : config.propertyPrefixIfChildren, updateCurrentLine: true, ); } if (uppercaseTitle) { description = description.toUpperCase(); } builder.write(description.trimRight(), allowWrap: wrapDescription); if (!includeName) { builder.incrementPrefixOtherLines( children.isEmpty ? config.propertyPrefixNoChildren : config.propertyPrefixIfChildren, updateCurrentLine: false, ); } } if (config.suffixLineOne.isNotEmpty) { builder.writeStretched(config.suffixLineOne, builder.wrapWidth); } final Iterable<DiagnosticsNode> propertiesIterable = node.getProperties().where( (DiagnosticsNode n) => !n.isFiltered(_minLevel) ); List<DiagnosticsNode> properties; if (_maxDescendentsTruncatableNode >= 0 && node.allowTruncate) { if (propertiesIterable.length < _maxDescendentsTruncatableNode) { properties = propertiesIterable.take(_maxDescendentsTruncatableNode).toList(); properties.add(DiagnosticsNode.message('...')); } else { properties = propertiesIterable.toList(); } if (_maxDescendentsTruncatableNode < children.length) { children = children.take(_maxDescendentsTruncatableNode).toList(); children.add(DiagnosticsNode.message('...')); } } else { properties = propertiesIterable.toList(); } // If the node does not show a separator and there is no description then // we should not place a separator between the name and the value. // Essentially in this case the properties are treated a bit like a value. if ((properties.isNotEmpty || children.isNotEmpty || node.emptyBodyDescription != null) && (node.showSeparator || description?.isNotEmpty == true)) { builder.write(config.afterDescriptionIfBody); } if (config.lineBreakProperties) builder.write(config.lineBreak); if (properties.isNotEmpty) builder.write(config.beforeProperties); builder.incrementPrefixOtherLines(config.bodyIndent, updateCurrentLine: false); if (node.emptyBodyDescription != null && properties.isEmpty && children.isEmpty && prefixLineOne.isNotEmpty) { builder.write(node.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); final TextTreeConfiguration propertyStyle = property.textTreeConfiguration; if (_isSingleLine(property.style)) { // We have to treat single line properties slightly differently to deal // with cases where a single line properties output may not have single // linebreak. final String propertyRender = render(property, prefixLineOne: '${propertyStyle.prefixLineOne}', prefixOtherLines: '${propertyStyle.childLinkSpace}${propertyStyle.prefixOtherLines}', parentConfiguration: config, ); final List<String> propertyLines = propertyRender.split('\n'); if (propertyLines.length == 1 && !config.lineBreakProperties) { builder.write(propertyLines.first); } else { builder.write(propertyRender, allowWrap: false); if (!propertyRender.endsWith('\n')) builder.write('\n'); } } else { final String propertyRender = render(property, prefixLineOne: '${builder.prefixOtherLines}${propertyStyle.prefixLineOne}', prefixOtherLines: '${builder.prefixOtherLines}${propertyStyle.childLinkSpace}${propertyStyle.prefixOtherLines}', parentConfiguration: config, ); builder.writeRawLines(propertyRender); } } if (properties.isNotEmpty) builder.write(config.afterProperties); builder.write(config.mandatoryAfterProperties); if (!config.lineBreakProperties) builder.write(config.lineBreak); final String prefixChildren = '${config.bodyIndent}'; final String prefixChildrenRaw = '$prefixOtherLines$prefixChildren'; if (children.isEmpty && config.addBlankLineIfNoChildren && builder.requiresMultipleLines && builder.prefixOtherLines.trimRight().isNotEmpty ) { builder.write(config.lineBreak); } if (children.isNotEmpty && config.showChildren) { if (config.isBlankLineBetweenPropertiesAndChildren && properties.isNotEmpty && children.first.textTreeConfiguration.isBlankLineBetweenPropertiesAndChildren) { builder.write(config.lineBreak); } builder.prefixOtherLines = prefixOtherLines; 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 = '$prefixChildrenRaw${childConfig.prefixLastChildLineOne}'; final String childPrefixOtherLines = '$prefixChildrenRaw${childConfig.childLinkSpace}${childConfig.prefixOtherLines}'; builder.writeRawLines(render( child, prefixLineOne: lastChildPrefixLineOne, prefixOtherLines: childPrefixOtherLines, parentConfiguration: config, )); if (childConfig.footer.isNotEmpty) { builder.prefixOtherLines = prefixChildrenRaw; builder.write('${childConfig.childLinkSpace}${childConfig.footer}'); if (childConfig.manditoryFooter.isNotEmpty) { builder.writeStretched( childConfig.manditoryFooter, math.max(builder.wrapWidth, _wrapWidthProperties + childPrefixOtherLines.length), ); } builder.write(config.lineBreak); } } else { final TextTreeConfiguration nextChildStyle = _childTextConfiguration(children[i + 1], config); final String childPrefixLineOne = '$prefixChildrenRaw${childConfig.prefixLineOne}'; final String childPrefixOtherLines ='$prefixChildrenRaw${nextChildStyle.linkCharacter}${childConfig.prefixOtherLines}'; builder.writeRawLines(render( child, prefixLineOne: childPrefixLineOne, prefixOtherLines: childPrefixOtherLines, parentConfiguration: config, )); if (childConfig.footer.isNotEmpty) { builder.prefixOtherLines = prefixChildrenRaw; builder.write('${childConfig.linkCharacter}${childConfig.footer}'); if (childConfig.manditoryFooter.isNotEmpty) { builder.writeStretched( childConfig.manditoryFooter, math.max(builder.wrapWidth, _wrapWidthProperties + childPrefixOtherLines.length), ); } builder.write(config.lineBreak); } } } } if (parentConfiguration == null && config.manditoryFooter.isNotEmpty) { builder.writeStretched(config.manditoryFooter, builder.wrapWidth); builder.write(config.lineBreak); } return builder.build(); } } /// 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, this.linePrefix, }) : 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.\n' 'name:\n' ' "$name"' ); /// 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, bool allowWrap = true, }) { assert(style != null); assert(level != null); return DiagnosticsProperty<void>( '', null, description: message, style: style, showName: false, allowWrap: allowWrap, 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 [DiagnosticLevel.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; /// Prefix to include at the start of each line final String linePrefix; /// 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; /// Whether to wrap text on onto multiple lines or not. bool get allowWrap => false; /// Whether to wrap the name onto multiple lines or not. bool get allowNameWrap => false; /// Whether to allow truncation when displaying the node and its children. bool get allowTruncate => false; /// 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>{ 'description': toDescription(), 'type': runtimeType.toString(), }; if (name != null) data['name'] = name; if (!showSeparator) data['showSeparator'] = showSeparator; if (level != DiagnosticLevel.info) data['level'] = describeEnum(level); if (showName == false) data['showName'] = showName; if (emptyBodyDescription != null) data['emptyBodyDescription'] = emptyBodyDescription; if (style != DiagnosticsTreeStyle.sparse) data['style'] = describeEnum(style); if (allowTruncate) data['allowTruncate'] = allowTruncate; final bool hasChildren = getChildren().isNotEmpty; if (hasChildren) data['hasChildren'] = hasChildren; if (linePrefix?.isNotEmpty == true) data['linePrefix'] = linePrefix; if (!allowWrap) data['allowWrap'] = allowWrap; if (allowNameWrap) data['allowNameWrap'] = allowNameWrap; 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 (_isSingleLine(style)) 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; case DiagnosticsTreeStyle.errorProperty: return errorPropertyTextConfiguration; case DiagnosticsTreeStyle.shallow: return shallowTextConfiguration; case DiagnosticsTreeStyle.error: return errorTextConfiguration; case DiagnosticsTreeStyle.truncateChildren: // Truncate children doesn't really need its own text style as the // rendering is quite custom. return whitespaceTextConfiguration; case DiagnosticsTreeStyle.flat: return flatTextConfiguration; } return null; } /// 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. String toStringDeep({ String prefixLineOne = '', String prefixOtherLines, TextTreeConfiguration parentConfiguration, DiagnosticLevel minLevel = DiagnosticLevel.debug, }) { return TextTreeRenderer( minLevel: minLevel, wrapWidth: 65, wrapWidthProperties: 65, ).render( this, prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines, parentConfiguration: parentConfiguration, ); } } /// Debugging message displayed like a property. /// /// {@tool sample} /// /// 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 /// var table = MessageProperty('table size', '$columns\u00D7$rows'); /// var usefulness = MessageProperty('usefulness ratio', 'no metrics collected yet (never painted)'); /// ``` /// {@end-tool} /// {@tool sample} /// /// On the other hand, [StringProperty] is better suited when the property has a /// concrete value that is a string: /// /// ```dart /// var name = StringProperty('name', _name); /// ``` /// {@end-tool} /// /// 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<void> { /// 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, { DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, DiagnosticLevel level = DiagnosticLevel.info, }) : assert(name != null), assert(message != null), assert(style != null), assert(level != null), super(name, null, description: message, style: style, 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], [style], 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, DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, DiagnosticLevel level = DiagnosticLevel.info, }) : assert(showName != null), assert(quoted != null), assert(style != null), assert(level != null), super( name, value, description: description, defaultValue: defaultValue, tooltip: tooltip, showName: showName, ifEmpty: ifEmpty, style: style, 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, DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, DiagnosticLevel level = DiagnosticLevel.info, }) : super( name, value, ifNull: ifNull, showName: showName, defaultValue: defaultValue, tooltip: tooltip, level: level, style: style, ); _NumProperty.lazy( String name, ComputePropertyValueCallback<T> computeValue, { String ifNull, this.unit, bool showName = true, Object defaultValue = kNoDefaultValue, String tooltip, DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, DiagnosticLevel level = DiagnosticLevel.info, }) : super.lazy( name, computeValue, ifNull: ifNull, showName: showName, defaultValue: defaultValue, tooltip: tooltip, style: style, 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], [style], and [level] arguments must not be null. DoubleProperty( String name, double value, { String ifNull, String unit, String tooltip, Object defaultValue = kNoDefaultValue, bool showName = true, DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, DiagnosticLevel level = DiagnosticLevel.info, }) : assert(showName != null), assert(style != null), assert(level != null), super( name, value, ifNull: ifNull, unit: unit, tooltip: tooltip, defaultValue: defaultValue, showName: showName, style :style, 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() => debugFormatDouble(value); } /// 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], [style], and [level] arguments must not be null. IntProperty( String name, int value, { String ifNull, bool showName = true, String unit, Object defaultValue = kNoDefaultValue, DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine, DiagnosticLevel level = DiagnosticLevel.info, }) : assert(showName != null), assert(level != null), assert(style != 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`. /// /// {@tool sample} /// /// ```dart /// FlagProperty( /// 'visible', /// value: true, /// ifFalse: 'hidden', /// ) /// ``` /// {@end-tool} /// {@tool sample} /// /// [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 /// FlagProperty( /// 'inherit', /// value: inherit, /// ifTrue: '<all styles inherited>', /// ifFalse: '<no style specified>', /// ) /// ``` /// {@end-tool} /// /// 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], [showSeparator], 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, bool showSeparator = true, DiagnosticLevel level = DiagnosticLevel.info, }) : assert(style != null), assert(showName != null), assert(showSeparator != null), assert(level != null), super( name, value, defaultValue: defaultValue, ifNull: ifNull, ifEmpty: ifEmpty, style: style, showName: showName, showSeparator: showSeparator, 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(_isSingleLine(style) ? ', ' : '\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 /// [DiagnosticLevel.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 [DiagnosticLevel.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 ComputePropertyValueCallback<T> = T Function(); /// 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, String linePrefix, this.expandableValue = false, this.allowWrap = true, this.allowNameWrap = true, 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, linePrefix: linePrefix, ); /// 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, this.expandableValue = false, this.allowWrap = true, this.allowNameWrap = true, 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; /// Whether to expose properties and children of the value as properties and /// children. final bool expandableValue; @override final bool allowWrap; @override final bool allowNameWrap; @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['defaultLevel'] = describeEnum(_defaultLevel); if (value is Diagnosticable || value is DiagnosticsNode) 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() { if (expandableValue) { final T object = value; if (object is DiagnosticsNode) { return object.getProperties(); } if (object is Diagnosticable) { return object.toDiagnosticsNode(style: style).getProperties(); } } return const <DiagnosticsNode>[]; } @override List<DiagnosticsNode> getChildren() { if (expandableValue) { final T object = value; if (object is DiagnosticsNode) { return object.getChildren(); } if (object is Diagnosticable) { return object.toDiagnosticsNode(style: style).getChildren(); } } return const <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 = 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()`. /// /// {@tool sample} /// /// ```dart /// enum Day { /// monday, tuesday, wednesday, thursday, friday, saturday, sunday /// } /// /// void validateDescribeEnum() { /// assert(Day.monday.toString() == 'Day.monday'); /// assert(describeEnum(Day.monday) == 'monday'); /// } /// ``` /// {@end-tool} 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. /// * [DiagnosticableMixin], which provides the implementation for /// [Diagnosticable], and can be used to add diagnostics to classes which /// already have a base class. /// * [DiagnosticableMixin.debugFillProperties], which lists best practices /// for specifying the properties of a [DiagnosticsNode]. The most common use /// case is to override [debugFillProperties] defining custom properties for /// a subclass of [DiagnosticableTreeMixin] using the existing /// [DiagnosticsProperty] subclasses. /// * [DiagnosticableTree.debugDescribeChildren], which lists best practices /// for describing the children of a [DiagnosticsNode]. 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 /// [DiagnosticsProperty] subclasses to handle common use cases. abstract class Diagnosticable with DiagnosticableMixin { /// Abstract const constructor. This constructor enables subclasses to provide /// const constructors so that they can be used in const expressions. const Diagnosticable(); } /// A mixin class that provides the implementation for [Diagnosticable]. /// /// This mixin can be used to add diagnostics to a class which already has an /// base class. /// /// 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: /// /// * [debugFillProperties], which lists best practices for specifying the /// properties of a [DiagnosticsNode]. The most common use case is to /// override [debugFillProperties] defining custom properties for a subclass /// of [DiagnosticableTreeMixin] using the existing [DiagnosticsProperty] /// subclasses. /// * [DiagnosticableTree], which extends this class to also describe the /// children of a tree structured object. /// * [DiagnosticableTree.debugDescribeChildren], which lists best practices /// for describing the children of a [DiagnosticsNode]. 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 /// [DiagnosticsProperty] subclasses to handle common use cases. mixin DiagnosticableMixin { /// 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 }) { String fullString; assert(() { fullString = toDiagnosticsNode(style: DiagnosticsTreeStyle.singleLine).toString(minLevel: minLevel); return true; }()); return fullString ?? toStringShort(); } /// Returns a debug representation of the object that is used by debugging /// tools and by [DiagnosticsNode.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 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 /// 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 /// 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. /// /// {@tool sample} /// /// 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(StringProperty('message', message, showName: false)); /// /// properties.add(DoubleProperty('stepWidth', stepWidth)); /// /// // A scale of 1.0 does nothing so should be hidden. /// properties.add(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(DoubleProperty('hitTestExtent', hitTestExtent, defaultValue: paintExtent)); /// /// // maxWidth of double.infinity indicates the width is unconstrained and /// // so maxWidth has no impact., /// properties.add(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(PercentProperty( /// 'progress', /// progress, /// showName: false, /// ifNull: '<indeterminate>', /// )); /// /// // Most text fields have maxLines set to 1. /// properties.add(IntProperty('maxLines', maxLines, defaultValue: 1)); /// /// // Specify the unit as otherwise it would be unclear that time is in /// // milliseconds. /// properties.add(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(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(ObjectFlagProperty<int>('depth', depth, ifNull: 'no depth')); /// /// // bool flag that is only shown when the value is true. /// properties.add(FlagProperty('using primary controller', value: primary)); /// /// properties.add(FlagProperty( /// 'isCurrent', /// value: isCurrent, /// ifTrue: 'active', /// ifFalse: 'inactive', /// showName: false, /// )); /// /// properties.add(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(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false)); /// /// properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null)); /// properties.add(EnumProperty<ImageRepeat>('repeat', repeat, defaultValue: ImageRepeat.noRepeat)); /// /// // Warn users when the widget is missing but do not show the value. /// properties.add(ObjectFlagProperty<Widget>('widget', widget, ifNull: 'no widget')); /// /// properties.add(IterableProperty<BoxShadow>( /// 'boxShadow', /// boxShadow, /// defaultValue: null, /// style: style, /// )); /// /// // Getting the value of size throws an exception unless hasSize is true. /// properties.add(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(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(DiagnosticsProperty<Color>('color', color)); /// /// // Use a custom description to generate a more terse summary than the /// // `toString` method on the map class. /// properties.add(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, /// )); /// } /// } /// ``` /// {@end-tool} /// /// 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], 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 multi-line 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 = StringBuffer(); result.write(toString()); result.write(joiner); final DiagnosticPropertiesBuilder builder = 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 _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. /// /// Used by [toStringDeep], [toDiagnosticsNode] and [toStringShallow]. /// /// See also: /// /// * [RenderTable.debugDescribeChildren], which provides high quality custom /// descriptions for its child nodes. @protected List<DiagnosticsNode> debugDescribeChildren() => const <DiagnosticsNode>[]; } /// A mixin that helps dump string and [DiagnosticsNode] representations of trees. /// /// This mixin is identical to class [DiagnosticableTree]. mixin DiagnosticableTreeMixin implements DiagnosticableTree { @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 = StringBuffer(); result.write(toStringShort()); result.write(joiner); final DiagnosticPropertiesBuilder builder = 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 _DiagnosticableTreeNode( name: name, value: this, style: style, ); } @override List<DiagnosticsNode> debugDescribeChildren() => const <DiagnosticsNode>[]; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { } } /// [DiagnosticsNode] that exists mainly to provide a container for other /// diagnostics that typically lacks a meaningful value of its own. /// /// This class is typically used for displaying complex nested error messages. class DiagnosticsBlock extends DiagnosticsNode { /// Creates a diagnostic with properties specified by [properties] and /// children specified by [children]. DiagnosticsBlock({ String name, DiagnosticsTreeStyle style = DiagnosticsTreeStyle.whitespace, bool showName = true, bool showSeparator = true, String linePrefix, this.value, String description, this.level = DiagnosticLevel.info, this.allowTruncate = false, List<DiagnosticsNode> children = const<DiagnosticsNode>[], List<DiagnosticsNode> properties = const <DiagnosticsNode>[], }) : _description = description, _children = children, _properties = properties, super( name: name, style: style, showName: showName && name != null, showSeparator: showSeparator, linePrefix: linePrefix, ); final List<DiagnosticsNode> _children; final List<DiagnosticsNode> _properties; @override final DiagnosticLevel level; final String _description; @override final Object value; @override final bool allowTruncate; @override List<DiagnosticsNode> getChildren() => _children; @override List<DiagnosticsNode> getProperties() => _properties; @override String toDescription({TextTreeConfiguration parentConfiguration}) => _description; }