diagnostics.dart 124 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:math' as math;
6
import 'dart:ui' show clampDouble;
7

8 9
import 'package:meta/meta.dart';

10
import 'assertions.dart';
11
import 'constants.dart';
12
import 'debug.dart';
13
import 'object.dart';
14

15
// Examples can assume:
16 17 18
// late int rows, columns;
// late String _name;
// late bool inherit;
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
// abstract class ExampleSuperclass with Diagnosticable { }
// late String message;
// late double stepWidth;
// late double scale;
// late double hitTestExtent;
// late double paintExtent;
// late double maxWidth;
// late double progress;
// late int maxLines;
// late Duration duration;
// late int depth;
// late bool primary;
// late bool isCurrent;
// late bool keepAlive;
// late bool obscureText;
// late TextAlign textAlign;
// late ImageRepeat repeat;
// late Widget widget;
// late List<BoxShadow> boxShadow;
// late Size size;
// late bool hasSize;
// late Matrix4 transform;
// late Color color;
// late Map<Listenable, VoidCallback>? handles;
// late DiagnosticsTreeStyle style;
// late IconData icon;
45
// late double devicePixelRatio;
46

47 48 49 50 51 52
/// 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.
53 54 55
///
/// In release mode, this level may not have any effect, as diagnostics in
/// release mode are compacted or truncated to reduce binary size.
56 57 58 59 60
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
61
  /// they should expect them to sometimes be misleading. For example,
62
  /// [FlagProperty] and [ObjectFlagProperty] have uglier formatting when the
nt4f04uNd's avatar
nt4f04uNd committed
63
  /// property `value` does not match a value with a custom flag
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
  /// 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,

95 96 97 98 99 100 101 102 103 104 105
  /// 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,

106 107 108 109 110 111 112 113 114 115 116 117
  /// 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,
}
118

119 120
/// Styles for displaying a node in a [DiagnosticsNode] tree.
///
121 122 123
/// In release mode, these styles may be ignored, as diagnostics are compacted
/// or truncated to save on binary size.
///
124 125 126 127 128
/// See also:
///
///  * [DiagnosticsNode.toStringDeep], which dumps text art trees for these
///    styles.
enum DiagnosticsTreeStyle {
129 130 131
  /// A style that does not display the tree, for release mode.
  none,

132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
  /// 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,

164 165 166 167 168 169 170 171
  /// 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,

172 173 174 175 176 177 178 179
  /// Render the tree just using whitespace without connecting parents to
  /// children using lines.
  ///
  /// See also:
  ///
  ///  * [SliverGeometry], which uses this style.
  whitespace,

180 181 182 183 184 185 186
  /// Render the tree without indenting children at all.
  ///
  /// See also:
  ///
  ///  * [DiagnosticsStackTrace], which uses this style.
  flat,

187 188
  /// Render the tree on a single line without showing children.
  singleLine,
189 190 191 192 193 194 195 196

  /// 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:
197
  ///
198 199 200 201 202 203 204 205
  ///  * [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:
  ///
Dan Field's avatar
Dan Field committed
206 207
  ///  * [DebugOverflowIndicatorMixin], which uses this style to display just
  ///    the immediate children of a node.
208 209 210 211 212
  shallow,

  /// Render only the children of a node truncating before the tree becomes too
  /// large.
  truncateChildren,
213 214 215 216 217
}

/// Configuration specifying how a particular [DiagnosticsTreeStyle] should be
/// rendered as text art.
///
218 219 220
/// In release mode, these configurations may be ignored, as diagnostics are
/// compacted or truncated to save on binary size.
///
221 222 223
/// See also:
///
///  * [sparseTextConfiguration], which is a typical style.
224
///  * [transitionTextConfiguration], which is an example of a complex tree style.
225 226 227
///  * [DiagnosticsNode.toStringDeep], for code using [TextTreeConfiguration]
///    to render text art for arbitrary trees of [DiagnosticsNode] objects.
class TextTreeConfiguration {
228
  /// Create a configuration object describing how to render a tree as text.
229
  TextTreeConfiguration({
230 231 232 233 234 235 236
    required this.prefixLineOne,
    required this.prefixOtherLines,
    required this.prefixLastChildLineOne,
    required this.prefixOtherLinesRootNode,
    required this.linkCharacter,
    required this.propertyPrefixIfChildren,
    required this.propertyPrefixNoChildren,
237 238 239 240
    this.lineBreak = '\n',
    this.lineBreakProperties = true,
    this.afterName = ':',
    this.afterDescriptionIfBody = '',
241
    this.afterDescription = '',
242 243
    this.beforeProperties = '',
    this.afterProperties = '',
244
    this.mandatoryAfterProperties = '',
245 246 247 248 249 250 251
    this.propertySeparator = '',
    this.bodyIndent = '',
    this.footer = '',
    this.showChildren = true,
    this.addBlankLineIfNoChildren = true,
    this.isNameOnOwnLine = false,
    this.isBlankLineBetweenPropertiesAndChildren = true,
252 253
    this.beforeName = '',
    this.suffixLineOne = '',
254
    this.mandatoryFooter = '',
255
  }) : childLinkSpace = ' ' * linkCharacter.length;
256 257 258 259

  /// Prefix to add to the first line to display a child with this style.
  final String prefixLineOne;

260 261 262
  /// Suffix to add to end of the first line to make its length match the footer.
  final String suffixLineOne;

263 264 265
  /// Prefix to add to other lines to display a child with this style.
  ///
  /// [prefixOtherLines] should typically be one character shorter than
266
  /// [prefixLineOne] is.
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
  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;

307 308 309 310
  /// Whether to place line breaks between properties or to leave all
  /// properties on one line.
  final bool lineBreakProperties;

311 312 313 314 315 316 317

  /// 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;

318 319
  /// Text added immediately after the name of the node.
  ///
320 321
  /// See [transitionTextConfiguration] for an example of using a value other
  /// than ':' to achieve a custom line art style.
322 323 324
  final String afterName;

  /// Text to add immediately after the description line of a node with
325
  /// properties and/or children if the node has a body.
326 327
  final String afterDescriptionIfBody;

328 329 330 331
  /// Text to add immediately after the description line of a node with
  /// properties and/or children.
  final String afterDescription;

332 333 334 335 336 337 338 339 340 341 342 343
  /// 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;

344 345 346 347
  /// Mandatory string to add after the properties of a node regardless of
  /// whether the node has any properties.
  final String mandatoryAfterProperties;

348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
  /// 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.
  ///
376
  /// See [transitionTextConfiguration] for an example of using footer to draw a box
377 378 379
  /// around the node. [footer] is indented the same amount as [prefixOtherLines].
  final String footer;

380
  /// Footer to add even for root nodes.
381
  final String mandatoryFooter;
382

383 384 385 386 387 388 389
  /// Add a blank line between properties and children if both are present.
  final bool isBlankLineBetweenPropertiesAndChildren;
}

/// Default text tree configuration.
///
/// Example:
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412
///
///     <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>
413 414 415
///
/// See also:
///
416
///  * [DiagnosticsTreeStyle.sparse], uses this style for ASCII art display.
417
final TextTreeConfiguration sparseTextConfiguration = TextTreeConfiguration(
418 419 420 421 422 423 424 425 426 427 428 429 430
  prefixLineOne:            '├─',
  prefixOtherLines:         ' ',
  prefixLastChildLineOne:   '└─',
  linkCharacter:            '│',
  propertyPrefixIfChildren: '│ ',
  propertyPrefixNoChildren: '  ',
  prefixOtherLinesRootNode: ' ',
);

/// Identical to [sparseTextConfiguration] except that the lines connecting
/// parent to children are dashed.
///
/// Example:
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465
///
///     <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>
466 467 468
///
/// See also:
///
469
///  * [DiagnosticsTreeStyle.offstage], uses this style for ASCII art display.
470
final TextTreeConfiguration dashedTextConfiguration = TextTreeConfiguration(
471 472 473 474 475 476 477 478 479 480 481 482 483 484
  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:
485 486 487 488
///
///     <root_name>: <root_description>(<property1>; <property2> <propertyN>)
///     ├<child_name>: <child_description>(<property1>, <property2>, <propertyN>)
///     └<child_name>: <child_description>(<property1>, <property2>, <propertyN>)
489 490 491
///
/// See also:
///
492
///  * [DiagnosticsTreeStyle.dense], uses this style for ASCII art display.
493
final TextTreeConfiguration denseTextConfiguration = TextTreeConfiguration(
494 495 496 497
  propertySeparator: ', ',
  beforeProperties: '(',
  afterProperties: ')',
  lineBreakProperties: false,
498 499 500 501 502 503 504 505
  prefixLineOne:            '├',
  prefixOtherLines:         '',
  prefixLastChildLineOne:   '└',
  linkCharacter:            '│',
  propertyPrefixIfChildren: '│',
  propertyPrefixNoChildren: ' ',
  prefixOtherLinesRootNode: '',
  addBlankLineIfNoChildren: false,
506
  isBlankLineBetweenPropertiesAndChildren: false,
507 508 509 510 511 512 513 514
);

/// 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:
515 516 517 518 519 520 521 522 523 524 525 526
///
///     <parent_node>
///     ╞═╦══ <name> ═══
///     │ ║  <description>:
///     │ ║    <body>
///     │ ║    ...
///     │ ╚═══════════
///     ╘═╦══ <name> ═══
///       ║  <description>:
///       ║    <body>
///       ║    ...
///       ╚═══════════
527
///
528
/// See also:
529
///
530
///  * [DiagnosticsTreeStyle.transition], uses this style for ASCII art display.
531
final TextTreeConfiguration transitionTextConfiguration = TextTreeConfiguration(
532 533 534
  prefixLineOne:           '╞═╦══ ',
  prefixLastChildLineOne:  '╘═╦══ ',
  prefixOtherLines:         ' ║ ',
535
  footer:                   ' ╚═══════════',
536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556
  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,
);

557 558 559 560 561 562 563 564 565
/// 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:
566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595
///
///     ══╡ <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>'
///     ════════════════════════════════════════════════════════════════
596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612
///
/// 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:            ' ╞══',
613
  mandatoryFooter:          '═════',
614 615 616 617 618 619
  // No need to add a blank line as the footer makes the boundary of this
  // subtree unambiguous.
  addBlankLineIfNoChildren: false,
  isBlankLineBetweenPropertiesAndChildren: false,
);

620 621 622 623
/// Whitespace only configuration where children are consistently indented
/// two spaces.
///
/// Use this style for displaying properties with structured values or for
624
/// displaying children within a [transitionTextConfiguration] as using a style that
625 626 627
/// draws line art would be visually distracting for those cases.
///
/// Example:
628 629 630 631 632 633 634 635
///
///     <parent_node>
///       <name>: <description>:
///         <properties>
///         <children>
///       <name>: <description>:
///         <properties>
///         <children>
636 637 638
///
/// See also:
///
639
///  * [DiagnosticsTreeStyle.whitespace], uses this style for ASCII art display.
640
final TextTreeConfiguration whitespaceTextConfiguration = TextTreeConfiguration(
641 642 643 644 645 646 647 648 649 650 651 652 653 654
  prefixLineOne: '',
  prefixLastChildLineOne: '',
  prefixOtherLines: ' ',
  prefixOtherLinesRootNode: '  ',
  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,
);

655 656 657 658 659 660
/// 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:
661 662 663 664 665 666 667 668
///
///     <parent_node>
///     <name>: <description>:
///     <properties>
///     <children>
///     <name>: <description>:
///     <properties>
///     <children>
669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686
///
/// See also:
///
///  * [DiagnosticsTreeStyle.flat], uses this style for ASCII art display.
final TextTreeConfiguration flatTextConfiguration = TextTreeConfiguration(
  prefixLineOne: '',
  prefixLastChildLineOne: '',
  prefixOtherLines: '',
  prefixOtherLinesRootNode: '',
  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,
);
687 688 689 690 691 692 693
/// Render a node as a single line omitting children.
///
/// Example:
/// `<name>: <description>(<property1>, <property2>, ..., <propertyN>)`
///
/// See also:
///
694
///  * [DiagnosticsTreeStyle.singleLine], uses this style for ASCII art display.
695
final TextTreeConfiguration singleLineTextConfiguration = TextTreeConfiguration(
696 697 698 699 700 701 702
  propertySeparator: ', ',
  beforeProperties: '(',
  afterProperties: ')',
  prefixLineOne: '',
  prefixOtherLines: '',
  prefixLastChildLineOne: '',
  lineBreak: '',
703
  lineBreakProperties: false,
704 705
  addBlankLineIfNoChildren: false,
  showChildren: false,
706 707
  propertyPrefixIfChildren: '  ',
  propertyPrefixNoChildren: '  ',
708 709 710 711
  linkCharacter: '',
  prefixOtherLinesRootNode: '',
);

712 713 714 715
/// Render the name on a line followed by the body and properties on the next
/// line omitting the children.
///
/// Example:
716 717 718
///
///     <name>:
///       <description>(<property1>, <property2>, ..., <propertyN>)
719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769
///
/// See also:
///
///  * [DiagnosticsTreeStyle.errorProperty], uses this style for ASCII art
///    display.
final TextTreeConfiguration errorPropertyTextConfiguration = TextTreeConfiguration(
  propertySeparator: ', ',
  beforeProperties: '(',
  afterProperties: ')',
  prefixLineOne: '',
  prefixOtherLines: '',
  prefixLastChildLineOne: '',
  lineBreakProperties: false,
  addBlankLineIfNoChildren: false,
  showChildren: false,
  propertyPrefixIfChildren: '  ',
  propertyPrefixNoChildren: '  ',
  linkCharacter: '',
  prefixOtherLinesRootNode: '',
  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: '  ',
  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 }

770 771 772 773 774 775 776 777
/// 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 {
778
  _PrefixedStringBuilder({
779 780
    required this.prefixLineOne,
    required String? prefixOtherLines,
781 782
    this.wrapWidth,
  })  : _prefixOtherLines = prefixOtherLines;
783 784 785 786 787 788 789 790

  /// 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.
791 792 793
  String? get prefixOtherLines => _nextPrefixOtherLines ?? _prefixOtherLines;
  String? _prefixOtherLines;
  set prefixOtherLines(String? prefix) {
794 795 796
    _prefixOtherLines = prefix;
    _nextPrefixOtherLines = null;
  }
797

798 799
  String? _nextPrefixOtherLines;
  void incrementPrefixOtherLines(String suffix, {required bool updateCurrentLine}) {
800
    if (_currentLine.isEmpty || updateCurrentLine) {
801
      _prefixOtherLines = prefixOtherLines! + suffix;
802 803
      _nextPrefixOtherLines = null;
    } else {
804
      _nextPrefixOtherLines = prefixOtherLines! + suffix;
805 806 807
    }
  }

808
  final int? wrapWidth;
809 810

  /// Buffer containing lines that have already been completely laid out.
811
  final StringBuffer _buffer = StringBuffer();
812 813 814 815 816
  /// 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>[];
817 818

  /// Whether the string being built already has more than 1 line.
819
  bool get requiresMultipleLines => _numLines > 1 || (_numLines == 1 && _currentLine.isNotEmpty) ||
820
      (_currentLine.length + _getCurrentPrefix(true)!.length > wrapWidth!);
821

822
  bool get isCurrentLineEmpty => _currentLine.isEmpty;
823

824 825 826 827 828 829 830 831 832 833 834 835 836 837
  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,
      );
838 839
      return;
    }
840 841 842
    final Iterable<String> lines = _wordWrapLine(
      text,
      _wrappableRanges,
843 844
      wrapWidth!,
      startOffset: firstLine ? prefixLineOne.length : _prefixOtherLines!.length,
Tong Mu's avatar
Tong Mu committed
845
      otherLineOffset: _prefixOtherLines!.length,
846 847 848
    );
    int i = 0;
    final int length = lines.length;
849
    for (final String line in lines) {
850 851 852 853 854 855
      i++;
      _writeLine(
        line,
        includeLineBreak: addTrailingLineBreak || i < length,
        firstLine: firstLine,
      );
856
    }
857 858
    _wrappableRanges.clear();
  }
859

860 861 862 863 864 865 866 867 868 869
  /// 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.
870
  static Iterable<String> _wordWrapLine(String message, List<int> wrapRanges, int width, { int startOffset = 0, int otherLineOffset = 0}) {
871 872
    if (message.length + startOffset < width) {
      // Nothing to do. The line doesn't wrap.
873
      return <String>[message];
874
    }
875
    final List<String> wrappedLine = <String>[];
876 877 878 879
    int startForLengthCalculations = -startOffset;
    bool addPrefix = false;
    int index = 0;
    _WordWrapParseMode mode = _WordWrapParseMode.inSpace;
880 881
    late int lastWordStart;
    int? lastWordEnd;
882 883 884 885 886 887 888
    int start = 0;

    int currentChunk = 0;

    // This helper is called with increasing indexes.
    bool noWrap(int index) {
      while (true) {
889
        if (currentChunk >= wrapRanges.length) {
890
          return true;
891
        }
892

893
        if (index < wrapRanges[currentChunk + 1]) {
894
          break; // Found nearest chunk.
895
        }
896 897 898
        currentChunk+= 2;
      }
      return index < wrapRanges[currentChunk];
899
    }
900 901 902
    while (true) {
      switch (mode) {
        case _WordWrapParseMode.inSpace: // at start of break point (or start of line); can't break until next break
903
          while ((index < message.length) && (message[index] == ' ')) {
904
            index += 1;
905
          }
906 907 908
          lastWordStart = index;
          mode = _WordWrapParseMode.inWord;
        case _WordWrapParseMode.inWord: // looking for a good break point. Treat all text
909
          while ((index < message.length) && (message[index] != ' ' || noWrap(index))) {
910
            index += 1;
911
          }
912 913 914 915 916 917 918 919 920 921
          mode = _WordWrapParseMode.atBreak;
        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);
922
            wrappedLine.add(line);
923
            addPrefix = true;
924
            if (lastWordEnd >= message.length) {
925
              return wrappedLine;
926
            }
927 928 929 930 931
            // 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.
932
              while ((index < message.length) && (message[index] == ' ')) {
933
                index += 1;
934
              }
935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952
              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;
          }
      }
953
    }
954
  }
955

956 957 958 959 960 961
  /// 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}) {
962
    if (s.isEmpty) {
963
      return;
964
    }
965

966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992
    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;
    }
993 994
  }

995 996
  void _writeLine(
    String line, {
997 998
    required bool includeLineBreak,
    required bool firstLine,
999 1000 1001
  }) {
    line = '${_getCurrentPrefix(firstLine)}$line';
    _buffer.write(line.trimRight());
1002
    if (includeLineBreak) {
1003
      _buffer.write('\n');
1004
    }
1005
    _numLines++;
1006 1007
  }

1008
  String? _getCurrentPrefix(bool firstLine) {
Tong Mu's avatar
Tong Mu committed
1009
    return _buffer.isEmpty ? prefixLineOne : _prefixOtherLines;
1010
  }
1011

1012
  /// Write lines assuming the lines obey the specified prefixes. Ensures that
1013
  /// a newline is added if one is not present.
1014
  void writeRawLines(String lines) {
1015
    if (lines.isEmpty) {
1016
      return;
1017
    }
1018 1019 1020 1021 1022 1023 1024

    if (_currentLine.isNotEmpty) {
      _finalizeLine(true);
    }
    assert (_currentLine.isEmpty);

    _buffer.write(lines);
1025
    if (!lines.endsWith('\n')) {
1026
      _buffer.write('\n');
1027
    }
1028 1029
    _numLines++;
    _updatePrefix();
1030 1031
  }

1032 1033 1034
  /// Finishes the current line with a stretched version of text.
  void writeStretched(String text, int targetLineLength) {
    write(text);
1035
    final int currentLineLength = _currentLine.length + _getCurrentPrefix(_buffer.isEmpty)!.length;
1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048
    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() {
1049
    if (_currentLine.isNotEmpty) {
1050
      _finalizeLine(false);
1051
    }
1052 1053 1054

    return _buffer.toString();
  }
1055 1056 1057 1058 1059 1060
}

class _NoDefaultValue {
  const _NoDefaultValue();
}

1061
/// Marker object indicating that a [DiagnosticsNode] has no default value.
1062
const Object kNoDefaultValue = _NoDefaultValue();
1063

1064
bool _isSingleLine(DiagnosticsTreeStyle? style) {
1065 1066 1067
  return style == DiagnosticsTreeStyle.singleLine;
}

1068
/// Renderer that creates ASCII art representations of trees of
1069 1070 1071 1072
/// [DiagnosticsNode] objects.
///
/// See also:
///
1073
///  * [DiagnosticsNode.toStringDeep], which uses a [TextTreeRenderer] to return a
1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090
///    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,
1091
  }) : _minLevel = minLevel,
1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106
       _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.
1107
  TextTreeConfiguration? _childTextConfiguration(
1108 1109
    DiagnosticsNode child,
    TextTreeConfiguration textStyle,
1110
  ) {
1111
    final DiagnosticsTreeStyle? childStyle = child.style;
1112 1113 1114 1115 1116 1117 1118
    return (_isSingleLine(childStyle) || childStyle == DiagnosticsTreeStyle.errorProperty) ? textStyle : child.textTreeConfiguration;
  }

  /// Renders a [node] to a String.
  String render(
    DiagnosticsNode node, {
    String prefixLineOne = '',
1119 1120
    String? prefixOtherLines,
    TextTreeConfiguration? parentConfiguration,
1121
  }) {
1122 1123
    if (kReleaseMode) {
      return '';
1124 1125
    } else {
      return _debugRender(
1126 1127 1128 1129 1130
        node,
        prefixLineOne: prefixLineOne,
        prefixOtherLines: prefixOtherLines,
        parentConfiguration: parentConfiguration,
      );
1131
    }
1132 1133 1134 1135 1136
  }

  String _debugRender(
    DiagnosticsNode node, {
    String prefixLineOne = '',
1137 1138
    String? prefixOtherLines,
    TextTreeConfiguration? parentConfiguration,
1139
  }) {
1140 1141 1142
    final bool isSingleLine = _isSingleLine(node.style) && parentConfiguration?.lineBreakProperties != true;
    prefixOtherLines ??= prefixLineOne;
    if (node.linePrefix != null) {
1143 1144
      prefixLineOne += node.linePrefix!;
      prefixOtherLines += node.linePrefix!;
1145 1146
    }

1147
    final TextTreeConfiguration config = node.textTreeConfiguration!;
1148
    if (prefixOtherLines.isEmpty) {
1149
      prefixOtherLines += config.prefixOtherLinesRootNode;
1150
    }
1151 1152 1153 1154 1155 1156 1157 1158 1159 1160

    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) {
1161
        for (final DiagnosticsNode child in node.getChildren()) {
1162 1163 1164
          if (lines < maxLines) {
            depth += 1;
            descendants.add('$prefixOtherLines${"  " * depth}$child');
1165
            if (depth < maxDepth) {
1166
              visitor(child);
1167
            }
1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194
            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();

1195
    String description = node.toDescription(parentConfiguration: parentConfiguration);
1196 1197 1198 1199 1200 1201
    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;
1202
    String? name = node.name;
1203 1204 1205
    if (uppercaseTitle) {
      name = name?.toUpperCase();
    }
1206
    if (description.isEmpty) {
1207
      if (node.showName && name != null) {
1208
        builder.write(name, allowWrap: wrapName);
1209
      }
1210 1211 1212 1213 1214
    } else {
      bool includeName = false;
      if (name != null && name.isNotEmpty && node.showName) {
        includeName = true;
        builder.write(name, allowWrap: wrapName);
1215
        if (node.showSeparator) {
1216
          builder.write(config.afterName, allowWrap: wrapName);
1217
        }
1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248

        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) {
1249
      builder.writeStretched(config.suffixLineOne, builder.wrapWidth!);
1250 1251 1252
    }

    final Iterable<DiagnosticsNode> propertiesIterable = node.getProperties().where(
1253
            (DiagnosticsNode n) => !n.isFiltered(_minLevel),
1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275
    );
    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) &&
Ian Hickson's avatar
Ian Hickson committed
1276
        (node.showSeparator || description.isNotEmpty)) {
1277 1278 1279
      builder.write(config.afterDescriptionIfBody);
    }

1280
    if (config.lineBreakProperties) {
1281
      builder.write(config.lineBreak);
1282
    }
1283

1284
    if (properties.isNotEmpty) {
1285
      builder.write(config.beforeProperties);
1286
    }
1287 1288 1289 1290 1291 1292 1293

    builder.incrementPrefixOtherLines(config.bodyIndent, updateCurrentLine: false);

    if (node.emptyBodyDescription != null &&
        properties.isEmpty &&
        children.isEmpty &&
        prefixLineOne.isNotEmpty) {
1294
      builder.write(node.emptyBodyDescription!);
1295
      if (config.lineBreakProperties) {
1296
        builder.write(config.lineBreak);
1297
      }
1298 1299 1300 1301
    }

    for (int i = 0; i < properties.length; ++i) {
      final DiagnosticsNode property = properties[i];
1302
      if (i > 0) {
1303
        builder.write(config.propertySeparator);
1304
      }
1305

1306
      final TextTreeConfiguration propertyStyle = property.textTreeConfiguration!;
1307 1308 1309 1310 1311
      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,
1312
          prefixLineOne: propertyStyle.prefixLineOne,
1313 1314 1315 1316 1317 1318 1319
          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 {
1320
          builder.write(propertyRender);
1321
          if (!propertyRender.endsWith('\n')) {
1322
            builder.write('\n');
1323
          }
1324
        }
1325
      } else {
1326 1327 1328 1329 1330 1331 1332 1333
        final String propertyRender = render(property,
          prefixLineOne: '${builder.prefixOtherLines}${propertyStyle.prefixLineOne}',
          prefixOtherLines: '${builder.prefixOtherLines}${propertyStyle.childLinkSpace}${propertyStyle.prefixOtherLines}',
          parentConfiguration: config,
        );
        builder.writeRawLines(propertyRender);
      }
    }
1334
    if (properties.isNotEmpty) {
1335
      builder.write(config.afterProperties);
1336
    }
1337 1338 1339

    builder.write(config.mandatoryAfterProperties);

1340
    if (!config.lineBreakProperties) {
1341
      builder.write(config.lineBreak);
1342
    }
1343

1344
    final String prefixChildren = config.bodyIndent;
1345 1346 1347 1348
    final String prefixChildrenRaw = '$prefixOtherLines$prefixChildren';
    if (children.isEmpty &&
        config.addBlankLineIfNoChildren &&
        builder.requiresMultipleLines &&
1349
        builder.prefixOtherLines!.trimRight().isNotEmpty
1350 1351 1352 1353 1354 1355 1356
    ) {
      builder.write(config.lineBreak);
    }

    if (children.isNotEmpty && config.showChildren) {
      if (config.isBlankLineBetweenPropertiesAndChildren &&
          properties.isNotEmpty &&
1357
          children.first.textTreeConfiguration!.isBlankLineBetweenPropertiesAndChildren) {
1358 1359 1360 1361 1362 1363 1364
        builder.write(config.lineBreak);
      }

      builder.prefixOtherLines = prefixOtherLines;

      for (int i = 0; i < children.length; i++) {
        final DiagnosticsNode child = children[i];
1365
        final TextTreeConfiguration childConfig = _childTextConfiguration(child, config)!;
1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377
        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}');
1378
            if (childConfig.mandatoryFooter.isNotEmpty) {
1379
              builder.writeStretched(
1380
                childConfig.mandatoryFooter,
1381
                math.max(builder.wrapWidth!, _wrapWidthProperties + childPrefixOtherLines.length),
1382 1383 1384 1385 1386
              );
            }
            builder.write(config.lineBreak);
          }
        } else {
1387
          final TextTreeConfiguration nextChildStyle = _childTextConfiguration(children[i + 1], config)!;
1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398
          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}');
1399
            if (childConfig.mandatoryFooter.isNotEmpty) {
1400
              builder.writeStretched(
1401
                childConfig.mandatoryFooter,
1402
                math.max(builder.wrapWidth!, _wrapWidthProperties + childPrefixOtherLines.length),
1403 1404 1405 1406 1407 1408 1409
              );
            }
            builder.write(config.lineBreak);
          }
        }
      }
    }
1410
    if (parentConfiguration == null && config.mandatoryFooter.isNotEmpty) {
1411
      builder.writeStretched(config.mandatoryFooter, builder.wrapWidth!);
1412 1413 1414 1415 1416 1417
      builder.write(config.lineBreak);
    }
    return builder.build();
  }
}

1418
/// Defines diagnostics data for a [value].
1419
///
1420
/// For debug and profile modes, [DiagnosticsNode] provides a high quality
1421
/// multiline string dump via [toStringDeep]. The core members are the [name],
1422 1423 1424 1425 1426 1427
/// [toDescription], [getProperties], [value], and [getChildren]. All other
/// members exist typically to provide hints for how [toStringDeep] and
/// debugging tools should format output.
///
/// In release mode, far less information is retained and some information may
/// not print at all.
1428
abstract class DiagnosticsNode {
1429 1430
  /// Initializes the object.
  ///
1431 1432
  /// The [style], [showName], and [showSeparator] arguments must not
  /// be null.
1433
  DiagnosticsNode({
1434
    required this.name,
1435
    this.style,
1436 1437
    this.showName = true,
    this.showSeparator = true,
1438
    this.linePrefix,
1439 1440 1441 1442
  }) : assert(
         // A name ending with ':' indicates that the user forgot that the ':' will
         // be automatically added for them when generating descriptions of the
         // property.
1443 1444 1445
         name == null || !name.endsWith(':'),
         'Names of diagnostic nodes must not end with colons.\n'
         'name:\n'
1446
         '  "$name"',
1447
       );
1448 1449 1450 1451 1452 1453

  /// Diagnostics containing just a string `message` and not a concrete name or
  /// value.
  ///
  /// See also:
  ///
1454
  ///  * [MessageProperty], which is better suited to messages that are to be
1455 1456 1457
  ///    formatted like a property with a separate name and message.
  factory DiagnosticsNode.message(
    String message, {
1458 1459
    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
    DiagnosticLevel level = DiagnosticLevel.info,
1460
    bool allowWrap = true,
1461
  }) {
1462
    return DiagnosticsProperty<void>(
1463 1464 1465 1466 1467
      '',
      null,
      description: message,
      style: style,
      showName: false,
1468
      allowWrap: allowWrap,
1469
      level: level,
1470 1471 1472
    );
  }

1473 1474 1475 1476
  /// Label describing the [DiagnosticsNode], typically shown before a separator
  /// (see [showSeparator]).
  ///
  /// The name will be omitted if the [showName] property is false.
1477
  final String? name;
1478

1479 1480 1481 1482 1483 1484
  /// 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.
Ian Hickson's avatar
Ian Hickson committed
1485
  String toDescription({ TextTreeConfiguration? parentConfiguration });
1486

1487
  /// Whether to show a separator between [name] and description.
1488 1489 1490 1491 1492
  ///
  /// If false, name and description should be shown with no separation.
  /// `:` is typically used as a separator when displaying as text.
  final bool showSeparator;

1493 1494 1495 1496
  /// Whether the diagnostic should be filtered due to its [level] being lower
  /// than `minLevel`.
  ///
  /// If `minLevel` is [DiagnosticLevel.hidden] no diagnostics will be filtered.
1497
  /// If `minLevel` is [DiagnosticLevel.off] all diagnostics will be filtered.
1498
  bool isFiltered(DiagnosticLevel minLevel) => kReleaseMode || level.index < minLevel.index;
1499 1500 1501 1502 1503 1504

  /// 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
1505
  /// subclasses have a [level] argument to their constructor which influences
1506 1507 1508
  /// 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.
1509
  DiagnosticLevel get level => kReleaseMode ? DiagnosticLevel.hidden : DiagnosticLevel.info;
1510

1511 1512 1513 1514 1515
  /// 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.
1516 1517
  final bool showName;

1518
  /// Prefix to include at the start of each line.
1519
  final String? linePrefix;
1520

1521
  /// Description to show if the node has no displayed properties or children.
1522
  String? get emptyBodyDescription => null;
1523

1524
  /// The actual object this is diagnostics data for.
1525
  Object? get value;
1526 1527

  /// Hint for how the node should be displayed.
1528
  final DiagnosticsTreeStyle? style;
1529

1530 1531 1532 1533 1534 1535 1536 1537 1538
  /// 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;

1539
  /// Properties of this [DiagnosticsNode].
1540 1541
  ///
  /// Properties and children are kept distinct even though they are both
1542
  /// [List<DiagnosticsNode>] because they should be grouped differently.
1543 1544
  List<DiagnosticsNode> getProperties();

1545
  /// Children of this [DiagnosticsNode].
1546 1547 1548
  ///
  /// See also:
  ///
1549 1550
  ///  * [getProperties], which returns the properties of the [DiagnosticsNode]
  ///    object.
1551 1552 1553 1554
  List<DiagnosticsNode> getChildren();

  String get _separator => showSeparator ? ':' : '';

Ian Hickson's avatar
Ian Hickson committed
1555 1556 1557 1558 1559 1560
  /// Converts the properties ([getProperties]) of this node to a form useful
  /// for [Timeline] event arguments (as in [Timeline.startSync]).
  ///
  /// Children ([getChildren]) are omitted.
  ///
  /// This method is only valid in debug builds. In profile builds, this method
1561
  /// throws an exception. In release builds it returns null.
Ian Hickson's avatar
Ian Hickson committed
1562 1563 1564 1565 1566
  ///
  /// See also:
  ///
  ///  * [toJsonMap], which converts this node to a structured form intended for
  ///    data exchange (e.g. with an IDE).
1567
  Map<String, String>? toTimelineArguments() {
Ian Hickson's avatar
Ian Hickson committed
1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578
    if (!kReleaseMode) {
      // We don't throw in release builds, to avoid hurting users. We also don't do anything useful.
      if (kProfileMode) {
        throw FlutterError(
          // Parts of this string are searched for verbatim by a test in dev/bots/test.dart.
          '$DiagnosticsNode.toTimelineArguments used in non-debug build.\n'
          'The $DiagnosticsNode.toTimelineArguments API is expensive and causes timeline traces '
          'to be non-representative. As such, it should not be used in profile builds. However, '
          'this application is compiled in profile mode and yet still invoked the method.'
        );
      }
1579
      final Map<String, String> result = <String, String>{};
Ian Hickson's avatar
Ian Hickson committed
1580 1581 1582 1583 1584
      for (final DiagnosticsNode property in getProperties()) {
        if (property.name != null) {
          result[property.name!] = property.toDescription(parentConfiguration: singleLineTextConfiguration);
        }
      }
1585
      return result;
Ian Hickson's avatar
Ian Hickson committed
1586
    }
1587
    return null;
Ian Hickson's avatar
Ian Hickson committed
1588 1589
  }

1590 1591
  /// Serialize the node to a JSON map according to the configuration provided
  /// in the [DiagnosticsSerializationDelegate].
1592 1593 1594 1595 1596 1597 1598 1599 1600 1601
  ///
  /// 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
1602 1603
  Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
    Map<String, Object?> result = <String, Object?>{};
1604 1605
    assert(() {
      final bool hasChildren = getChildren().isNotEmpty;
1606
      result = <String, Object?>{
1607 1608 1609 1610 1611 1612 1613
        'description': toDescription(),
        'type': runtimeType.toString(),
        if (name != null)
          'name': name,
        if (!showSeparator)
          'showSeparator': showSeparator,
        if (level != DiagnosticLevel.info)
1614
          'level': level.name,
1615
        if (!showName)
1616 1617 1618 1619
          'showName': showName,
        if (emptyBodyDescription != null)
          'emptyBodyDescription': emptyBodyDescription,
        if (style != DiagnosticsTreeStyle.sparse)
1620
          'style': style!.name,
1621 1622 1623 1624
        if (allowTruncate)
          'allowTruncate': allowTruncate,
        if (hasChildren)
          'hasChildren': hasChildren,
1625
        if (linePrefix?.isNotEmpty ?? false)
1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647
          'linePrefix': linePrefix,
        if (!allowWrap)
          'allowWrap': allowWrap,
        if (allowNameWrap)
          'allowNameWrap': allowNameWrap,
        ...delegate.additionalNodeProperties(this),
        if (delegate.includeProperties)
          'properties': toJsonList(
            delegate.filterProperties(getProperties(), this),
            this,
            delegate,
          ),
        if (delegate.subtreeDepth > 0)
          'children': toJsonList(
            delegate.filterChildren(getChildren(), this),
            this,
            delegate,
          ),
      };
      return true;
    }());
    return result;
1648 1649
  }

1650 1651 1652 1653 1654
  /// Serializes a [List] of [DiagnosticsNode]s to a JSON list according to
  /// the configuration provided by the [DiagnosticsSerializationDelegate].
  ///
  /// The provided `nodes` may be properties or children of the `parent`
  /// [DiagnosticsNode].
1655 1656 1657
  static List<Map<String, Object?>> toJsonList(
    List<DiagnosticsNode>? nodes,
    DiagnosticsNode? parent,
1658
    DiagnosticsSerializationDelegate delegate,
1659 1660
  ) {
    bool truncated = false;
1661
    if (nodes == null) {
1662
      return const <Map<String, Object?>>[];
1663
    }
1664 1665 1666 1667 1668 1669
    final int originalNodeCount = nodes.length;
    nodes = delegate.truncateNodesList(nodes, parent);
    if (nodes.length != originalNodeCount) {
      nodes.add(DiagnosticsNode.message('...'));
      truncated = true;
    }
1670
    final List<Map<String, Object?>> json = nodes.map<Map<String, Object?>>((DiagnosticsNode node) {
1671 1672
      return node.toJsonMap(delegate.delegateForNode(node));
    }).toList();
1673
    if (truncated) {
1674
      json.last['truncated'] = true;
1675
    }
1676 1677 1678
    return json;
  }

1679 1680 1681 1682 1683 1684
  /// 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.
1685 1686 1687
  ///
  /// `minLevel` specifies the minimum [DiagnosticLevel] for properties included
  /// in the output.
1688 1689 1690
  ///
  /// In release mode, far less information is retained and some information may
  /// not print at all.
1691
  @override
1692
  String toString({
1693
    TextTreeConfiguration? parentConfiguration,
1694
    DiagnosticLevel minLevel = DiagnosticLevel.info,
1695
  }) {
1696
    String result = super.toString();
1697
    assert(style != null);
1698 1699
    assert(() {
      if (_isSingleLine(style)) {
1700
        result = toStringDeep(parentConfiguration: parentConfiguration, minLevel: minLevel);
1701
      } else {
Ian Hickson's avatar
Ian Hickson committed
1702
        final String description = toDescription(parentConfiguration: parentConfiguration);
1703

1704
        if (name == null || name!.isEmpty || !showName) {
1705 1706 1707 1708 1709 1710 1711 1712 1713
          result = description;
        } else {
          result = description.contains('\n') ? '$name$_separator\n$description'
              : '$name$_separator $description';
        }
      }
      return true;
    }());
    return result;
1714 1715
  }

1716 1717 1718
  /// Returns a configuration specifying how this object should be rendered
  /// as text art.
  @protected
1719
  TextTreeConfiguration? get textTreeConfiguration {
1720
    assert(style != null);
1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737
    return switch (style!) {
      DiagnosticsTreeStyle.none          => null,
      DiagnosticsTreeStyle.dense         => denseTextConfiguration,
      DiagnosticsTreeStyle.sparse        => sparseTextConfiguration,
      DiagnosticsTreeStyle.offstage      => dashedTextConfiguration,
      DiagnosticsTreeStyle.whitespace    => whitespaceTextConfiguration,
      DiagnosticsTreeStyle.transition    => transitionTextConfiguration,
      DiagnosticsTreeStyle.singleLine    => singleLineTextConfiguration,
      DiagnosticsTreeStyle.errorProperty => errorPropertyTextConfiguration,
      DiagnosticsTreeStyle.shallow       => shallowTextConfiguration,
      DiagnosticsTreeStyle.error         => errorTextConfiguration,
      DiagnosticsTreeStyle.flat          => flatTextConfiguration,

      // Truncate children doesn't really need its own text style as the
      // rendering is quite custom.
      DiagnosticsTreeStyle.truncateChildren => whitespaceTextConfiguration,
    };
1738 1739 1740 1741
  }

  /// Returns a string representation of this node and its descendants.
  ///
1742 1743 1744 1745 1746
  /// `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.
  ///
1747 1748 1749 1750 1751
  /// `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.
1752
  ///
1753 1754 1755
  /// In release mode, far less information is retained and some information may
  /// not print at all.
  ///
1756 1757
  /// See also:
  ///
1758
  ///  * [toString], for a brief description of the [value] but not its
1759
  ///    children.
1760
  String toStringDeep({
1761
    String prefixLineOne = '',
1762 1763
    String? prefixOtherLines,
    TextTreeConfiguration? parentConfiguration,
1764
    DiagnosticLevel minLevel = DiagnosticLevel.debug,
1765
  }) {
1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779
    String result = '';
    assert(() {
      result = TextTreeRenderer(
        minLevel: minLevel,
        wrapWidth: 65,
      ).render(
        this,
        prefixLineOne: prefixLineOne,
        prefixOtherLines: prefixOtherLines,
        parentConfiguration: parentConfiguration,
      );
      return true;
    }());
    return result;
1780 1781 1782 1783 1784
  }
}

/// Debugging message displayed like a property.
///
1785
/// {@tool snippet}
1786 1787 1788 1789 1790
///
/// 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:
1791 1792
///
/// ```dart
1793 1794
/// MessageProperty table = MessageProperty('table size', '$columns\u00D7$rows');
/// MessageProperty usefulness = MessageProperty('usefulness ratio', 'no metrics collected yet (never painted)');
1795
/// ```
1796
/// {@end-tool}
1797
/// {@tool snippet}
1798
///
1799 1800
/// On the other hand, [StringProperty] is better suited when the property has a
/// concrete value that is a string:
1801
///
1802
/// ```dart
1803
/// StringProperty name = StringProperty('name', _name);
1804
/// ```
1805
/// {@end-tool}
1806 1807 1808
///
/// See also:
///
1809
///  * [DiagnosticsNode.message], which serves the same role for messages
1810
///    without a clear property name.
1811
///  * [StringProperty], which is a better fit for properties with string values.
1812
class MessageProperty extends DiagnosticsProperty<void> {
1813 1814 1815
  /// Create a diagnostics property that displays a message.
  ///
  /// Messages have no concrete [value] (so [value] will return null). The
1816
  /// message is stored as the description.
1817 1818 1819
  MessageProperty(
    String name,
    String message, {
1820
    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
1821
    DiagnosticLevel level = DiagnosticLevel.info,
1822
  }) : super(name, null, description: message, style: style, level: level);
1823 1824 1825 1826 1827 1828
}

/// Property which encloses its string [value] in quotes.
///
/// See also:
///
1829
///  * [MessageProperty], which is a better fit for showing a message
1830 1831
///    instead of describing a property with a string value.
class StringProperty extends DiagnosticsProperty<String> {
1832
  /// Create a diagnostics property for strings.
1833
  StringProperty(
1834 1835 1836 1837 1838 1839
    String super.name,
    super.value, {
    super.description,
    super.tooltip,
    super.showName,
    super.defaultValue,
1840
    this.quoted = true,
1841 1842 1843
    super.ifEmpty,
    super.style,
    super.level,
1844
  });
1845

Ian Hickson's avatar
Ian Hickson committed
1846
  /// Whether the value is enclosed in double quotes.
1847 1848
  final bool quoted;

1849
  @override
1850 1851
  Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
    final Map<String, Object?> json = super.toJsonMap(delegate);
1852 1853 1854 1855
    json['quoted'] = quoted;
    return json;
  }

1856
  @override
1857 1858
  String valueToString({ TextTreeConfiguration? parentConfiguration }) {
    String? text = _description ?? value;
1859 1860 1861 1862 1863 1864
    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.
1865
      text = text.replaceAll('\n', r'\n');
1866 1867
    }

1868 1869 1870
    if (quoted && text != null) {
      // An empty value would not appear empty after being surrounded with
      // quotes so we have to handle this case separately.
1871
      if (ifEmpty != null && text.isEmpty) {
1872
        return ifEmpty!;
1873
      }
1874 1875 1876 1877 1878 1879 1880
      return '"$text"';
    }
    return text.toString();
  }
}

abstract class _NumProperty<T extends num> extends DiagnosticsProperty<T> {
1881
  _NumProperty(
1882 1883 1884
    String super.name,
    super.value, {
    super.ifNull,
1885
    this.unit,
1886 1887 1888 1889 1890 1891
    super.showName,
    super.defaultValue,
    super.tooltip,
    super.style,
    super.level,
  });
1892

1893
  _NumProperty.lazy(
1894 1895 1896
    String super.name,
    super.computeValue, {
    super.ifNull,
1897
    this.unit,
1898 1899 1900 1901 1902 1903
    super.showName,
    super.defaultValue,
    super.tooltip,
    super.style,
    super.level,
  }) : super.lazy();
1904

1905
  @override
1906 1907
  Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
    final Map<String, Object?> json = super.toJsonMap(delegate);
1908
    if (unit != null) {
1909
      json['unit'] = unit;
1910
    }
1911 1912 1913 1914

    json['numberToString'] = numberToString();
    return json;
  }
1915 1916 1917 1918 1919 1920

  /// 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].
1921
  final String? unit;
1922 1923 1924 1925 1926

  /// String describing just the numeric [value] without a unit suffix.
  String numberToString();

  @override
1927
  String valueToString({ TextTreeConfiguration? parentConfiguration }) {
1928
    if (value == null) {
1929
      return value.toString();
1930
    }
1931

1932
    return unit != null ? '${numberToString()}$unit' : numberToString();
1933 1934
  }
}
1935
/// Property describing a [double] [value] with an optional [unit] of measurement.
1936 1937 1938
///
/// Numeric formatting is optimized for debug message readability.
class DoubleProperty extends _NumProperty<double> {
1939
  /// If specified, [unit] describes the unit for the [value] (e.g. px).
1940
  DoubleProperty(
1941 1942 1943 1944 1945 1946 1947 1948 1949
    super.name,
    super.value, {
    super.ifNull,
    super.unit,
    super.tooltip,
    super.defaultValue,
    super.showName,
    super.style,
    super.level,
1950
  });
1951 1952 1953 1954 1955 1956

  /// Property with a [value] that is computed only when needed.
  ///
  /// Use if computing the property [value] may throw an exception or is
  /// expensive.
  DoubleProperty.lazy(
1957 1958 1959 1960 1961 1962 1963 1964
    super.name,
    super.computeValue, {
    super.ifNull,
    super.showName,
    super.unit,
    super.tooltip,
    super.defaultValue,
    super.level,
1965
  }) : super.lazy();
1966 1967

  @override
1968
  String numberToString() => debugFormatDouble(value);
1969 1970 1971 1972 1973 1974
}

/// 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> {
1975
  /// Create a diagnostics property for integers.
1976
  IntProperty(
1977 1978 1979 1980 1981 1982 1983 1984
    super.name,
    super.value, {
    super.ifNull,
    super.showName,
    super.unit,
    super.defaultValue,
    super.style,
    super.level,
1985
  });
1986 1987

  @override
1988
  String numberToString() => value.toString();
1989 1990 1991 1992 1993
}

/// Property which clamps a [double] to between 0 and 1 and formats it as a
/// percentage.
class PercentProperty extends DoubleProperty {
1994 1995 1996 1997 1998 1999
  /// 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.
2000
  PercentProperty(
2001 2002 2003 2004 2005 2006 2007
    super.name,
    super.fraction, {
    super.ifNull,
    super.showName,
    super.tooltip,
    super.unit,
    super.level,
2008
  });
2009 2010

  @override
2011
  String valueToString({ TextTreeConfiguration? parentConfiguration }) {
2012
    if (value == null) {
2013
      return value.toString();
2014
    }
2015
    return unit != null ? '${numberToString()} $unit' : numberToString();
2016 2017 2018 2019
  }

  @override
  String numberToString() {
2020
    final double? v = value;
2021
    if (v == null) {
2022
      return value.toString();
2023
    }
2024
    return '${(clampDouble(v, 0.0, 1.0) * 100.0).toStringAsFixed(1)}%';
2025 2026 2027 2028
  }
}

/// Property where the description is either [ifTrue] or [ifFalse] depending on
2029
/// whether [value] is true or false.
2030
///
2031 2032
/// Using [FlagProperty] instead of [DiagnosticsProperty<bool>] can make
/// diagnostics display more polished. For example, given a property named
2033
/// `visible` that is typically true, the following code will return 'hidden'
2034 2035
/// when `visible` is false and nothing when visible is true, in contrast to
/// `visible: true` or `visible: false`.
2036
///
2037
/// {@tool snippet}
2038 2039
///
/// ```dart
2040
/// FlagProperty(
2041 2042 2043 2044 2045
///   'visible',
///   value: true,
///   ifFalse: 'hidden',
/// )
/// ```
2046
/// {@end-tool}
2047
/// {@tool snippet}
2048
///
2049
/// [FlagProperty] should also be used instead of [DiagnosticsProperty<bool>]
2050 2051 2052 2053
/// if showing the bool value would not clearly indicate the meaning of the
/// property value.
///
/// ```dart
2054
/// FlagProperty(
2055 2056 2057 2058 2059 2060
///   'inherit',
///   value: inherit,
///   ifTrue: '<all styles inherited>',
///   ifFalse: '<no style specified>',
/// )
/// ```
2061
/// {@end-tool}
2062 2063 2064 2065
///
/// See also:
///
///  * [ObjectFlagProperty], which provides similar behavior describing whether
2066
///    a [value] is null.
2067
class FlagProperty extends DiagnosticsProperty<bool> {
2068
  /// Constructs a FlagProperty with the given descriptions with the specified descriptions.
2069 2070 2071
  ///
  /// [showName] defaults to false as typically [ifTrue] and [ifFalse] should
  /// be descriptions that make the property name redundant.
2072 2073
  FlagProperty(
    String name, {
2074
    required bool? value,
2075 2076
    this.ifTrue,
    this.ifFalse,
2077
    bool showName = false,
2078
    Object? defaultValue,
2079
    DiagnosticLevel level = DiagnosticLevel.info,
2080
  }) : assert(ifTrue != null || ifFalse != null),
2081
       super(
2082 2083 2084 2085 2086 2087
         name,
         value,
         showName: showName,
         defaultValue: defaultValue,
         level: level,
       );
2088

2089
  @override
2090 2091
  Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
    final Map<String, Object?> json = super.toJsonMap(delegate);
2092
    if (ifTrue != null) {
2093
      json['ifTrue'] = ifTrue;
2094 2095
    }
    if (ifFalse != null) {
2096
      json['ifFalse'] = ifFalse;
2097
    }
2098 2099 2100 2101

    return json;
  }

2102
  /// Description to use if the property [value] is true.
2103
  ///
2104 2105
  /// If not specified and [value] equals true the property's priority [level]
  /// will be [DiagnosticLevel.hidden].
2106
  final String? ifTrue;
2107

2108
  /// Description to use if the property value is false.
2109
  ///
2110 2111
  /// If not specified and [value] equals false, the property's priority [level]
  /// will be [DiagnosticLevel.hidden].
2112
  final String? ifFalse;
2113 2114

  @override
2115
  String valueToString({ TextTreeConfiguration? parentConfiguration }) {
2116
    if (value ?? false) {
2117
      if (ifTrue != null) {
2118
        return ifTrue!;
2119
      }
2120
    } else if (value == false) {
2121
      if (ifFalse != null) {
2122
        return ifFalse!;
2123
      }
2124 2125
    }
    return super.valueToString(parentConfiguration: parentConfiguration);
2126 2127 2128
  }

  @override
2129
  bool get showName {
2130
    if (value == null || ((value ?? false) && ifTrue == null) || (!(value ?? true) && ifFalse == null)) {
2131 2132
      // 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
2133
      // so users will not see this property in this case unless they are
2134
      // displaying hidden properties.
2135
      return true;
2136 2137 2138 2139 2140 2141
    }
    return super.showName;
  }

  @override
  DiagnosticLevel get level {
2142
    if (value ?? false) {
2143
      if (ifTrue == null) {
2144
        return DiagnosticLevel.hidden;
2145
      }
2146 2147
    }
    if (value == false) {
2148
      if (ifFalse == null) {
2149
        return DiagnosticLevel.hidden;
2150
      }
2151 2152
    }
    return super.level;
2153 2154 2155 2156 2157 2158
  }
}

/// Property with an `Iterable<T>` [value] that can be displayed with
/// different [DiagnosticsTreeStyle] for custom rendering.
///
2159
/// If [style] is [DiagnosticsTreeStyle.singleLine], the iterable is described
2160 2161 2162
/// as a comma separated list, otherwise the iterable is described as a line
/// break separated list.
class IterableProperty<T> extends DiagnosticsProperty<Iterable<T>> {
2163 2164
  /// Create a diagnostics property for iterables (e.g. lists).
  ///
2165
  /// The [ifEmpty] argument is used to indicate how an iterable [value] with 0
2166
  /// elements is displayed. If [ifEmpty] equals null that indicates that an
2167 2168 2169
  /// 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.
2170
  IterableProperty(
2171 2172 2173 2174 2175 2176 2177 2178 2179
    String super.name,
    super.value, {
    super.defaultValue,
    super.ifNull,
    super.ifEmpty = '[]',
    super.style,
    super.showName,
    super.showSeparator,
    super.level,
2180
  });
2181 2182

  @override
2183
  String valueToString({TextTreeConfiguration? parentConfiguration}) {
2184
    if (value == null) {
2185
      return value.toString();
2186
    }
2187

2188
    if (value!.isEmpty) {
2189
      return ifEmpty ?? '[]';
2190
    }
2191

2192
    final Iterable<String> formattedValues = value!.map((T v) {
2193 2194 2195 2196 2197 2198 2199
      if (T == double && v is double) {
        return debugFormatDouble(v);
      } else {
        return v.toString();
      }
    });

2200 2201 2202
    if (parentConfiguration != null && !parentConfiguration.lineBreakProperties) {
      // Always display the value as a single line and enclose the iterable
      // value in brackets to avoid ambiguity.
2203
      return '[${formattedValues.join(', ')}]';
2204 2205
    }

2206
    return formattedValues.join(_isSingleLine(style) ? ', ' : '\n');
2207
  }
2208 2209 2210 2211

  /// Priority level of the diagnostic used to control which diagnostics should
  /// be shown and filtered.
  ///
2212
  /// If [ifEmpty] is null and the [value] is an empty [Iterable] then level
2213
  /// [DiagnosticLevel.fine] is returned in a similar way to how an
2214 2215
  /// [ObjectFlagProperty] handles when [ifNull] is null and the [value] is
  /// null.
2216 2217
  @override
  DiagnosticLevel get level {
2218
    if (ifEmpty == null && value != null && value!.isEmpty && super.level != DiagnosticLevel.hidden) {
2219
      return DiagnosticLevel.fine;
2220
    }
2221 2222
    return super.level;
  }
2223 2224

  @override
2225 2226
  Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
    final Map<String, Object?> json = super.toJsonMap(delegate);
2227
    if (value != null) {
2228
      json['values'] = value!.map<String>((T value) => value.toString()).toList();
2229 2230 2231
    }
    return json;
  }
2232 2233
}

2234
/// [DiagnosticsProperty] that has an [Enum] as value.
2235
///
2236
/// The enum value is displayed with the enum name stripped. For example:
2237
/// [HitTestBehavior.deferToChild] is shown as `deferToChild`.
2238
///
2239
/// This class can be used with enums and returns the enum's name getter. It
2240 2241 2242
/// can also be used with nullable properties; the null value is represented as
/// `null`.
///
2243 2244 2245
/// See also:
///
///  * [DiagnosticsProperty] which documents named parameters common to all
2246
///    [DiagnosticsProperty].
2247
class EnumProperty<T extends Enum?> extends DiagnosticsProperty<T> {
2248 2249
  /// Create a diagnostics property that displays an enum.
  ///
2250
  /// The [level] argument must also not be null.
2251
  EnumProperty(
2252 2253 2254 2255
    String super.name,
    super.value, {
    super.defaultValue,
    super.level,
2256
  });
2257 2258

  @override
2259
  String valueToString({ TextTreeConfiguration? parentConfiguration }) {
2260
    return value?.name ?? 'null';
2261 2262 2263
  }
}

2264 2265 2266 2267 2268 2269
/// 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
2270
/// omitted, that is taken to mean that [level] should be
2271
/// [DiagnosticLevel.hidden] when [value] is non-null or null respectively.
2272
///
2273
/// This kind of diagnostics property is typically used for opaque
2274 2275 2276
/// 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.
2277 2278 2279
///
/// See also:
///
2280 2281 2282 2283
///
///  * [FlagsSummary], which provides similar functionality but accepts multiple
///    flags under the same name, and is preferred if there are multiple such
///    values that can fit into a same category (such as "listeners").
2284
///  * [FlagProperty], which provides similar functionality describing whether
2285
///    a [value] is true or false.
2286
class ObjectFlagProperty<T> extends DiagnosticsProperty<T> {
2287 2288 2289 2290
  /// 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).
  ///
2291
  /// At least one of [ifPresent] or [ifNull] must be non-null.
2292
  ObjectFlagProperty(
2293 2294
    String super.name,
    super.value, {
2295
    this.ifPresent,
2296 2297 2298
    super.ifNull,
    super.showName = false,
    super.level,
2299
  }) : assert(ifPresent != null || ifNull != null);
2300 2301 2302 2303 2304 2305

  /// 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.
  ObjectFlagProperty.has(
2306 2307 2308
    String super.name,
    super.value, {
    super.level,
2309
  }) : ifPresent = 'has $name',
2310 2311 2312
       super(
    showName: false,
  );
2313

2314
  /// Description to use if the property [value] is not null.
2315
  ///
2316
  /// If the property [value] is not null and [ifPresent] is null, the
2317
  /// [level] for the property is [DiagnosticLevel.hidden] and the description
2318
  /// from superclass is used.
2319
  final String? ifPresent;
2320 2321

  @override
2322
  String valueToString({ TextTreeConfiguration? parentConfiguration }) {
2323
    if (value != null) {
2324
      if (ifPresent != null) {
2325
        return ifPresent!;
2326
      }
2327
    } else {
2328
      if (ifNull != null) {
2329
        return ifNull!;
2330
      }
2331 2332
    }
    return super.valueToString(parentConfiguration: parentConfiguration);
2333 2334 2335
  }

  @override
2336 2337 2338 2339
  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
2340
      // so users will not see this property in this case unless they are
2341
      // displaying hidden properties.
2342
      return true;
2343 2344 2345 2346 2347 2348 2349
    }
    return super.showName;
  }

  @override
  DiagnosticLevel get level {
    if (value != null) {
2350
      if (ifPresent == null) {
2351
        return DiagnosticLevel.hidden;
2352
      }
2353
    } else {
2354
      if (ifNull == null) {
2355
        return DiagnosticLevel.hidden;
2356
      }
2357 2358 2359
    }

    return super.level;
2360
  }
2361 2362

  @override
2363 2364
  Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
    final Map<String, Object?> json = super.toJsonMap(delegate);
2365
    if (ifPresent != null) {
2366
      json['ifPresent'] = ifPresent;
2367
    }
2368 2369
    return json;
  }
2370 2371
}

2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388
/// A summary of multiple properties, indicating whether each of them is present
/// (non-null) or absent (null).
///
/// Each entry of [value] is described by its key. The eventual description will
/// be a list of keys of non-null entries.
///
/// The [ifEmpty] describes the entire collection of [value] when it contains no
/// non-null entries. If [ifEmpty] is omitted, [level] will be
/// [DiagnosticLevel.hidden] when [value] contains no non-null entries.
///
/// This kind of diagnostics property is typically used for 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:
///
2389
///  * [ObjectFlagProperty], which provides similar functionality but accepts
2390 2391 2392
///    only one flag, and is preferred if there is only one entry.
///  * [IterableProperty], which provides similar functionality describing
///    the values a collection of objects.
2393
class FlagsSummary<T> extends DiagnosticsProperty<Map<String, T?>> {
2394 2395 2396 2397 2398 2399
  /// Create a summary for multiple properties, indicating whether each of them
  /// is present (non-null) or absent (null).
  ///
  /// The [value], [showName], [showSeparator] and [level] arguments must not be
  /// null.
  FlagsSummary(
2400 2401 2402 2403 2404 2405
    String super.name,
    Map<String, T?> super.value, {
    super.ifEmpty,
    super.showName,
    super.showSeparator,
    super.level,
2406
  });
2407 2408

  @override
2409
  Map<String, T?> get value => super.value!;
2410 2411 2412

  @override
  String valueToString({TextTreeConfiguration? parentConfiguration}) {
2413
    if (!_hasNonNullEntry() && ifEmpty != null) {
2414
      return ifEmpty!;
2415
    }
2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433

    final Iterable<String> formattedValues = _formattedValues();
    if (parentConfiguration != null && !parentConfiguration.lineBreakProperties) {
      // Always display the value as a single line and enclose the iterable
      // value in brackets to avoid ambiguity.
      return '[${formattedValues.join(', ')}]';
    }

    return formattedValues.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] contains no non-null entries, then
  /// level [DiagnosticLevel.hidden] is returned.
  @override
  DiagnosticLevel get level {
2434
    if (!_hasNonNullEntry() && ifEmpty == null) {
2435
      return DiagnosticLevel.hidden;
2436
    }
2437 2438 2439 2440
    return super.level;
  }

  @override
2441 2442
  Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
    final Map<String, Object?> json = super.toJsonMap(delegate);
2443
    if (value.isNotEmpty) {
2444
      json['values'] = _formattedValues().toList();
2445
    }
2446 2447 2448
    return json;
  }

2449
  bool _hasNonNullEntry() => value.values.any((T? o) => o != null);
2450 2451 2452 2453 2454

  // An iterable of each entry's description in [value].
  //
  // For a non-null value, its description is its key.
  //
2455
  // For a null value, it is omitted unless `includeEmpty` is true and
2456
  // [ifEntryNull] contains a corresponding description.
2457 2458 2459 2460
  Iterable<String> _formattedValues() {
    return value.entries
        .where((MapEntry<String, T?> entry) => entry.value != null)
        .map((MapEntry<String, T?> entry) => entry.key);
2461 2462 2463
  }
}

2464 2465 2466 2467 2468
/// 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.
2469
typedef ComputePropertyValueCallback<T> = T? Function();
2470 2471 2472

/// Property with a [value] of type [T].
///
2473
/// If the default `value.toString()` does not provide an adequate description
2474
/// of the value, specify `description` defining a custom description.
2475 2476 2477
///
/// The [showSeparator] property indicates whether a separator should be placed
/// between the property [name] and its [value].
2478
class DiagnosticsProperty<T> extends DiagnosticsNode {
2479 2480
  /// Create a diagnostics property.
  ///
2481 2482
  /// 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
2483 2484
  /// level. For example, if the property value is null and [missingIfNull] is
  /// true, [level] is raised to [DiagnosticLevel.warning].
2485
  DiagnosticsProperty(
2486 2487 2488 2489
    String? name,
    T? value, {
    String? description,
    String? ifNull,
2490
    this.ifEmpty,
2491 2492
    super.showName,
    super.showSeparator,
2493
    this.defaultValue = kNoDefaultValue,
2494
    this.tooltip,
2495
    this.missingIfNull = false,
2496
    super.linePrefix,
2497 2498 2499
    this.expandableValue = false,
    this.allowWrap = true,
    this.allowNameWrap = true,
2500
    DiagnosticsTreeStyle super.style = DiagnosticsTreeStyle.singleLine,
2501
    DiagnosticLevel level = DiagnosticLevel.info,
2502
  }) : _description = description,
2503 2504 2505
       _valueComputed = true,
       _value = value,
       _computeValue = null,
2506 2507
       ifNull = ifNull ?? (missingIfNull ? 'MISSING' : null),
       _defaultLevel = level,
2508 2509 2510 2511 2512 2513 2514 2515
       super(
         name: name,
      );

  /// Property with a [value] that is computed only when needed.
  ///
  /// Use if computing the property [value] may throw an exception or is
  /// expensive.
2516
  ///
2517
  /// The [level] argument is just a suggestion and can be overridden
2518 2519 2520
  /// 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].
2521
  DiagnosticsProperty.lazy(
2522
    String? name,
2523
    ComputePropertyValueCallback<T> computeValue, {
2524 2525
    String? description,
    String? ifNull,
2526
    this.ifEmpty,
2527 2528
    super.showName,
    super.showSeparator,
2529
    this.defaultValue = kNoDefaultValue,
2530
    this.tooltip,
2531
    this.missingIfNull = false,
2532 2533 2534
    this.expandableValue = false,
    this.allowWrap = true,
    this.allowNameWrap = true,
2535
    DiagnosticsTreeStyle super.style = DiagnosticsTreeStyle.singleLine,
2536
    DiagnosticLevel level = DiagnosticLevel.info,
2537
  }) : assert(defaultValue == kNoDefaultValue || defaultValue is T?),
2538
       _description = description,
2539 2540 2541
       _valueComputed = false,
       _value = null,
       _computeValue = computeValue,
2542
       _defaultLevel = level,
2543
       ifNull = ifNull ?? (missingIfNull ? 'MISSING' : null),
2544 2545 2546 2547
       super(
         name: name,
       );

2548
  final String? _description;
2549

2550 2551 2552 2553 2554 2555 2556 2557 2558 2559
  /// Whether to expose properties and children of the value as properties and
  /// children.
  final bool expandableValue;

  @override
  final bool allowWrap;

  @override
  final bool allowNameWrap;

2560
  @override
2561 2562 2563
  Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
    final T? v = value;
    List<Map<String, Object?>>? properties;
2564
    if (delegate.expandPropertyValues && delegate.includeProperties && v is Diagnosticable && getProperties().isEmpty) {
2565 2566 2567 2568 2569 2570 2571 2572
      // Exclude children for expanded nodes to avoid cycles.
      delegate = delegate.copyWith(subtreeDepth: 0, includeProperties: false);
      properties = DiagnosticsNode.toJsonList(
        delegate.filterProperties(v.toDiagnosticsNode().getProperties(), this),
        this,
        delegate,
      );
    }
2573
    final Map<String, Object?> json = super.toJsonMap(delegate);
2574 2575 2576
    if (properties != null) {
      json['properties'] = properties;
    }
2577
    if (defaultValue != kNoDefaultValue) {
2578
      json['defaultValue'] = defaultValue.toString();
2579 2580
    }
    if (ifEmpty != null) {
2581
      json['ifEmpty'] = ifEmpty;
2582 2583
    }
    if (ifNull != null) {
2584
      json['ifNull'] = ifNull;
2585 2586
    }
    if (tooltip != null) {
2587
      json['tooltip'] = tooltip;
2588
    }
2589
    json['missingIfNull'] = missingIfNull;
2590
    if (exception != null) {
2591
      json['exception'] = exception.toString();
2592
    }
2593
    json['propertyType'] = propertyType.toString();
2594
    json['defaultLevel'] = _defaultLevel.name;
2595
    if (value is Diagnosticable || value is DiagnosticsNode) {
2596
      json['isDiagnosticableValue'] = true;
2597 2598 2599 2600
    }
    if (v is num) {
      // TODO(jacob314): Workaround, since JSON.stringify replaces infinity and NaN with null,
      // https://github.com/flutter/flutter/issues/39937#issuecomment-529558033)
2601
      json['value'] = v.isFinite ? v :  v.toString();
2602 2603
    }
    if (value is String || value is bool || value == null) {
2604
      json['value'] = value;
2605
    }
2606 2607 2608
    return json;
  }

2609 2610
  /// Returns a string representation of the property value.
  ///
2611
  /// Subclasses should override this method instead of [toDescription] to
2612 2613 2614
  /// customize how property values are converted to strings.
  ///
  /// Overriding this method ensures that behavior controlling how property
2615 2616 2617
  /// 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].
2618 2619 2620 2621
  ///
  /// `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.
2622 2623
  String valueToString({ TextTreeConfiguration? parentConfiguration }) {
    final T? v = value;
2624 2625 2626
    // 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.
2627
    return v is DiagnosticableTree ? v.toStringShort() : v.toString();
2628
  }
2629 2630

  @override
2631
  String toDescription({ TextTreeConfiguration? parentConfiguration }) {
2632
    if (_description != null) {
2633
      return _addTooltip(_description);
2634
    }
2635

2636
    if (exception != null) {
2637
      return 'EXCEPTION (${exception.runtimeType})';
2638
    }
2639

2640
    if (ifNull != null && value == null) {
2641
      return _addTooltip(ifNull!);
2642
    }
2643

2644
    String result = valueToString(parentConfiguration: parentConfiguration);
2645
    if (result.isEmpty && ifEmpty != null) {
2646
      result = ifEmpty!;
2647
    }
2648
    return _addTooltip(result);
2649 2650
  }

2651 2652 2653 2654
  /// 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.
  String _addTooltip(String text) {
2655 2656 2657 2658
    return tooltip == null ? text : '$text ($tooltip)';
  }

  /// Description if the property [value] is null.
2659
  final String? ifNull;
2660 2661

  /// Description if the property description would otherwise be empty.
2662
  final String? ifEmpty;
2663 2664 2665 2666 2667 2668 2669

  /// 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.
2670
  final String? tooltip;
2671

2672 2673 2674 2675
  /// Whether a [value] of null causes the property to have [level]
  /// [DiagnosticLevel.warning] warning that the property is missing a [value].
  final bool missingIfNull;

2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686
  /// 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".
2687 2688 2689 2690 2691 2692
  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],
2693
  /// [value] returns null and the exception thrown can be found via the
2694 2695 2696 2697 2698
  /// [exception] property.
  ///
  /// See also:
  ///
  ///  * [valueToString], which converts the property value to a string.
2699
  @override
2700
  T? get value {
2701 2702 2703 2704
    _maybeCacheValue();
    return _value;
  }

2705
  T? _value;
2706 2707 2708

  bool _valueComputed;

2709
  Object? _exception;
2710 2711 2712 2713

  /// Exception thrown if accessing the property [value] threw an exception.
  ///
  /// Returns null if computing the property value did not throw an exception.
2714
  Object? get exception {
2715 2716 2717 2718 2719
    _maybeCacheValue();
    return _exception;
  }

  void _maybeCacheValue() {
2720
    if (_valueComputed) {
2721
      return;
2722
    }
2723 2724 2725 2726

    _valueComputed = true;
    assert(_computeValue != null);
    try {
2727
      _value = _computeValue!();
2728
    } catch (exception) {
2729 2730
      // The error is reported to inspector; rethrowing would destroy the
      // debugging experience.
2731 2732 2733 2734 2735
      _exception = exception;
      _value = null;
    }
  }

2736 2737 2738 2739 2740 2741 2742
  /// The default value of this property, when it has not been set to a specific
  /// value.
  ///
  /// For most [DiagnosticsProperty] classes, if the [value] of the property
  /// equals [defaultValue], then the priority [level] of the property is
  /// downgraded to [DiagnosticLevel.fine] on the basis that the property value
  /// is uninteresting. This is implemented by [isInteresting].
2743
  ///
2744 2745
  /// The [defaultValue] is [kNoDefaultValue] by default. Otherwise it must be of
  /// type `T?`.
2746
  final Object? defaultValue;
2747

2748 2749 2750 2751 2752
  /// Whether to consider the property's value interesting. When a property is
  /// uninteresting, its [level] is downgraded to [DiagnosticLevel.fine]
  /// regardless of the value provided as the constructor's `level` argument.
  bool get isInteresting => defaultValue == kNoDefaultValue || value != defaultValue;

2753
  final DiagnosticLevel _defaultLevel;
2754

2755 2756
  /// Priority level of the diagnostic used to control which diagnostics should
  /// be shown and filtered.
2757
  ///
2758
  /// The property level defaults to the value specified by the [level]
2759 2760 2761 2762 2763 2764
  /// 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].
2765
  @override
2766
  DiagnosticLevel get level {
2767
    if (_defaultLevel == DiagnosticLevel.hidden) {
2768
      return _defaultLevel;
2769
    }
2770

2771
    if (exception != null) {
2772
      return DiagnosticLevel.error;
2773
    }
2774

2775
    if (value == null && missingIfNull) {
2776
      return DiagnosticLevel.warning;
2777
    }
2778

2779
    if (!isInteresting) {
2780
      return DiagnosticLevel.fine;
2781
    }
2782 2783

    return _defaultLevel;
2784 2785
  }

2786
  final ComputePropertyValueCallback<T>? _computeValue;
2787 2788

  @override
2789 2790
  List<DiagnosticsNode> getProperties() {
    if (expandableValue) {
2791
      final T? object = value;
2792 2793 2794
      if (object is DiagnosticsNode) {
        return object.getProperties();
      }
2795
      if (object is Diagnosticable) {
2796 2797 2798 2799 2800
        return object.toDiagnosticsNode(style: style).getProperties();
      }
    }
    return const <DiagnosticsNode>[];
  }
2801 2802

  @override
2803 2804
  List<DiagnosticsNode> getChildren() {
    if (expandableValue) {
2805
      final T? object = value;
2806 2807 2808
      if (object is DiagnosticsNode) {
        return object.getChildren();
      }
2809
      if (object is Diagnosticable) {
2810 2811 2812 2813 2814
        return object.toDiagnosticsNode(style: style).getChildren();
      }
    }
    return const <DiagnosticsNode>[];
  }
2815 2816
}

2817 2818
/// [DiagnosticsNode] that lazily calls the associated [Diagnosticable] [value]
/// to implement [getChildren] and [getProperties].
2819
class DiagnosticableNode<T extends Diagnosticable> extends DiagnosticsNode {
2820
  /// Create a diagnostics describing a [Diagnosticable] value.
2821
  DiagnosticableNode({
2822
    super.name,
2823
    required this.value,
2824
    required super.style,
2825
  });
2826 2827

  @override
2828
  final T value;
2829

2830
  DiagnosticPropertiesBuilder? _cachedBuilder;
2831

2832 2833 2834
  /// Retrieve the [DiagnosticPropertiesBuilder] of current node.
  ///
  /// It will cache the result to prevent duplicate operation.
2835
  DiagnosticPropertiesBuilder? get builder {
2836
    if (kReleaseMode) {
2837
      return null;
2838 2839 2840 2841
    } else {
      assert(() {
        if (_cachedBuilder == null) {
          _cachedBuilder = DiagnosticPropertiesBuilder();
2842
          value.debugFillProperties(_cachedBuilder!);
2843 2844 2845 2846
        }
        return true;
      }());
      return _cachedBuilder;
2847 2848 2849
    }
  }

2850 2851
  @override
  DiagnosticsTreeStyle get style {
2852
    return kReleaseMode ? DiagnosticsTreeStyle.none : super.style ?? builder!.defaultDiagnosticsTreeStyle;
2853
  }
2854 2855

  @override
2856
  String? get emptyBodyDescription => (kReleaseMode || kProfileMode) ? '' : builder!.emptyBodyDescription;
2857 2858

  @override
2859
  List<DiagnosticsNode> getProperties() => (kReleaseMode || kProfileMode) ? const <DiagnosticsNode>[] : builder!.properties;
2860 2861

  @override
2862 2863
  List<DiagnosticsNode> getChildren() {
    return const<DiagnosticsNode>[];
2864 2865 2866
  }

  @override
2867
  String toDescription({ TextTreeConfiguration? parentConfiguration }) {
2868 2869 2870 2871 2872 2873
    String result = '';
    assert(() {
      result = value.toStringShort();
      return true;
    }());
    return result;
2874 2875 2876
  }
}

2877
/// [DiagnosticsNode] for an instance of [DiagnosticableTree].
2878 2879 2880
class DiagnosticableTreeNode extends DiagnosticableNode<DiagnosticableTree> {
  /// Creates a [DiagnosticableTreeNode].
  DiagnosticableTreeNode({
2881 2882 2883 2884
    super.name,
    required super.value,
    required super.style,
  });
2885 2886

  @override
2887
  List<DiagnosticsNode> getChildren() => value.debugDescribeChildren();
2888 2889
}

2890
/// Returns a 5 character long hexadecimal string generated from
2891
/// [Object.hashCode]'s 20 least-significant bits.
2892
String shortHash(Object? object) {
2893 2894 2895 2896
  return object.hashCode.toUnsigned(20).toRadixString(16).padLeft(5, '0');
}

/// Returns a summary of the runtime type and hash code of `object`.
2897 2898 2899 2900 2901 2902 2903 2904
///
/// 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.
2905
String describeIdentity(Object? object) => '${objectRuntimeType(object, '<optimized out>')}#${shortHash(object)}';
2906

2907 2908 2909 2910
/// Returns a short description of an enum value.
///
/// Strips off the enum class name from the `enumEntry.toString()`.
///
2911 2912 2913 2914 2915 2916 2917 2918 2919
/// For real enums, this is redundant with calling the `name` getter on the enum
/// value (see [EnumName.name]), a feature that was added to Dart 2.15.
///
/// This function can also be used with classes whose `toString` return a value
/// in the same form as an enum (the class name, a dot, then the value name).
/// For example, it's used with [SemanticsAction], which is written to appear to
/// be an enum but is actually a bespoke class so that the index values can be
/// set as powers of two instead of as sequential integers.
///
2920
/// {@tool snippet}
2921 2922 2923 2924 2925 2926
///
/// ```dart
/// enum Day {
///   monday, tuesday, wednesday, thursday, friday, saturday, sunday
/// }
///
2927
/// void validateDescribeEnum() {
2928 2929
///   assert(Day.monday.toString() == 'Day.monday');
///   assert(describeEnum(Day.monday) == 'monday');
2930
///   assert(Day.monday.name == 'monday'); // preferred for real enums
2931 2932
/// }
/// ```
2933
/// {@end-tool}
2934 2935
@Deprecated(
  'Use the `name` getter on enums instead. '
2936
  'This feature was deprecated after v3.14.0-2.0.pre.'
2937
)
2938
String describeEnum(Object enumEntry) {
2939
  if (enumEntry is Enum) {
2940
    return enumEntry.name;
2941
  }
2942 2943
  final String description = enumEntry.toString();
  final int indexOfDot = description.indexOf('.');
2944 2945 2946 2947
  assert(
    indexOfDot != -1 && indexOfDot < description.length - 1,
    'The provided object "$enumEntry" is not an enum.',
  );
2948 2949 2950
  return description.substring(indexOfDot + 1);
}

2951
/// Builder to accumulate properties and configuration used to assemble a
2952
/// [DiagnosticsNode] from a [Diagnosticable] object.
2953
class DiagnosticPropertiesBuilder {
2954 2955 2956 2957 2958 2959 2960
  /// Creates a [DiagnosticPropertiesBuilder] with [properties] initialize to
  /// an empty array.
  DiagnosticPropertiesBuilder() : properties = <DiagnosticsNode>[];

  /// Creates a [DiagnosticPropertiesBuilder] with a given [properties].
  DiagnosticPropertiesBuilder.fromProperties(this.properties);

2961 2962
  /// Add a property to the list of properties.
  void add(DiagnosticsNode property) {
2963
    assert(() {
2964
      properties.add(property);
2965 2966
      return true;
    }());
2967 2968 2969
  }

  /// List of properties accumulated so far.
2970
  final List<DiagnosticsNode> properties;
2971 2972 2973 2974 2975

  /// 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.
2976
  String? emptyBodyDescription;
2977 2978
}

2979
// Examples can assume:
2980
// class ExampleSuperclass with Diagnosticable { late String message; late double stepWidth; late double scale; late double paintExtent; late double hitTestExtent; late double paintExtend; late double maxWidth; late bool primary; late double progress; late int maxLines; late Duration duration; late int depth; Iterable<BoxShadow>? boxShadow; late DiagnosticsTreeStyle style; late bool hasSize; late Matrix4 transform; Map<Listenable, VoidCallback>? handles; late Color color; late bool obscureText; late ImageRepeat repeat; late Size size; late Widget widget; late bool isCurrent; late bool keepAlive; late TextAlign textAlign; }
2981

2982 2983
/// A mixin class for providing string and [DiagnosticsNode] debug
/// representations describing the properties of an object.
2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005
///
/// 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.
3006
mixin Diagnosticable {
3007 3008 3009 3010 3011
  /// A brief description of this object, usually just the [runtimeType] and the
  /// [hashCode].
  ///
  /// See also:
  ///
3012 3013
  ///  * [toString], for a detailed description of the object.
  String toStringShort() => describeIdentity(this);
3014

3015
  @override
3016
  String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
3017
    String? fullString;
3018 3019 3020 3021 3022
    assert(() {
      fullString = toDiagnosticsNode(style: DiagnosticsTreeStyle.singleLine).toString(minLevel: minLevel);
      return true;
    }());
    return fullString ?? toStringShort();
3023 3024
  }

3025
  /// Returns a debug representation of the object that is used by debugging
3026
  /// tools and by [DiagnosticsNode.toStringDeep].
3027
  ///
3028
  /// Leave [name] as null if there is not a meaningful description of the
3029
  /// relationship between the this node and its parent.
3030
  ///
3031 3032 3033
  /// 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.
3034
  DiagnosticsNode toDiagnosticsNode({ String? name, DiagnosticsTreeStyle? style }) {
3035
    return DiagnosticableNode<Diagnosticable>(
3036
      name: name,
3037
      value: this,
3038 3039
      style: style,
    );
3040 3041
  }

3042 3043
  /// Add additional properties associated with the node.
  ///
3044 3045
  /// {@youtube 560 315 https://www.youtube.com/watch?v=DnC7eT-vh1k}
  ///
3046 3047
  /// Use the most specific [DiagnosticsProperty] existing subclass to describe
  /// each property instead of the [DiagnosticsProperty] base class. There are
3048
  /// only a small number of [DiagnosticsProperty] subclasses each covering a
3049 3050
  /// common use case. Consider what values a property is relevant for users
  /// debugging as users debugging large trees are overloaded with information.
3051
  /// Common named parameters in [DiagnosticsNode] subclasses help filter when
3052 3053
  /// and how properties are displayed.
  ///
3054
  /// `defaultValue`, `showName`, `showSeparator`, and `level` keep string
3055 3056 3057 3058
  /// representations of diagnostics terse and hide properties when they are not
  /// very useful.
  ///
  ///  * Use `defaultValue` any time the default value of a property is
3059 3060
  ///    uninteresting. For example, specify a default value of null any time
  ///    a property being null does not indicate an error.
3061
  ///  * Avoid specifying the `level` parameter unless the result you want
3062
  ///    cannot be achieved by using the `defaultValue` parameter or using
3063 3064 3065 3066 3067
  ///    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
3068
  ///    DiagnosticsProperty<Object>('child(3, 4)', null, ifNull: 'is null', showSeparator: false).toString()
3069 3070 3071 3072
  ///    ```
  ///    Shows using `showSeparator` to get output `child(3, 4) is null` which
  ///    is more polished than `child(3, 4): is null`.
  ///    ```dart
3073
  ///    DiagnosticsProperty<IconData>('icon', icon, ifNull: '<empty>', showName: false).toString()
3074 3075 3076 3077 3078 3079 3080 3081
  ///    ```
  ///    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.
  ///
3082
  /// ## DiagnosticsProperty subclasses for primitive types
3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093
  ///
  ///  * [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
3094
  ///    `DiagnosticsProperty<bool>` instead of using [FlagProperty] as the
3095 3096
  ///    output is more verbose but unambiguous.
  ///
3097
  /// ## Other important [DiagnosticsProperty] variants
3098 3099 3100 3101 3102 3103 3104 3105 3106
  ///
  ///  * [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.
3107 3108 3109 3110
  ///  * [ColorProperty], which must be used if the property value is
  ///    a [Color] or one of its subclasses.
  ///  * [IconDataProperty], which must be used if the property value
  ///    is of type [IconData].
3111
  ///
3112
  /// If none of these subclasses apply, use the [DiagnosticsProperty]
3113 3114 3115
  /// 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
3116
  /// `toString` method implementation works fine using [DiagnosticsProperty]
3117 3118
  /// directly.
  ///
3119
  /// {@tool snippet}
3120 3121
  ///
  /// This example shows best practices for implementing [debugFillProperties]
3122
  /// illustrating use of all common [DiagnosticsProperty] subclasses and all
3123 3124 3125
  /// common [DiagnosticsProperty] parameters.
  ///
  /// ```dart
3126
  /// class ExampleObject extends ExampleSuperclass {
3127 3128 3129
  ///
  ///   // ...various members and properties...
  ///
3130
  ///   @override
3131
  ///   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
3132
  ///     // Always add properties from the base class first.
3133
  ///     super.debugFillProperties(properties);
3134 3135 3136
  ///
  ///     // Omit the property name 'message' when displaying this String property
  ///     // as it would just add visual noise.
3137
  ///     properties.add(StringProperty('message', message, showName: false));
3138
  ///
3139
  ///     properties.add(DoubleProperty('stepWidth', stepWidth));
3140 3141
  ///
  ///     // A scale of 1.0 does nothing so should be hidden.
3142
  ///     properties.add(DoubleProperty('scale', scale, defaultValue: 1.0));
3143 3144 3145
  ///
  ///     // If the hitTestExtent matches the paintExtent, it is just set to its
  ///     // default value so is not relevant.
3146
  ///     properties.add(DoubleProperty('hitTestExtent', hitTestExtent, defaultValue: paintExtent));
3147
  ///
3148
  ///     // maxWidth of double.infinity indicates the width is unconstrained and
3149
  ///     // so maxWidth has no impact.
3150
  ///     properties.add(DoubleProperty('maxWidth', maxWidth, defaultValue: double.infinity));
3151 3152 3153 3154
  ///
  ///     // 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.
3155
  ///     properties.add(PercentProperty(
3156 3157 3158 3159 3160 3161 3162
  ///       'progress',
  ///       progress,
  ///       showName: false,
  ///       ifNull: '<indeterminate>',
  ///     ));
  ///
  ///     // Most text fields have maxLines set to 1.
3163
  ///     properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
3164 3165 3166
  ///
  ///     // Specify the unit as otherwise it would be unclear that time is in
  ///     // milliseconds.
3167
  ///     properties.add(IntProperty('duration', duration.inMilliseconds, unit: 'ms'));
3168 3169 3170 3171
  ///
  ///     // 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.
3172
  ///     properties.add(DoubleProperty(
3173
  ///       'device pixel ratio',
3174
  ///       devicePixelRatio,
3175 3176 3177 3178 3179
  ///       tooltip: 'physical pixels per logical pixel',
  ///     ));
  ///
  ///     // Displaying the depth value would be distracting. Instead only display
  ///     // if the depth value is missing.
3180
  ///     properties.add(ObjectFlagProperty<int>('depth', depth, ifNull: 'no depth'));
3181 3182
  ///
  ///     // bool flag that is only shown when the value is true.
3183
  ///     properties.add(FlagProperty('using primary controller', value: primary));
3184
  ///
3185
  ///     properties.add(FlagProperty(
3186 3187 3188 3189 3190 3191
  ///       'isCurrent',
  ///       value: isCurrent,
  ///       ifTrue: 'active',
  ///       ifFalse: 'inactive',
  ///     ));
  ///
3192
  ///     properties.add(DiagnosticsProperty<bool>('keepAlive', keepAlive));
3193 3194 3195 3196
  ///
  ///     // 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.
3197
  ///     properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
3198
  ///
3199 3200
  ///     properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
  ///     properties.add(EnumProperty<ImageRepeat>('repeat', repeat, defaultValue: ImageRepeat.noRepeat));
3201 3202
  ///
  ///     // Warn users when the widget is missing but do not show the value.
3203
  ///     properties.add(ObjectFlagProperty<Widget>('widget', widget, ifNull: 'no widget'));
3204
  ///
3205
  ///     properties.add(IterableProperty<BoxShadow>(
3206 3207 3208 3209 3210 3211 3212
  ///       'boxShadow',
  ///       boxShadow,
  ///       defaultValue: null,
  ///       style: style,
  ///     ));
  ///
  ///     // Getting the value of size throws an exception unless hasSize is true.
3213
  ///     properties.add(DiagnosticsProperty<Size>.lazy(
3214 3215 3216 3217 3218 3219 3220 3221 3222
  ///       '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.
3223
  ///     properties.add(TransformProperty('transform', transform));
3224 3225 3226 3227 3228 3229 3230
  ///
  ///     // 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.
3231
  ///     properties.add(DiagnosticsProperty<Color>('color', color));
3232 3233 3234
  ///
  ///     // Use a custom description to generate a more terse summary than the
  ///     // `toString` method on the map class.
3235
  ///     properties.add(DiagnosticsProperty<Map<Listenable, VoidCallback>>(
3236 3237
  ///       'handles',
  ///       handles,
3238 3239 3240
  ///       description: handles != null
  ///         ? '${handles!.length} active client${ handles!.length == 1 ? "" : "s" }'
  ///         : null,
3241 3242 3243 3244 3245 3246
  ///       ifNull: 'no notifications ever received',
  ///       showName: false,
  ///     ));
  ///   }
  /// }
  /// ```
3247
  /// {@end-tool}
3248
  ///
3249
  /// Used by [toDiagnosticsNode] and [toString].
3250
  ///
3251
  /// Do not add values that have lifetime shorter than the object.
3252 3253
  @protected
  @mustCallSuper
3254 3255 3256 3257 3258 3259 3260 3261 3262 3263 3264 3265 3266
  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:
///
3267
///  * [DiagnosticableTreeMixin], a mixin that implements this class.
3268
///  * [Diagnosticable], which should be used instead of this class to
3269
///    provide diagnostics for objects without children.
3270
abstract class DiagnosticableTree with Diagnosticable {
3271 3272 3273 3274 3275 3276 3277 3278 3279
  /// 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.
  ///
3280 3281
  /// `joiner` specifies the string which is place between each part obtained
  /// from [debugFillProperties]. Passing a string such as `'\n '` will result
3282
  /// in a multiline string that indents the properties of the object below its
3283 3284 3285 3286
  /// name (as per [toString]).
  ///
  /// `minLevel` specifies the minimum [DiagnosticLevel] for properties included
  /// in the output.
3287 3288 3289 3290 3291
  ///
  /// See also:
  ///
  ///  * [toString], for a brief description of the object.
  ///  * [toStringDeep], for a description of the subtree rooted at this object.
3292
  String toStringShallow({
3293 3294
    String joiner = ', ',
    DiagnosticLevel minLevel = DiagnosticLevel.debug,
3295
  }) {
3296
    String? shallowString;
3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309
    assert(() {
      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),
      );
      shallowString = result.toString();
      return true;
    }());
3310
    return shallowString ?? toString();
3311 3312 3313 3314
  }

  /// Returns a string representation of this node and its descendants.
  ///
3315 3316 3317 3318 3319
  /// `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.
  ///
3320 3321 3322 3323 3324
  /// `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.
3325 3326 3327 3328 3329 3330
  ///
  /// 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.
3331
  String toStringDeep({
3332
    String prefixLineOne = '',
3333
    String? prefixOtherLines,
3334
    DiagnosticLevel minLevel = DiagnosticLevel.debug,
3335 3336
  }) {
    return toDiagnosticsNode().toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines, minLevel: minLevel);
3337 3338 3339 3340 3341 3342
  }

  @override
  String toStringShort() => describeIdentity(this);

  @override
3343
  DiagnosticsNode toDiagnosticsNode({ String? name, DiagnosticsTreeStyle? style }) {
3344
    return DiagnosticableTreeNode(
3345 3346 3347 3348 3349
      name: name,
      value: this,
      style: style,
    );
  }
3350

3351 3352 3353
  /// Returns a list of [DiagnosticsNode] objects describing this node's
  /// children.
  ///
3354
  /// Children that are offstage should be added with `style` set to
3355 3356
  /// [DiagnosticsTreeStyle.offstage] to indicate that they are offstage.
  ///
3357
  /// The list must not contain any null entries. If there are explicit null
3358
  /// children to report, consider [DiagnosticsNode.message] or
3359 3360 3361
  /// [DiagnosticsProperty<Object>] as possible [DiagnosticsNode] objects to
  /// provide.
  ///
3362 3363
  /// Used by [toStringDeep], [toDiagnosticsNode] and [toStringShallow].
  ///
3364 3365 3366
  /// See also:
  ///
  ///  * [RenderTable.debugDescribeChildren], which provides high quality custom
3367
  ///    descriptions for its child nodes.
3368
  @protected
3369 3370 3371
  List<DiagnosticsNode> debugDescribeChildren() => const <DiagnosticsNode>[];
}

3372
/// A mixin that helps dump string and [DiagnosticsNode] representations of trees.
3373
///
3374 3375
/// This mixin is identical to class [DiagnosticableTree].
mixin DiagnosticableTreeMixin implements DiagnosticableTree {
3376
  @override
3377
  String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
3378
    return toDiagnosticsNode(style: DiagnosticsTreeStyle.singleLine).toString(minLevel: minLevel);
3379
  }
3380 3381

  @override
3382
  String toStringShallow({
3383 3384
    String joiner = ', ',
    DiagnosticLevel minLevel = DiagnosticLevel.debug,
3385
  }) {
3386
    String? shallowString;
3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399
    assert(() {
      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),
      );
      shallowString = result.toString();
      return true;
    }());
3400
    return shallowString ?? toString();
3401 3402 3403
  }

  @override
3404
  String toStringDeep({
3405
    String prefixLineOne = '',
3406
    String? prefixOtherLines,
3407
    DiagnosticLevel minLevel = DiagnosticLevel.debug,
3408 3409
  }) {
    return toDiagnosticsNode().toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines, minLevel: minLevel);
3410 3411 3412 3413 3414 3415
  }

  @override
  String toStringShort() => describeIdentity(this);

  @override
3416
  DiagnosticsNode toDiagnosticsNode({ String? name, DiagnosticsTreeStyle? style }) {
3417
    return DiagnosticableTreeNode(
3418 3419 3420 3421 3422 3423 3424 3425 3426 3427 3428
      name: name,
      value: this,
      style: style,
    );
  }

  @override
  List<DiagnosticsNode> debugDescribeChildren() => const <DiagnosticsNode>[];

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) { }
3429
}
3430 3431 3432 3433 3434 3435 3436 3437 3438 3439


/// [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({
3440 3441
    super.name,
    DiagnosticsTreeStyle super.style = DiagnosticsTreeStyle.whitespace,
3442
    bool showName = true,
3443 3444
    super.showSeparator,
    super.linePrefix,
3445
    this.value,
3446
    String? description,
3447 3448 3449 3450
    this.level = DiagnosticLevel.info,
    this.allowTruncate = false,
    List<DiagnosticsNode> children = const<DiagnosticsNode>[],
    List<DiagnosticsNode> properties = const <DiagnosticsNode>[],
Ian Hickson's avatar
Ian Hickson committed
3451
  }) : _description = description ?? '',
3452 3453 3454 3455 3456 3457 3458 3459 3460 3461 3462
       _children = children,
       _properties = properties,
    super(
    showName: showName && name != null,
  );

  final List<DiagnosticsNode> _children;
  final List<DiagnosticsNode> _properties;

  @override
  final DiagnosticLevel level;
3463

Ian Hickson's avatar
Ian Hickson committed
3464
  final String _description;
3465

3466
  @override
3467
  final Object? value;
3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478

  @override
  final bool allowTruncate;

  @override
  List<DiagnosticsNode> getChildren() => _children;

  @override
  List<DiagnosticsNode> getProperties() => _properties;

  @override
Ian Hickson's avatar
Ian Hickson committed
3479
  String toDescription({TextTreeConfiguration? parentConfiguration}) => _description;
3480
}
3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503

/// A delegate that configures how a hierarchy of [DiagnosticsNode]s should be
/// serialized.
///
/// Implement this class in a subclass to fully configure how [DiagnosticsNode]s
/// get serialized.
abstract class DiagnosticsSerializationDelegate {
  /// Creates a simple [DiagnosticsSerializationDelegate] that controls the
  /// [subtreeDepth] and whether to [includeProperties].
  ///
  /// For additional configuration options, extend
  /// [DiagnosticsSerializationDelegate] and provide custom implementations
  /// for the methods of this class.
  const factory DiagnosticsSerializationDelegate({
    int subtreeDepth,
    bool includeProperties,
  }) = _DefaultDiagnosticsSerializationDelegate;

  /// Returns a serializable map of additional information that will be included
  /// in the serialization of the given [DiagnosticsNode].
  ///
  /// This method is called for every [DiagnosticsNode] that's included in
  /// the serialization.
3504
  Map<String, Object?> additionalNodeProperties(DiagnosticsNode node);
3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541

  /// Filters the list of [DiagnosticsNode]s that will be included as children
  /// for the given `owner` node.
  ///
  /// The callback may return a subset of the children in the provided list
  /// or replace the entire list with new child nodes.
  ///
  /// See also:
  ///
  ///  * [subtreeDepth], which controls how many levels of children will be
  ///    included in the serialization.
  List<DiagnosticsNode> filterChildren(List<DiagnosticsNode> nodes, DiagnosticsNode owner);

  /// Filters the list of [DiagnosticsNode]s that will be included as properties
  /// for the given `owner` node.
  ///
  /// The callback may return a subset of the properties in the provided list
  /// or replace the entire list with new property nodes.
  ///
  /// By default, `nodes` is returned as-is.
  ///
  /// See also:
  ///
  ///  * [includeProperties], which controls whether properties will be included
  ///    at all.
  List<DiagnosticsNode> filterProperties(List<DiagnosticsNode> nodes, DiagnosticsNode owner);

  /// Truncates the given list of [DiagnosticsNode] that will be added to the
  /// serialization as children or properties of the `owner` node.
  ///
  /// The method must return a subset of the provided nodes and may
  /// not replace any nodes. While [filterProperties] and [filterChildren]
  /// completely hide a node from the serialization, truncating a node will
  /// leave a hint in the serialization that there were additional nodes in the
  /// result that are not included in the current serialization.
  ///
  /// By default, `nodes` is returned as-is.
3542
  List<DiagnosticsNode> truncateNodesList(List<DiagnosticsNode> nodes, DiagnosticsNode? owner);
3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554 3555 3556 3557 3558 3559 3560 3561 3562 3563 3564 3565 3566 3567 3568 3569 3570 3571 3572 3573 3574 3575 3576

  /// Returns the [DiagnosticsSerializationDelegate] to be used
  /// for adding the provided [DiagnosticsNode] to the serialization.
  ///
  /// By default, this will return a copy of this delegate, which has the
  /// [subtreeDepth] reduced by one.
  ///
  /// This is called for nodes that will be added to the serialization as
  /// property or child of another node. It may return the same delegate if no
  /// changes to it are necessary.
  DiagnosticsSerializationDelegate delegateForNode(DiagnosticsNode node);

  /// Controls how many levels of children will be included in the serialized
  /// hierarchy of [DiagnosticsNode]s.
  ///
  /// Defaults to zero.
  ///
  /// See also:
  ///
  ///  * [filterChildren], which provides a way to filter the children that
  ///    will be included.
  int get subtreeDepth;

  /// Whether to include the properties of a [DiagnosticsNode] in the
  /// serialization.
  ///
  /// Defaults to false.
  ///
  /// See also:
  ///
  ///  * [filterProperties], which provides a way to filter the properties that
  ///    will be included.
  bool get includeProperties;

3577
  /// Whether properties that have a [Diagnosticable] as value should be
3578 3579 3580 3581 3582 3583 3584 3585 3586 3587 3588 3589 3590 3591 3592 3593 3594 3595
  /// expanded.
  bool get expandPropertyValues;

  /// Creates a copy of this [DiagnosticsSerializationDelegate] with the
  /// provided values.
  DiagnosticsSerializationDelegate copyWith({
    int subtreeDepth,
    bool includeProperties,
  });
}

class _DefaultDiagnosticsSerializationDelegate implements DiagnosticsSerializationDelegate {
  const _DefaultDiagnosticsSerializationDelegate({
    this.includeProperties = false,
    this.subtreeDepth = 0,
  });

  @override
3596 3597
  Map<String, Object?> additionalNodeProperties(DiagnosticsNode node) {
    return const <String, Object?>{};
3598 3599 3600 3601 3602 3603 3604 3605 3606 3607 3608 3609 3610 3611 3612 3613 3614 3615 3616 3617 3618 3619 3620 3621 3622 3623 3624
  }

  @override
  DiagnosticsSerializationDelegate delegateForNode(DiagnosticsNode node) {
    return subtreeDepth > 0 ? copyWith(subtreeDepth: subtreeDepth - 1) : this;
  }

  @override
  bool get expandPropertyValues => false;

  @override
  List<DiagnosticsNode> filterChildren(List<DiagnosticsNode> nodes, DiagnosticsNode owner) {
    return nodes;
  }

  @override
  List<DiagnosticsNode> filterProperties(List<DiagnosticsNode> nodes, DiagnosticsNode owner) {
    return nodes;
  }

  @override
  final bool includeProperties;

  @override
  final int subtreeDepth;

  @override
3625
  List<DiagnosticsNode> truncateNodesList(List<DiagnosticsNode> nodes, DiagnosticsNode? owner) {
3626 3627 3628 3629
    return nodes;
  }

  @override
3630
  DiagnosticsSerializationDelegate copyWith({int? subtreeDepth, bool? includeProperties}) {
3631 3632 3633 3634 3635 3636
    return _DefaultDiagnosticsSerializationDelegate(
      subtreeDepth: subtreeDepth ?? this.subtreeDepth,
      includeProperties: includeProperties ?? this.includeProperties,
    );
  }
}