diagnostics.dart 127 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 6
import 'dart:math' as math;

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

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

14 15 16 17 18
// Examples can assume:
// int rows, columns;
// String _name;
// bool inherit;

19 20 21 22 23 24
/// 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.
25 26 27
///
/// In release mode, this level may not have any effect, as diagnostics in
/// release mode are compacted or truncated to reduce binary size.
28 29 30 31 32
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
33
  /// they should expect them to sometimes be misleading. For example,
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
  /// [FlagProperty] and [ObjectFlagProperty] have uglier formatting when the
  /// property `value` does does not match a value with a custom flag
  /// description. An example of a misleading diagnostic is a diagnostic for
  /// a property that has no effect because some other property of the object is
  /// set in a way that causes the hidden property to have no effect.
  hidden,

  /// A diagnostic that is likely to be low value but where the diagnostic
  /// display is just as high quality as a diagnostic with a higher level.
  ///
  /// Use this level for diagnostic properties that match their default value
  /// and other cases where showing a diagnostic would not add much value such
  /// as an [IterableProperty] where the value is empty.
  fine,

  /// Diagnostics that should only be shown when performing fine grained
  /// debugging of an object.
  ///
  /// Unlike a [fine] diagnostic, these diagnostics provide important
  /// information about the object that is likely to be needed to debug. Used by
  /// properties that are important but where the property value is too verbose
  /// (e.g. 300+ characters long) to show with a higher diagnostic level.
  debug,

  /// Interesting diagnostics that should be typically shown.
  info,

  /// Very important diagnostics that indicate problematic property values.
  ///
  /// For example, use if you would write the property description
  /// message in ALL CAPS.
  warning,

67 68 69 70 71 72 73 74 75 76 77
  /// 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,

78 79 80 81 82 83 84 85 86 87 88 89
  /// 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,
}
90

91 92
/// Styles for displaying a node in a [DiagnosticsNode] tree.
///
93 94 95
/// In release mode, these styles may be ignored, as diagnostics are compacted
/// or truncated to save on binary size.
///
96 97 98 99 100
/// See also:
///
///  * [DiagnosticsNode.toStringDeep], which dumps text art trees for these
///    styles.
enum DiagnosticsTreeStyle {
101 102 103
  /// A style that does not display the tree, for release mode.
  none,

104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
  /// 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,

136 137 138 139 140 141 142 143
  /// 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,

144 145 146 147 148 149 150 151
  /// Render the tree just using whitespace without connecting parents to
  /// children using lines.
  ///
  /// See also:
  ///
  ///  * [SliverGeometry], which uses this style.
  whitespace,

152 153 154 155 156 157 158
  /// Render the tree without indenting children at all.
  ///
  /// See also:
  ///
  ///  * [DiagnosticsStackTrace], which uses this style.
  flat,

159 160
  /// Render the tree on a single line without showing children.
  singleLine,
161 162 163 164 165 166 167 168

  /// 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:
169
  ///
170 171 172 173 174 175 176 177
  ///  * [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
178 179
  ///  * [DebugOverflowIndicatorMixin], which uses this style to display just
  ///    the immediate children of a node.
180 181 182 183 184
  shallow,

  /// Render only the children of a node truncating before the tree becomes too
  /// large.
  truncateChildren,
185 186 187 188 189
}

/// Configuration specifying how a particular [DiagnosticsTreeStyle] should be
/// rendered as text art.
///
190 191 192
/// In release mode, these configurations may be ignored, as diagnostics are
/// compacted or truncated to save on binary size.
///
193 194 195
/// See also:
///
///  * [sparseTextConfiguration], which is a typical style.
196
///  * [transitionTextConfiguration], which is an example of a complex tree style.
197 198 199
///  * [DiagnosticsNode.toStringDeep], for code using [TextTreeConfiguration]
///    to render text art for arbitrary trees of [DiagnosticsNode] objects.
class TextTreeConfiguration {
200 201 202
  /// Create a configuration object describing how to render a tree as text.
  ///
  /// All of the arguments must not be null.
203
  TextTreeConfiguration({
204 205 206 207 208 209 210
    required this.prefixLineOne,
    required this.prefixOtherLines,
    required this.prefixLastChildLineOne,
    required this.prefixOtherLinesRootNode,
    required this.linkCharacter,
    required this.propertyPrefixIfChildren,
    required this.propertyPrefixNoChildren,
211 212 213 214
    this.lineBreak = '\n',
    this.lineBreakProperties = true,
    this.afterName = ':',
    this.afterDescriptionIfBody = '',
215
    this.afterDescription = '',
216 217
    this.beforeProperties = '',
    this.afterProperties = '',
218
    this.mandatoryAfterProperties = '',
219 220 221 222 223 224 225
    this.propertySeparator = '',
    this.bodyIndent = '',
    this.footer = '',
    this.showChildren = true,
    this.addBlankLineIfNoChildren = true,
    this.isNameOnOwnLine = false,
    this.isBlankLineBetweenPropertiesAndChildren = true,
226 227
    this.beforeName = '',
    this.suffixLineOne = '',
228
    this.mandatoryFooter = '',
229 230 231 232 233 234 235 236
  }) : assert(prefixLineOne != null),
       assert(prefixOtherLines != null),
       assert(prefixLastChildLineOne != null),
       assert(prefixOtherLinesRootNode != null),
       assert(linkCharacter != null),
       assert(propertyPrefixIfChildren != null),
       assert(propertyPrefixNoChildren != null),
       assert(lineBreak != null),
237
       assert(lineBreakProperties != null),
238 239
       assert(afterName != null),
       assert(afterDescriptionIfBody != null),
240
       assert(afterDescription != null),
241 242 243 244 245 246 247 248 249 250
       assert(beforeProperties != null),
       assert(afterProperties != null),
       assert(propertySeparator != null),
       assert(bodyIndent != null),
       assert(footer != null),
       assert(showChildren != null),
       assert(addBlankLineIfNoChildren != null),
       assert(isNameOnOwnLine != null),
       assert(isBlankLineBetweenPropertiesAndChildren != null),
       childLinkSpace = ' ' * linkCharacter.length;
251 252 253 254

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

255 256 257
  /// Suffix to add to end of the first line to make its length match the footer.
  final String suffixLineOne;

258 259 260
  /// Prefix to add to other lines to display a child with this style.
  ///
  /// [prefixOtherLines] should typically be one character shorter than
261
  /// [prefixLineOne] is.
262 263 264 265 266 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
  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;

302 303 304 305
  /// Whether to place line breaks between properties or to leave all
  /// properties on one line.
  final bool lineBreakProperties;

306 307 308 309 310 311 312

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

313 314
  /// Text added immediately after the name of the node.
  ///
315 316
  /// See [transitionTextConfiguration] for an example of using a value other
  /// than ':' to achieve a custom line art style.
317 318 319
  final String afterName;

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

323 324 325 326
  /// Text to add immediately after the description line of a node with
  /// properties and/or children.
  final String afterDescription;

327 328 329 330 331 332 333 334 335 336 337 338
  /// 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;

339 340 341 342
  /// Mandatory string to add after the properties of a node regardless of
  /// whether the node has any properties.
  final String mandatoryAfterProperties;

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

375
  /// Footer to add even for root nodes.
376
  final String mandatoryFooter;
377

378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411
  /// Add a blank line between properties and children if both are present.
  final bool isBlankLineBetweenPropertiesAndChildren;
}

/// Default text tree configuration.
///
/// Example:
/// ```
/// <root_name>: <root_description>
///  │ <property1>
///  │ <property2>
///  │ ...
///  │ <propertyN>
///  ├─<child_name>: <child_description>
///  │ │ <property1>
///  │ │ <property2>
///  │ │ ...
///  │ │ <propertyN>
///  │ │
///  │ └─<child_name>: <child_description>
///  │     <property1>
///  │     <property2>
///  │     ...
///  │     <propertyN>
///  │
///  └─<child_name>: <child_description>'
///    <property1>
///    <property2>
///    ...
///    <propertyN>
/// ```
///
/// See also:
///
412
///  * [DiagnosticsTreeStyle.sparse], uses this style for ASCII art display.
413
final TextTreeConfiguration sparseTextConfiguration = TextTreeConfiguration(
414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 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
  prefixLineOne:            '├─',
  prefixOtherLines:         ' ',
  prefixLastChildLineOne:   '└─',
  linkCharacter:            '│',
  propertyPrefixIfChildren: '│ ',
  propertyPrefixNoChildren: '  ',
  prefixOtherLinesRootNode: ' ',
);

/// Identical to [sparseTextConfiguration] except that the lines connecting
/// parent to children are dashed.
///
/// Example:
/// ```
/// <root_name>: <root_description>
///  │ <property1>
///  │ <property2>
///  │ ...
///  │ <propertyN>
///  ├─<normal_child_name>: <child_description>
///  ╎ │ <property1>
///  ╎ │ <property2>
///  ╎ │ ...
///  ╎ │ <propertyN>
///  ╎ │
///  ╎ └─<child_name>: <child_description>
///  ╎     <property1>
///  ╎     <property2>
///  ╎     ...
///  ╎     <propertyN>
///  ╎
///  ╎╌<dashed_child_name>: <child_description>
///  ╎ │ <property1>
///  ╎ │ <property2>
///  ╎ │ ...
///  ╎ │ <propertyN>
///  ╎ │
///  ╎ └─<child_name>: <child_description>
///  ╎     <property1>
///  ╎     <property2>
///  ╎     ...
///  ╎     <propertyN>
///  ╎
///  └╌<dashed_child_name>: <child_description>'
///    <property1>
///    <property2>
///    ...
///    <propertyN>
/// ```
///
/// See also:
///
466
///  * [DiagnosticsTreeStyle.offstage], uses this style for ASCII art display.
467
final TextTreeConfiguration dashedTextConfiguration = TextTreeConfiguration(
468 469 470 471 472 473 474 475 476 477 478 479 480 481 482
  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:
/// ```
483 484 485
/// <root_name>: <root_description>(<property1>; <property2> <propertyN>)
/// ├<child_name>: <child_description>(<property1>, <property2>, <propertyN>)
/// └<child_name>: <child_description>(<property1>, <property2>, <propertyN>)
486 487 488 489
/// ```
///
/// See also:
///
490
///  * [DiagnosticsTreeStyle.dense], uses this style for ASCII art display.
491
final TextTreeConfiguration denseTextConfiguration = TextTreeConfiguration(
492 493 494 495
  propertySeparator: ', ',
  beforeProperties: '(',
  afterProperties: ')',
  lineBreakProperties: false,
496 497 498 499 500 501 502 503
  prefixLineOne:            '├',
  prefixOtherLines:         '',
  prefixLastChildLineOne:   '└',
  linkCharacter:            '│',
  propertyPrefixIfChildren: '│',
  propertyPrefixNoChildren: ' ',
  prefixOtherLinesRootNode: '',
  addBlankLineIfNoChildren: false,
504
  isBlankLineBetweenPropertiesAndChildren: false,
505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526
);

/// Configuration that draws a box around a leaf node.
///
/// Used by leaf nodes such as [TextSpan] to draw a clear border around the
/// contents of a node.
///
/// Example:
/// ```
///  <parent_node>
///  ╞═╦══ <name> ═══
///  │ ║  <description>:
///  │ ║    <body>
///  │ ║    ...
///  │ ╚═══════════
///  ╘═╦══ <name> ═══
///    ║  <description>:
///    ║    <body>
///    ║    ...
///    ╚═══════════
/// ```
///
527
/// See also:
528
///
529
///  * [DiagnosticsTreeStyle.transition], uses this style for ASCII art display.
530
final TextTreeConfiguration transitionTextConfiguration = TextTreeConfiguration(
531 532 533
  prefixLineOne:           '╞═╦══ ',
  prefixLastChildLineOne:  '╘═╦══ ',
  prefixOtherLines:         ' ║ ',
534
  footer:                   ' ╚═══════════',
535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555
  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,
);

556 557 558 559 560 561 562 563 564 565 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 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612
/// Configuration that draws a box around a node ignoring the connection to the
/// parents.
///
/// If nested in a tree, this node is best displayed in the property box rather
/// than as a traditional child.
///
/// Used to draw a decorative box around detailed descriptions of an exception.
///
/// Example:
/// ```
/// ══╡ <name>: <description> ╞═════════════════════════════════════
/// <body>
/// ...
/// ├─<normal_child_name>: <child_description>
/// ╎ │ <property1>
/// ╎ │ <property2>
/// ╎ │ ...
/// ╎ │ <propertyN>
/// ╎ │
/// ╎ └─<child_name>: <child_description>
/// ╎     <property1>
/// ╎     <property2>
/// ╎     ...
/// ╎     <propertyN>
/// ╎
/// ╎╌<dashed_child_name>: <child_description>
/// ╎ │ <property1>
/// ╎ │ <property2>
/// ╎ │ ...
/// ╎ │ <propertyN>
/// ╎ │
/// ╎ └─<child_name>: <child_description>
/// ╎     <property1>
/// ╎     <property2>
/// ╎     ...
/// ╎     <propertyN>
/// ╎
/// └╌<dashed_child_name>: <child_description>'
/// ════════════════════════════════════════════════════════════════
/// ```
///
/// See also:
///
///  * [DiagnosticsTreeStyle.error], uses this style for ASCII art display.
final TextTreeConfiguration errorTextConfiguration = TextTreeConfiguration(
  prefixLineOne:           '╞═╦',
  prefixLastChildLineOne:  '╘═╦',
  prefixOtherLines:         ' ║ ',
  footer:                   ' ╚═══════════',
  linkCharacter:            '│',
  // Subtree boundaries are clear due to the border around the node so omit the
  // property prefix.
  propertyPrefixIfChildren: '',
  propertyPrefixNoChildren: '',
  prefixOtherLinesRootNode: '',
  beforeName:               '══╡ ',
  suffixLineOne:            ' ╞══',
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 628 629 630 631 632 633 634 635
/// draws line art would be visually distracting for those cases.
///
/// Example:
/// ```
/// <parent_node>
///   <name>: <description>:
///     <properties>
///     <children>
///   <name>: <description>:
///     <properties>
///     <children>
636
/// ```
637 638 639
///
/// See also:
///
640
///  * [DiagnosticsTreeStyle.whitespace], uses this style for ASCII art display.
641
final TextTreeConfiguration whitespaceTextConfiguration = TextTreeConfiguration(
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656
  prefixLineOne: '',
  prefixLastChildLineOne: '',
  prefixOtherLines: ' ',
  prefixOtherLinesRootNode: '  ',
  bodyIndent: '',
  propertyPrefixIfChildren: '',
  propertyPrefixNoChildren: '',
  linkCharacter: ' ',
  addBlankLineIfNoChildren: false,
  // Add a colon after the description and before the properties to link the
  // properties to the description line.
  afterDescriptionIfBody: ':',
  isBlankLineBetweenPropertiesAndChildren: false,
);

657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690
/// Whitespace only configuration where children are not indented.
///
/// Use this style when indentation is not needed to disambiguate parents from
/// children as in the case of a [DiagnosticsStackTrace].
///
/// Example:
/// ```
/// <parent_node>
/// <name>: <description>:
/// <properties>
/// <children>
/// <name>: <description>:
/// <properties>
/// <children>
/// ```
///
/// See also:
///
///  * [DiagnosticsTreeStyle.flat], uses this style for ASCII art display.
final TextTreeConfiguration flatTextConfiguration = TextTreeConfiguration(
  prefixLineOne: '',
  prefixLastChildLineOne: '',
  prefixOtherLines: '',
  prefixOtherLinesRootNode: '',
  bodyIndent: '',
  propertyPrefixIfChildren: '',
  propertyPrefixNoChildren: '',
  linkCharacter: '',
  addBlankLineIfNoChildren: false,
  // Add a colon after the description and before the properties to link the
  // properties to the description line.
  afterDescriptionIfBody: ':',
  isBlankLineBetweenPropertiesAndChildren: false,
);
691 692 693 694 695 696 697
/// Render a node as a single line omitting children.
///
/// Example:
/// `<name>: <description>(<property1>, <property2>, ..., <propertyN>)`
///
/// See also:
///
698
///  * [DiagnosticsTreeStyle.singleLine], uses this style for ASCII art display.
699
final TextTreeConfiguration singleLineTextConfiguration = TextTreeConfiguration(
700 701 702 703 704 705 706
  propertySeparator: ', ',
  beforeProperties: '(',
  afterProperties: ')',
  prefixLineOne: '',
  prefixOtherLines: '',
  prefixLastChildLineOne: '',
  lineBreak: '',
707
  lineBreakProperties: false,
708 709
  addBlankLineIfNoChildren: false,
  showChildren: false,
710 711
  propertyPrefixIfChildren: '  ',
  propertyPrefixNoChildren: '  ',
712 713 714 715
  linkCharacter: '',
  prefixOtherLinesRootNode: '',
);

716 717 718 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 770 771 772 773 774 775 776 777
/// Render the name on a line followed by the body and properties on the next
/// line omitting the children.
///
/// Example:
/// ```
/// <name>:
///   <description>(<property1>, <property2>, ..., <propertyN>)
/// ```
///
/// See also:
///
///  * [DiagnosticsTreeStyle.errorProperty], uses this style for ASCII art
///    display.
final TextTreeConfiguration errorPropertyTextConfiguration = TextTreeConfiguration(
  propertySeparator: ', ',
  beforeProperties: '(',
  afterProperties: ')',
  prefixLineOne: '',
  prefixOtherLines: '',
  prefixLastChildLineOne: '',
  lineBreak: '\n',
  lineBreakProperties: false,
  addBlankLineIfNoChildren: false,
  showChildren: false,
  propertyPrefixIfChildren: '  ',
  propertyPrefixNoChildren: '  ',
  linkCharacter: '',
  prefixOtherLinesRootNode: '',
  afterName: ':',
  isNameOnOwnLine: true,
);

/// Render a node on multiple lines omitting children.
///
/// Example:
/// `<name>: <description>
///   <property1>
///   <property2>
///   <propertyN>`
///
/// See also:
///
///  * [DiagnosticsTreeStyle.shallow]
final TextTreeConfiguration shallowTextConfiguration = TextTreeConfiguration(
  prefixLineOne: '',
  prefixLastChildLineOne: '',
  prefixOtherLines: ' ',
  prefixOtherLinesRootNode: '  ',
  bodyIndent: '',
  propertyPrefixIfChildren: '',
  propertyPrefixNoChildren: '',
  linkCharacter: ' ',
  addBlankLineIfNoChildren: false,
  // Add a colon after the description and before the properties to link the
  // properties to the description line.
  afterDescriptionIfBody: ':',
  isBlankLineBetweenPropertiesAndChildren: false,
  showChildren: false,
);

enum _WordWrapParseMode { inSpace, inWord, atBreak }

778 779 780 781 782 783 784 785
/// 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 {
786
  _PrefixedStringBuilder({
787 788
    required this.prefixLineOne,
    required String? prefixOtherLines,
789 790
    this.wrapWidth}) :
      _prefixOtherLines = prefixOtherLines;
791 792 793 794 795 796 797 798

  /// 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.
799 800 801
  String? get prefixOtherLines => _nextPrefixOtherLines ?? _prefixOtherLines;
  String? _prefixOtherLines;
  set prefixOtherLines(String? prefix) {
802 803 804
    _prefixOtherLines = prefix;
    _nextPrefixOtherLines = null;
  }
805

806 807
  String? _nextPrefixOtherLines;
  void incrementPrefixOtherLines(String suffix, {required bool updateCurrentLine}) {
808
    if (_currentLine.isEmpty || updateCurrentLine) {
809
      _prefixOtherLines = prefixOtherLines! + suffix;
810 811
      _nextPrefixOtherLines = null;
    } else {
812
      _nextPrefixOtherLines = prefixOtherLines! + suffix;
813 814 815
    }
  }

816
  final int? wrapWidth;
817 818

  /// Buffer containing lines that have already been completely laid out.
819
  final StringBuffer _buffer = StringBuffer();
820 821 822 823 824
  /// 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>[];
825 826

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

830
  bool get isCurrentLineEmpty => _currentLine.isEmpty;
831

832 833 834 835 836 837 838 839 840 841 842 843 844 845
  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,
      );
846 847
      return;
    }
848 849 850
    final Iterable<String> lines = _wordWrapLine(
      text,
      _wrappableRanges,
851 852 853
      wrapWidth!,
      startOffset: firstLine ? prefixLineOne.length : _prefixOtherLines!.length,
      otherLineOffset: firstLine ? _prefixOtherLines!.length : _prefixOtherLines!.length,
854 855 856
    );
    int i = 0;
    final int length = lines.length;
857
    for (final String line in lines) {
858 859 860 861 862 863
      i++;
      _writeLine(
        line,
        includeLineBreak: addTrailingLineBreak || i < length,
        firstLine: firstLine,
      );
864
    }
865 866
    _wrappableRanges.clear();
  }
867

868 869 870 871 872 873 874 875 876 877
  /// 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.
878
  static Iterable<String> _wordWrapLine(String message, List<int> wrapRanges, int width, { int startOffset = 0, int otherLineOffset = 0}) sync* {
879 880 881 882 883 884 885 886 887
    if (message.length + startOffset < width) {
      // Nothing to do. The line doesn't wrap.
      yield message;
      return;
    }
    int startForLengthCalculations = -startOffset;
    bool addPrefix = false;
    int index = 0;
    _WordWrapParseMode mode = _WordWrapParseMode.inSpace;
888 889
    late int lastWordStart;
    int? lastWordEnd;
890 891 892 893 894 895 896 897 898 899 900 901 902 903 904
    int start = 0;

    int currentChunk = 0;

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

        if (index < wrapRanges[currentChunk + 1])
          break; // Found nearest chunk.
        currentChunk+= 2;
      }
      return index < wrapRanges[currentChunk];
905
    }
906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957
    while (true) {
      switch (mode) {
        case _WordWrapParseMode.inSpace: // at start of break point (or start of line); can't break until next break
          while ((index < message.length) && (message[index] == ' '))
            index += 1;
          lastWordStart = index;
          mode = _WordWrapParseMode.inWord;
          break;
        case _WordWrapParseMode.inWord: // looking for a good break point. Treat all text
          while ((index < message.length) && (message[index] != ' ' || noWrap(index)))
            index += 1;
          mode = _WordWrapParseMode.atBreak;
          break;
        case _WordWrapParseMode.atBreak: // at start of break point
          if ((index - startForLengthCalculations > width) || (index == message.length)) {
            // we are over the width line, so break
            if ((index - startForLengthCalculations <= width) || (lastWordEnd == null)) {
              // we should use this point, because either it doesn't actually go over the
              // end (last line), or it does, but there was no earlier break point
              lastWordEnd = index;
            }
            final String line = message.substring(start, lastWordEnd);
            yield line;
            addPrefix = true;
            if (lastWordEnd >= message.length)
              return;
            // just yielded a line
            if (lastWordEnd == index) {
              // we broke at current position
              // eat all the wrappable spaces, then set our start point
              // Even if some of the spaces are not wrappable that is ok.
              while ((index < message.length) && (message[index] == ' '))
                index += 1;
              start = index;
              mode = _WordWrapParseMode.inWord;
            } else {
              // we broke at the previous break point, and we're at the start of a new one
              assert(lastWordStart > lastWordEnd);
              start = lastWordStart;
              mode = _WordWrapParseMode.atBreak;
            }
            startForLengthCalculations = start - otherLineOffset;
            assert(addPrefix);
            lastWordEnd = null;
          } else {
            // save this break point, we're not yet over the line width
            lastWordEnd = index;
            // skip to the end of this break point
            mode = _WordWrapParseMode.inSpace;
          }
          break;
      }
958
    }
959
  }
960

961 962 963 964 965 966 967 968
  /// Write text ensuring the specified prefixes for the first and subsequent
  /// lines.
  ///
  /// If [allowWrap] is true, the text may be wrapped to stay within the
  /// allow `wrapWidth`.
  void write(String s, {bool allowWrap = false}) {
    if (s.isEmpty)
      return;
969

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

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

1011
  String? _getCurrentPrefix(bool firstLine) {
1012 1013
    return _buffer.isEmpty ? prefixLineOne : (firstLine ? _prefixOtherLines : _prefixOtherLines);
  }
1014

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

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

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

1033 1034 1035
  /// Finishes the current line with a stretched version of text.
  void writeStretched(String text, int targetLineLength) {
    write(text);
1036
    final int currentLineLength = _currentLine.length + _getCurrentPrefix(_buffer.isEmpty)!.length;
1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054
    assert (_currentLine.length > 0);
    final int targetLength = targetLineLength - currentLineLength;
    if (targetLength > 0) {
      assert(text.isNotEmpty);
      final String lastChar = text[text.length - 1];
      assert(lastChar != '\n');
      _currentLine.write(lastChar * targetLength);
    }
    // Mark the entire line as not wrappable.
    _wrappableRanges.clear();
  }

  String build() {
    if (_currentLine.isNotEmpty)
      _finalizeLine(false);

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

class _NoDefaultValue {
  const _NoDefaultValue();
}

1061
/// Marker object indicating that a [DiagnosticsNode] has no default value.
1062
const _NoDefaultValue 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 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107
///    string representation of this node and its descendants.
class TextTreeRenderer {
  /// Creates a [TextTreeRenderer] object with the given arguments specifying
  /// how the tree is rendered.
  ///
  /// Lines are wrapped to at the maximum of [wrapWidth] and the current indent
  /// plus [wrapWidthProperties] characters. This ensures that wrapping does not
  /// become too excessive when displaying very deep trees and that wrapping
  /// only occurs at the overall [wrapWidth] when the tree is not very indented.
  /// If [maxDescendentsTruncatableNode] is specified, [DiagnosticsNode] objects
  /// with `allowTruncate` set to `true` are truncated after including
  /// [maxDescendentsTruncatableNode] descendants of the node to be truncated.
  TextTreeRenderer({
    DiagnosticLevel minLevel = DiagnosticLevel.debug,
    int wrapWidth = 100,
    int wrapWidthProperties = 65,
    int maxDescendentsTruncatableNode = -1,
  }) : assert(minLevel != null),
       _minLevel = minLevel,
       _wrapWidth = wrapWidth,
       _wrapWidthProperties = wrapWidthProperties,
       _maxDescendentsTruncatableNode = maxDescendentsTruncatableNode;

  final int _wrapWidth;
  final int _wrapWidthProperties;
  final DiagnosticLevel _minLevel;
  final int _maxDescendentsTruncatableNode;

  /// Text configuration to use to connect this node to a `child`.
  ///
  /// The singleLine styles are special cased because the connection from the
  /// parent to the child should be consistent with the parent's style as the
  /// single line style does not provide any meaningful style for how children
  /// should be connected to their parents.
1108
  TextTreeConfiguration? _childTextConfiguration(
1109 1110
    DiagnosticsNode child,
    TextTreeConfiguration textStyle,
1111
  ) {
1112
    final DiagnosticsTreeStyle? childStyle = child.style;
1113 1114 1115 1116 1117 1118 1119
    return (_isSingleLine(childStyle) || childStyle == DiagnosticsTreeStyle.errorProperty) ? textStyle : child.textTreeConfiguration;
  }

  /// Renders a [node] to a String.
  String render(
    DiagnosticsNode node, {
    String prefixLineOne = '',
1120 1121
    String? prefixOtherLines,
    TextTreeConfiguration? parentConfiguration,
1122
  }) {
1123 1124
    if (kReleaseMode) {
      return '';
1125 1126 1127 1128 1129 1130
    } else {
      return _debugRender(
          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 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159
    if (prefixOtherLines.isEmpty)
      prefixOtherLines += config.prefixOtherLinesRootNode;

    if (node.style == DiagnosticsTreeStyle.truncateChildren) {
      // This style is different enough that it isn't worthwhile to reuse the
      // existing logic.
      final List<String> descendants = <String>[];
      const int maxDepth = 5;
      int depth = 0;
      const int maxLines = 25;
      int lines = 0;
      void visitor(DiagnosticsNode node) {
1160
        for (final DiagnosticsNode child in node.getChildren()) {
1161 1162 1163 1164 1165 1166 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
          if (lines < maxLines) {
            depth += 1;
            descendants.add('$prefixOtherLines${"  " * depth}$child');
            if (depth < maxDepth)
              visitor(child);
            depth -= 1;
          } else if (lines == maxLines) {
            descendants.add('$prefixOtherLines  ...(descendants list truncated after $lines lines)');
          }
          lines += 1;
        }
      }
      visitor(node);
      final StringBuffer information = StringBuffer(prefixLineOne);
      if (lines > 1) {
        information.writeln('This ${node.name} had the following descendants (showing up to depth $maxDepth):');
      } else if (descendants.length == 1) {
        information.writeln('This ${node.name} had the following child:');
      } else {
        information.writeln('This ${node.name} has no descendants.');
      }
      information.writeAll(descendants, '\n');
      return information.toString();
    }
    final _PrefixedStringBuilder builder = _PrefixedStringBuilder(
      prefixLineOne: prefixLineOne,
      prefixOtherLines: prefixOtherLines,
      wrapWidth: math.max(_wrapWidth, prefixOtherLines.length + _wrapWidthProperties),
    );

    List<DiagnosticsNode> children = node.getChildren();

1193
    String? description = node.toDescription(parentConfiguration: parentConfiguration);
1194 1195 1196 1197 1198 1199
    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;
1200
    String? name = node.name;
1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 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
    if (uppercaseTitle) {
      name = name?.toUpperCase();
    }
    if (description == null || description.isEmpty) {
      if (node.showName && name != null)
        builder.write(name, allowWrap: wrapName);
    } else {
      bool includeName = false;
      if (name != null && name.isNotEmpty && node.showName) {
        includeName = true;
        builder.write(name, allowWrap: wrapName);
        if (node.showSeparator)
          builder.write(config.afterName, allowWrap: wrapName);

        builder.write(
          config.isNameOnOwnLine || description.contains('\n') ? '\n' : ' ',
          allowWrap: wrapName,
        );
      }
      if (!isSingleLine && builder.requiresMultipleLines && !builder.isCurrentLineEmpty) {
        // Make sure there is a break between the current line and next one if
        // there is not one already.
        builder.write('\n');
      }
      if (includeName) {
        builder.incrementPrefixOtherLines(
          children.isEmpty ? config.propertyPrefixNoChildren : config.propertyPrefixIfChildren,
          updateCurrentLine: true,
        );
      }

      if (uppercaseTitle) {
        description = description.toUpperCase();
      }
      builder.write(description.trimRight(), allowWrap: wrapDescription);

      if (!includeName) {
        builder.incrementPrefixOtherLines(
          children.isEmpty ? config.propertyPrefixNoChildren : config.propertyPrefixIfChildren,
          updateCurrentLine: false,
        );
      }
    }
    if (config.suffixLineOne.isNotEmpty) {
1245
      builder.writeStretched(config.suffixLineOne, builder.wrapWidth!);
1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287
    }

    final Iterable<DiagnosticsNode> propertiesIterable = node.getProperties().where(
            (DiagnosticsNode n) => !n.isFiltered(_minLevel)
    );
    List<DiagnosticsNode> properties;
    if (_maxDescendentsTruncatableNode >= 0 && node.allowTruncate) {
      if (propertiesIterable.length < _maxDescendentsTruncatableNode) {
        properties =
            propertiesIterable.take(_maxDescendentsTruncatableNode).toList();
        properties.add(DiagnosticsNode.message('...'));
      } else {
        properties = propertiesIterable.toList();
      }
      if (_maxDescendentsTruncatableNode < children.length) {
        children = children.take(_maxDescendentsTruncatableNode).toList();
        children.add(DiagnosticsNode.message('...'));
      }
    } else {
      properties = propertiesIterable.toList();
    }

    // If the node does not show a separator and there is no description then
    // we should not place a separator between the name and the value.
    // Essentially in this case the properties are treated a bit like a value.
    if ((properties.isNotEmpty || children.isNotEmpty || node.emptyBodyDescription != null) &&
        (node.showSeparator || description?.isNotEmpty == true)) {
      builder.write(config.afterDescriptionIfBody);
    }

    if (config.lineBreakProperties)
      builder.write(config.lineBreak);

    if (properties.isNotEmpty)
      builder.write(config.beforeProperties);

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

    if (node.emptyBodyDescription != null &&
        properties.isEmpty &&
        children.isEmpty &&
        prefixLineOne.isNotEmpty) {
1288
      builder.write(node.emptyBodyDescription!);
1289 1290 1291 1292 1293 1294 1295 1296 1297
      if (config.lineBreakProperties)
        builder.write(config.lineBreak);
    }

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

1298
      final TextTreeConfiguration propertyStyle = property.textTreeConfiguration!;
1299 1300 1301 1302 1303
      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,
1304
          prefixLineOne: propertyStyle.prefixLineOne,
1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315
          prefixOtherLines: '${propertyStyle.childLinkSpace}${propertyStyle.prefixOtherLines}',
          parentConfiguration: config,
        );
        final List<String> propertyLines = propertyRender.split('\n');
        if (propertyLines.length == 1 && !config.lineBreakProperties) {
          builder.write(propertyLines.first);
        } else {
          builder.write(propertyRender, allowWrap: false);
          if (!propertyRender.endsWith('\n'))
            builder.write('\n');
        }
1316
      } else {
1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332
        final String propertyRender = render(property,
          prefixLineOne: '${builder.prefixOtherLines}${propertyStyle.prefixLineOne}',
          prefixOtherLines: '${builder.prefixOtherLines}${propertyStyle.childLinkSpace}${propertyStyle.prefixOtherLines}',
          parentConfiguration: config,
        );
        builder.writeRawLines(propertyRender);
      }
    }
    if (properties.isNotEmpty)
      builder.write(config.afterProperties);

    builder.write(config.mandatoryAfterProperties);

    if (!config.lineBreakProperties)
      builder.write(config.lineBreak);

1333
    final String prefixChildren = config.bodyIndent;
1334 1335 1336 1337
    final String prefixChildrenRaw = '$prefixOtherLines$prefixChildren';
    if (children.isEmpty &&
        config.addBlankLineIfNoChildren &&
        builder.requiresMultipleLines &&
1338
        builder.prefixOtherLines!.trimRight().isNotEmpty
1339 1340 1341 1342 1343 1344 1345
    ) {
      builder.write(config.lineBreak);
    }

    if (children.isNotEmpty && config.showChildren) {
      if (config.isBlankLineBetweenPropertiesAndChildren &&
          properties.isNotEmpty &&
1346
          children.first.textTreeConfiguration!.isBlankLineBetweenPropertiesAndChildren) {
1347 1348 1349 1350 1351 1352 1353 1354
        builder.write(config.lineBreak);
      }

      builder.prefixOtherLines = prefixOtherLines;

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

1408
/// Defines diagnostics data for a [value].
1409
///
1410
/// For debug and profile modes, [DiagnosticsNode] provides a high quality
1411
/// multiline string dump via [toStringDeep]. The core members are the [name],
1412 1413 1414 1415 1416 1417
/// [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.
1418
abstract class DiagnosticsNode {
1419 1420
  /// Initializes the object.
  ///
1421 1422
  /// The [style], [showName], and [showSeparator] arguments must not
  /// be null.
1423
  DiagnosticsNode({
1424
    required this.name,
1425
    this.style,
1426 1427
    this.showName = true,
    this.showSeparator = true,
1428
    this.linePrefix,
1429
  }) : assert(showName != null),
1430 1431 1432 1433
       assert(showSeparator != null),
       // A name ending with ':' indicates that the user forgot that the ':' will
       // be automatically added for them when generating descriptions of the
       // property.
1434 1435 1436 1437 1438 1439
       assert(
         name == null || !name.endsWith(':'),
         'Names of diagnostic nodes must not end with colons.\n'
         'name:\n'
         '  "$name"'
       );
1440 1441 1442 1443

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

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

1475 1476 1477 1478 1479 1480
  /// 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.
1481
  String? toDescription({ TextTreeConfiguration? parentConfiguration });
1482

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

1489 1490 1491 1492
  /// Whether the diagnostic should be filtered due to its [level] being lower
  /// than `minLevel`.
  ///
  /// If `minLevel` is [DiagnosticLevel.hidden] no diagnostics will be filtered.
1493
  /// If `minLevel` is [DiagnosticLevel.off] all diagnostics will be filtered.
1494
  bool isFiltered(DiagnosticLevel minLevel) => kReleaseMode || level.index < minLevel.index;
1495 1496 1497 1498 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
  /// subclasses have a `level` argument to their constructor which influences
  /// the value returned here but other factors also influence it. For example,
  /// whether an exception is thrown computing a property value
  /// [DiagnosticLevel.error] is returned.
1505
  DiagnosticLevel get level => kReleaseMode ? DiagnosticLevel.hidden : DiagnosticLevel.info;
1506

1507 1508 1509 1510 1511
  /// 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.
1512 1513
  final bool showName;

1514
  /// Prefix to include at the start of each line.
1515
  final String? linePrefix;
1516

1517
  /// Description to show if the node has no displayed properties or children.
1518
  String? get emptyBodyDescription => null;
1519

1520
  /// The actual object this is diagnostics data for.
1521
  Object? get value;
1522 1523

  /// Hint for how the node should be displayed.
1524
  final DiagnosticsTreeStyle? style;
1525

1526 1527 1528 1529 1530 1531 1532 1533 1534
  /// 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;

1535
  /// Properties of this [DiagnosticsNode].
1536 1537
  ///
  /// Properties and children are kept distinct even though they are both
1538
  /// [List<DiagnosticsNode>] because they should be grouped differently.
1539 1540
  List<DiagnosticsNode> getProperties();

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

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

1551 1552
  /// Serialize the node to a JSON map according to the configuration provided
  /// in the [DiagnosticsSerializationDelegate].
1553 1554 1555 1556 1557 1558 1559 1560 1561 1562
  ///
  /// 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
1563 1564
  Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
    Map<String, Object?> result = <String, Object?>{};
1565 1566
    assert(() {
      final bool hasChildren = getChildren().isNotEmpty;
1567
      result = <String, Object?>{
1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580
        'description': toDescription(),
        'type': runtimeType.toString(),
        if (name != null)
          'name': name,
        if (!showSeparator)
          'showSeparator': showSeparator,
        if (level != DiagnosticLevel.info)
          'level': describeEnum(level),
        if (showName == false)
          'showName': showName,
        if (emptyBodyDescription != null)
          'emptyBodyDescription': emptyBodyDescription,
        if (style != DiagnosticsTreeStyle.sparse)
1581
          'style': describeEnum(style!),
1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608
        if (allowTruncate)
          'allowTruncate': allowTruncate,
        if (hasChildren)
          'hasChildren': hasChildren,
        if (linePrefix?.isNotEmpty == true)
          '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;
1609 1610
  }

1611 1612 1613 1614 1615
  /// 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].
1616 1617 1618
  static List<Map<String, Object?>> toJsonList(
    List<DiagnosticsNode>? nodes,
    DiagnosticsNode? parent,
1619
    DiagnosticsSerializationDelegate delegate,
1620 1621 1622
  ) {
    bool truncated = false;
    if (nodes == null)
1623
      return const <Map<String, Object?>>[];
1624 1625 1626 1627 1628 1629
    final int originalNodeCount = nodes.length;
    nodes = delegate.truncateNodesList(nodes, parent);
    if (nodes.length != originalNodeCount) {
      nodes.add(DiagnosticsNode.message('...'));
      truncated = true;
    }
1630
    final List<Map<String, Object?>> json = nodes.map<Map<String, Object?>>((DiagnosticsNode node) {
1631 1632 1633 1634 1635 1636 1637
      return node.toJsonMap(delegate.delegateForNode(node));
    }).toList();
    if (truncated)
      json.last['truncated'] = true;
    return json;
  }

1638 1639 1640 1641 1642 1643
  /// 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.
1644 1645 1646
  ///
  /// `minLevel` specifies the minimum [DiagnosticLevel] for properties included
  /// in the output.
1647 1648 1649
  ///
  /// In release mode, far less information is retained and some information may
  /// not print at all.
1650
  @override
1651
  String toString({
1652
    TextTreeConfiguration? parentConfiguration,
1653
    DiagnosticLevel minLevel = DiagnosticLevel.info,
1654
  }) {
1655
    String result = super.toString();
1656
    assert(style != null);
1657
    assert(minLevel != null);
1658 1659 1660 1661 1662
    assert(() {
      if (_isSingleLine(style)) {
        result = toStringDeep(
            parentConfiguration: parentConfiguration, minLevel: minLevel);
      } else {
1663
        final String description = toDescription(parentConfiguration: parentConfiguration)!;
1664

1665
        if (name == null || name!.isEmpty || !showName) {
1666 1667 1668 1669 1670 1671 1672 1673 1674
          result = description;
        } else {
          result = description.contains('\n') ? '$name$_separator\n$description'
              : '$name$_separator $description';
        }
      }
      return true;
    }());
    return result;
1675 1676
  }

1677 1678 1679
  /// Returns a configuration specifying how this object should be rendered
  /// as text art.
  @protected
1680
  TextTreeConfiguration? get textTreeConfiguration {
1681
    assert(style != null);
1682
    switch (style!) {
1683 1684
      case DiagnosticsTreeStyle.none:
        return null;
1685 1686 1687 1688 1689 1690 1691 1692 1693
      case DiagnosticsTreeStyle.dense:
        return denseTextConfiguration;
      case DiagnosticsTreeStyle.sparse:
        return sparseTextConfiguration;
      case DiagnosticsTreeStyle.offstage:
        return dashedTextConfiguration;
      case DiagnosticsTreeStyle.whitespace:
        return whitespaceTextConfiguration;
      case DiagnosticsTreeStyle.transition:
1694
        return transitionTextConfiguration;
1695 1696
      case DiagnosticsTreeStyle.singleLine:
        return singleLineTextConfiguration;
1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708
      case DiagnosticsTreeStyle.errorProperty:
        return errorPropertyTextConfiguration;
      case DiagnosticsTreeStyle.shallow:
        return shallowTextConfiguration;
      case DiagnosticsTreeStyle.error:
        return errorTextConfiguration;
      case DiagnosticsTreeStyle.truncateChildren:
        // Truncate children doesn't really need its own text style as the
        // rendering is quite custom.
        return whitespaceTextConfiguration;
      case DiagnosticsTreeStyle.flat:
        return flatTextConfiguration;
1709 1710 1711 1712 1713
    }
  }

  /// Returns a string representation of this node and its descendants.
  ///
1714 1715 1716 1717 1718
  /// `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.
  ///
1719 1720 1721 1722 1723
  /// `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.
1724
  ///
1725 1726 1727
  /// In release mode, far less information is retained and some information may
  /// not print at all.
  ///
1728 1729
  /// See also:
  ///
1730
  ///  * [toString], for a brief description of the [value] but not its
1731
  ///    children.
1732
  String toStringDeep({
1733
    String prefixLineOne = '',
1734 1735
    String? prefixOtherLines,
    TextTreeConfiguration? parentConfiguration,
1736
    DiagnosticLevel minLevel = DiagnosticLevel.debug,
1737
  }) {
1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752
    String result = '';
    assert(() {
      result = TextTreeRenderer(
        minLevel: minLevel,
        wrapWidth: 65,
        wrapWidthProperties: 65,
      ).render(
        this,
        prefixLineOne: prefixLineOne,
        prefixOtherLines: prefixOtherLines,
        parentConfiguration: parentConfiguration,
      );
      return true;
    }());
    return result;
1753 1754 1755 1756 1757
  }
}

/// Debugging message displayed like a property.
///
1758
/// {@tool snippet}
1759 1760 1761 1762 1763
///
/// 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:
1764 1765
///
/// ```dart
1766 1767
/// var table = MessageProperty('table size', '$columns\u00D7$rows');
/// var usefulness = MessageProperty('usefulness ratio', 'no metrics collected yet (never painted)');
1768
/// ```
1769
/// {@end-tool}
1770
/// {@tool snippet}
1771
///
1772 1773
/// On the other hand, [StringProperty] is better suited when the property has a
/// concrete value that is a string:
1774
///
1775
/// ```dart
1776
/// var name = StringProperty('name', _name);
1777
/// ```
1778
/// {@end-tool}
1779 1780 1781
///
/// See also:
///
1782
///  * [DiagnosticsNode.message], which serves the same role for messages
1783
///    without a clear property name.
1784
///  * [StringProperty], which is a better fit for properties with string values.
1785
class MessageProperty extends DiagnosticsProperty<void> {
1786 1787 1788
  /// Create a diagnostics property that displays a message.
  ///
  /// Messages have no concrete [value] (so [value] will return null). The
1789
  /// message is stored as the description.
1790
  ///
1791
  /// The [name], `message`, and [level] arguments must not be null.
1792 1793 1794
  MessageProperty(
    String name,
    String message, {
1795
    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
1796
    DiagnosticLevel level = DiagnosticLevel.info,
1797 1798
  }) : assert(name != null),
       assert(message != null),
1799
       assert(style != null),
1800
       assert(level != null),
1801
       super(name, null, description: message, style: style, level: level);
1802 1803 1804 1805 1806 1807
}

/// Property which encloses its string [value] in quotes.
///
/// See also:
///
1808
///  * [MessageProperty], which is a better fit for showing a message
1809 1810
///    instead of describing a property with a string value.
class StringProperty extends DiagnosticsProperty<String> {
1811 1812
  /// Create a diagnostics property for strings.
  ///
1813
  /// The [showName], [quoted], [style], and [level] arguments must not be null.
1814 1815
  StringProperty(
    String name,
1816 1817 1818
    String? value, {
    String? description,
    String? tooltip,
1819
    bool showName = true,
1820
    Object? defaultValue = kNoDefaultValue,
1821
    this.quoted = true,
1822
    String? ifEmpty,
1823
    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
1824
    DiagnosticLevel level = DiagnosticLevel.info,
1825 1826
  }) : assert(showName != null),
       assert(quoted != null),
1827
       assert(style != null),
1828
       assert(level != null),
1829
       super(
1830 1831 1832 1833
    name,
    value,
    description: description,
    defaultValue: defaultValue,
Ian Hickson's avatar
Ian Hickson committed
1834
    tooltip: tooltip,
1835 1836
    showName: showName,
    ifEmpty: ifEmpty,
1837
    style: style,
1838
    level: level,
1839 1840
  );

Ian Hickson's avatar
Ian Hickson committed
1841
  /// Whether the value is enclosed in double quotes.
1842 1843
  final bool quoted;

1844
  @override
1845 1846
  Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
    final Map<String, Object?> json = super.toJsonMap(delegate);
1847 1848 1849 1850
    json['quoted'] = quoted;
    return json;
  }

1851
  @override
1852 1853
  String valueToString({ TextTreeConfiguration? parentConfiguration }) {
    String? text = _description ?? value;
1854 1855 1856 1857 1858 1859
    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.
1860
      text = text.replaceAll('\n', r'\n');
1861 1862
    }

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

abstract class _NumProperty<T extends num> extends DiagnosticsProperty<T> {
1875 1876
  _NumProperty(
    String name,
1877 1878
    T? value, {
    String? ifNull,
1879
    this.unit,
1880
    bool showName = true,
1881 1882
    Object? defaultValue = kNoDefaultValue,
    String? tooltip,
1883
    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
1884
    DiagnosticLevel level = DiagnosticLevel.info,
1885 1886 1887 1888 1889 1890 1891
  }) : super(
    name,
    value,
    ifNull: ifNull,
    showName: showName,
    defaultValue: defaultValue,
    tooltip: tooltip,
1892
    level: level,
1893
    style: style,
1894 1895
  );

1896 1897
  _NumProperty.lazy(
    String name,
1898
    ComputePropertyValueCallback<T?> computeValue, {
1899
    String? ifNull,
1900
    this.unit,
1901
    bool showName = true,
1902 1903
    Object? defaultValue = kNoDefaultValue,
    String? tooltip,
1904
    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
1905
    DiagnosticLevel level = DiagnosticLevel.info,
1906 1907 1908 1909 1910 1911 1912
  }) : super.lazy(
    name,
    computeValue,
    ifNull: ifNull,
    showName: showName,
    defaultValue: defaultValue,
    tooltip: tooltip,
1913
    style: style,
1914
    level: level,
1915 1916
  );

1917
  @override
1918 1919
  Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
    final Map<String, Object?> json = super.toJsonMap(delegate);
1920 1921 1922 1923 1924 1925
    if (unit != null)
      json['unit'] = unit;

    json['numberToString'] = numberToString();
    return json;
  }
1926 1927 1928 1929 1930 1931

  /// 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].
1932
  final String? unit;
1933 1934 1935 1936 1937

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

  @override
1938
  String valueToString({ TextTreeConfiguration? parentConfiguration }) {
1939 1940 1941
    if (value == null)
      return value.toString();

1942
    return unit != null ? '${numberToString()}$unit' : numberToString();
1943 1944
  }
}
1945
/// Property describing a [double] [value] with an optional [unit] of measurement.
1946 1947 1948
///
/// Numeric formatting is optimized for debug message readability.
class DoubleProperty extends _NumProperty<double> {
1949
  /// If specified, [unit] describes the unit for the [value] (e.g. px).
1950
  ///
1951
  /// The [showName], [style], and [level] arguments must not be null.
1952 1953
  DoubleProperty(
    String name,
1954 1955 1956 1957 1958
    double? value, {
    String? ifNull,
    String? unit,
    String? tooltip,
    Object? defaultValue = kNoDefaultValue,
1959
    bool showName = true,
1960
    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
1961
    DiagnosticLevel level = DiagnosticLevel.info,
1962
  }) : assert(showName != null),
1963
       assert(style != null),
1964 1965
       assert(level != null),
       super(
1966 1967 1968 1969 1970 1971 1972
    name,
    value,
    ifNull: ifNull,
    unit: unit,
    tooltip: tooltip,
    defaultValue: defaultValue,
    showName: showName,
1973
    style :style,
1974
    level: level,
1975 1976 1977 1978 1979 1980
  );

  /// Property with a [value] that is computed only when needed.
  ///
  /// Use if computing the property [value] may throw an exception or is
  /// expensive.
1981 1982
  ///
  /// The [showName] and [level] arguments must not be null.
1983 1984
  DoubleProperty.lazy(
    String name,
1985
    ComputePropertyValueCallback<double?> computeValue, {
1986
    String? ifNull,
1987
    bool showName = true,
1988 1989 1990
    String? unit,
    String? tooltip,
    Object? defaultValue = kNoDefaultValue,
1991
    DiagnosticLevel level = DiagnosticLevel.info,
1992 1993 1994
  }) : assert(showName != null),
       assert(level != null),
       super.lazy(
1995 1996
    name,
    computeValue,
1997
    showName: showName,
1998 1999 2000 2001
    ifNull: ifNull,
    unit: unit,
    tooltip: tooltip,
    defaultValue: defaultValue,
2002
    level: level,
2003 2004 2005
  );

  @override
2006
  String numberToString() => debugFormatDouble(value);
2007 2008 2009 2010 2011 2012
}

/// 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> {
2013 2014
  /// Create a diagnostics property for integers.
  ///
2015
  /// The [showName], [style], and [level] arguments must not be null.
2016 2017
  IntProperty(
    String name,
2018 2019
    int? value, {
    String? ifNull,
2020
    bool showName = true,
2021 2022
    String? unit,
    Object? defaultValue = kNoDefaultValue,
2023
    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
2024
    DiagnosticLevel level = DiagnosticLevel.info,
2025
  }) : assert(showName != null),
2026
       assert(level != null),
2027
       assert(style != null),
2028
       super(
2029 2030 2031 2032 2033 2034
    name,
    value,
    ifNull: ifNull,
    showName: showName,
    unit: unit,
    defaultValue: defaultValue,
2035
    level: level,
2036 2037 2038
  );

  @override
2039
  String numberToString() => value.toString();
2040 2041 2042 2043 2044
}

/// Property which clamps a [double] to between 0 and 1 and formats it as a
/// percentage.
class PercentProperty extends DoubleProperty {
2045 2046 2047 2048 2049 2050 2051
  /// 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.
  ///
2052
  /// The [showName] and [level] arguments must not be null.
2053 2054
  PercentProperty(
    String name,
2055
    double? fraction, {
2056
    String? ifNull,
2057
    bool showName = true,
2058 2059
    String? tooltip,
    String? unit,
2060
    DiagnosticLevel level  = DiagnosticLevel.info,
2061
  }) : assert(showName != null),
2062
       assert(level != null),
2063
       super(
2064 2065 2066 2067 2068 2069
    name,
    fraction,
    ifNull: ifNull,
    showName: showName,
    tooltip: tooltip,
    unit: unit,
2070
    level: level,
2071 2072 2073
  );

  @override
2074
  String valueToString({ TextTreeConfiguration? parentConfiguration }) {
2075 2076
    if (value == null)
      return value.toString();
2077
    return unit != null ? '${numberToString()} $unit' : numberToString();
2078 2079 2080 2081
  }

  @override
  String numberToString() {
2082 2083
    final double? v = value;
    if (v == null)
2084
      return value.toString();
2085
    return '${(v.clamp(0.0, 1.0) * 100.0).toStringAsFixed(1)}%';
2086 2087 2088 2089
  }
}

/// Property where the description is either [ifTrue] or [ifFalse] depending on
2090
/// whether [value] is true or false.
2091
///
2092 2093
/// Using [FlagProperty] instead of [DiagnosticsProperty<bool>] can make
/// diagnostics display more polished. For example, given a property named
2094
/// `visible` that is typically true, the following code will return 'hidden'
2095 2096
/// when `visible` is false and nothing when visible is true, in contrast to
/// `visible: true` or `visible: false`.
2097
///
2098
/// {@tool snippet}
2099 2100
///
/// ```dart
2101
/// FlagProperty(
2102 2103 2104 2105 2106
///   'visible',
///   value: true,
///   ifFalse: 'hidden',
/// )
/// ```
2107
/// {@end-tool}
2108
/// {@tool snippet}
2109
///
2110
/// [FlagProperty] should also be used instead of [DiagnosticsProperty<bool>]
2111 2112 2113 2114
/// if showing the bool value would not clearly indicate the meaning of the
/// property value.
///
/// ```dart
2115
/// FlagProperty(
2116 2117 2118 2119 2120 2121
///   'inherit',
///   value: inherit,
///   ifTrue: '<all styles inherited>',
///   ifFalse: '<no style specified>',
/// )
/// ```
2122
/// {@end-tool}
2123 2124 2125 2126
///
/// See also:
///
///  * [ObjectFlagProperty], which provides similar behavior describing whether
2127
///    a [value] is null.
2128
class FlagProperty extends DiagnosticsProperty<bool> {
2129
  /// Constructs a FlagProperty with the given descriptions with the specified descriptions.
2130 2131 2132
  ///
  /// [showName] defaults to false as typically [ifTrue] and [ifFalse] should
  /// be descriptions that make the property name redundant.
2133 2134
  ///
  /// The [showName] and [level] arguments must not be null.
2135 2136
  FlagProperty(
    String name, {
2137
    required bool? value,
2138 2139
    this.ifTrue,
    this.ifFalse,
2140
    bool showName = false,
2141
    Object? defaultValue,
2142
    DiagnosticLevel level = DiagnosticLevel.info,
2143 2144
  }) : assert(showName != null),
       assert(level != null),
2145
       assert(ifTrue != null || ifFalse != null),
2146
       super(
2147 2148 2149 2150 2151 2152
         name,
         value,
         showName: showName,
         defaultValue: defaultValue,
         level: level,
       );
2153

2154
  @override
2155 2156
  Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
    final Map<String, Object?> json = super.toJsonMap(delegate);
2157 2158 2159 2160 2161 2162 2163 2164
    if (ifTrue != null)
      json['ifTrue'] = ifTrue;
    if (ifFalse != null)
      json['ifFalse'] = ifFalse;

    return json;
  }

2165
  /// Description to use if the property [value] is true.
2166
  ///
2167 2168
  /// If not specified and [value] equals true the property's priority [level]
  /// will be [DiagnosticLevel.hidden].
2169
  final String? ifTrue;
2170

2171
  /// Description to use if the property value is false.
2172
  ///
2173 2174
  /// If not specified and [value] equals false, the property's priority [level]
  /// will be [DiagnosticLevel.hidden].
2175
  final String? ifFalse;
2176 2177

  @override
2178
  String valueToString({ TextTreeConfiguration? parentConfiguration }) {
2179 2180
    if (value == true) {
      if (ifTrue != null)
2181
        return ifTrue!;
2182 2183
    } else if (value == false) {
      if (ifFalse != null)
2184
        return ifFalse!;
2185 2186
    }
    return super.valueToString(parentConfiguration: parentConfiguration);
2187 2188 2189
  }

  @override
2190 2191 2192 2193 2194 2195
  bool get showName {
    if (value == null || (value == true && ifTrue == null) || (value == false && ifFalse == null)) {
      // We are missing a description for the flag value so we need to show the
      // flag name. The property will have DiagnosticLevel.hidden for this case
      // so users will not see this the property in this case unless they are
      // displaying hidden properties.
2196
      return true;
2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211
    }
    return super.showName;
  }

  @override
  DiagnosticLevel get level {
    if (value == true) {
      if (ifTrue == null)
        return DiagnosticLevel.hidden;
    }
    if (value == false) {
      if (ifFalse == null)
        return DiagnosticLevel.hidden;
    }
    return super.level;
2212 2213 2214 2215 2216 2217
  }
}

/// Property with an `Iterable<T>` [value] that can be displayed with
/// different [DiagnosticsTreeStyle] for custom rendering.
///
2218
/// If [style] is [DiagnosticsTreeStyle.singleLine], the iterable is described
2219 2220 2221
/// as a comma separated list, otherwise the iterable is described as a line
/// break separated list.
class IterableProperty<T> extends DiagnosticsProperty<Iterable<T>> {
2222 2223
  /// Create a diagnostics property for iterables (e.g. lists).
  ///
2224
  /// The [ifEmpty] argument is used to indicate how an iterable [value] with 0
2225
  /// elements is displayed. If [ifEmpty] equals null that indicates that an
2226 2227 2228
  /// 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.
2229
  ///
2230
  /// The [style], [showName], [showSeparator], and [level] arguments must not be null.
2231 2232
  IterableProperty(
    String name,
2233 2234 2235 2236
    Iterable<T>? value, {
    Object? defaultValue = kNoDefaultValue,
    String? ifNull,
    String? ifEmpty = '[]',
2237 2238
    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
    bool showName = true,
2239
    bool showSeparator = true,
2240
    DiagnosticLevel level = DiagnosticLevel.info,
2241
  }) : assert(style != null),
2242
       assert(showName != null),
2243
       assert(showSeparator != null),
2244
       assert(level != null),
2245
       super(
2246 2247 2248 2249 2250 2251
    name,
    value,
    defaultValue: defaultValue,
    ifNull: ifNull,
    ifEmpty: ifEmpty,
    style: style,
2252
    showName: showName,
2253
    showSeparator: showSeparator,
2254
    level: level,
2255 2256 2257
  );

  @override
2258
  String valueToString({TextTreeConfiguration? parentConfiguration}) {
2259 2260
    if (value == null)
      return value.toString();
2261

2262
    if (value!.isEmpty)
2263 2264
      return ifEmpty ?? '[]';

2265
    final Iterable<String> formattedValues = value!.map((T v) {
2266 2267 2268 2269 2270 2271 2272
      if (T == double && v is double) {
        return debugFormatDouble(v);
      } else {
        return v.toString();
      }
    });

2273 2274 2275
    if (parentConfiguration != null && !parentConfiguration.lineBreakProperties) {
      // Always display the value as a single line and enclose the iterable
      // value in brackets to avoid ambiguity.
2276
      return '[${formattedValues.join(', ')}]';
2277 2278
    }

2279
    return formattedValues.join(_isSingleLine(style) ? ', ' : '\n');
2280
  }
2281 2282 2283 2284

  /// Priority level of the diagnostic used to control which diagnostics should
  /// be shown and filtered.
  ///
2285
  /// If [ifEmpty] is null and the [value] is an empty [Iterable] then level
2286
  /// [DiagnosticLevel.fine] is returned in a similar way to how an
2287 2288
  /// [ObjectFlagProperty] handles when [ifNull] is null and the [value] is
  /// null.
2289 2290
  @override
  DiagnosticLevel get level {
2291
    if (ifEmpty == null && value != null && value!.isEmpty && super.level != DiagnosticLevel.hidden)
2292 2293 2294
      return DiagnosticLevel.fine;
    return super.level;
  }
2295 2296

  @override
2297 2298
  Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
    final Map<String, Object?> json = super.toJsonMap(delegate);
2299
    if (value != null) {
2300
      json['values'] = value!.map<String>((T value) => value.toString()).toList();
2301 2302 2303
    }
    return json;
  }
2304 2305 2306 2307
}

/// An property than displays enum values tersely.
///
2308 2309
/// The enum value is displayed with the class name stripped. For example:
/// [HitTestBehavior.deferToChild] is shown as `deferToChild`.
2310 2311 2312 2313
///
/// See also:
///
///  * [DiagnosticsProperty] which documents named parameters common to all
2314
///    [DiagnosticsProperty].
2315
class EnumProperty<T> extends DiagnosticsProperty<T> {
2316 2317
  /// Create a diagnostics property that displays an enum.
  ///
2318
  /// The [level] argument must also not be null.
2319 2320
  EnumProperty(
    String name,
2321 2322
    T? value, {
    Object? defaultValue = kNoDefaultValue,
2323
    DiagnosticLevel level  = DiagnosticLevel.info,
2324 2325
  }) : assert(level != null),
       super (
2326 2327 2328
    name,
    value,
    defaultValue: defaultValue,
2329
    level: level,
2330 2331 2332
  );

  @override
2333
  String valueToString({ TextTreeConfiguration? parentConfiguration }) {
2334 2335
    if (value == null)
      return value.toString();
2336
    return describeEnum(value!);
2337 2338 2339
  }
}

2340 2341 2342 2343 2344 2345
/// 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
2346
/// omitted, that is taken to mean that [level] should be
2347
/// [DiagnosticLevel.hidden] when [value] is non-null or null respectively.
2348
///
2349
/// This kind of diagnostics property is typically used for opaque
2350 2351 2352
/// 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.
2353 2354 2355
///
/// See also:
///
2356 2357 2358 2359
///
///  * [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").
2360
///  * [FlagProperty], which provides similar functionality describing whether
2361
///    a [value] is true or false.
2362
class ObjectFlagProperty<T> extends DiagnosticsProperty<T> {
2363 2364 2365 2366
  /// 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).
  ///
2367
  /// The [showName] and [level] arguments must not be null. Additionally, at
2368
  /// least one of [ifPresent] and [ifNull] must not be null.
2369 2370
  ObjectFlagProperty(
    String name,
2371
    T? value, {
2372
    this.ifPresent,
2373
    String? ifNull,
2374 2375
    bool showName = false,
    DiagnosticLevel level  = DiagnosticLevel.info,
2376 2377
  }) : assert(ifPresent != null || ifNull != null),
       assert(showName != null),
2378
       assert(level != null),
2379
       super(
2380 2381 2382 2383
    name,
    value,
    showName: showName,
    ifNull: ifNull,
2384
    level: level,
2385
  );
2386 2387 2388 2389 2390

  /// 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.
2391 2392
  ///
  /// The [name] and [level] arguments must not be null.
2393 2394
  ObjectFlagProperty.has(
    String name,
2395
    T? value, {
2396
    DiagnosticLevel level = DiagnosticLevel.info,
2397 2398 2399 2400 2401 2402 2403 2404 2405
  }) : assert(name != null),
       assert(level != null),
       ifPresent = 'has $name',
       super(
    name,
    value,
    showName: false,
    level: level,
  );
2406

2407
  /// Description to use if the property [value] is not null.
2408
  ///
2409
  /// If the property [value] is not null and [ifPresent] is null, the
2410
  /// [level] for the property is [DiagnosticLevel.hidden] and the description
2411
  /// from superclass is used.
2412
  final String? ifPresent;
2413 2414

  @override
2415
  String valueToString({ TextTreeConfiguration? parentConfiguration }) {
2416 2417
    if (value != null) {
      if (ifPresent != null)
2418
        return ifPresent!;
2419 2420
    } else {
      if (ifNull != null)
2421
        return ifNull!;
2422 2423
    }
    return super.valueToString(parentConfiguration: parentConfiguration);
2424 2425 2426
  }

  @override
2427 2428 2429 2430 2431 2432
  bool get showName {
    if ((value != null && ifPresent == null) || (value == null && ifNull == null)) {
      // We are missing a description for the flag value so we need to show the
      // flag name. The property will have DiagnosticLevel.hidden for this case
      // so users will not see this the property in this case unless they are
      // displaying hidden properties.
2433
      return true;
2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448
    }
    return super.showName;
  }

  @override
  DiagnosticLevel get level {
    if (value != null) {
      if (ifPresent == null)
        return DiagnosticLevel.hidden;
    } else {
      if (ifNull == null)
        return DiagnosticLevel.hidden;
    }

    return super.level;
2449
  }
2450 2451

  @override
2452 2453
  Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
    final Map<String, Object?> json = super.toJsonMap(delegate);
2454 2455 2456 2457
    if (ifPresent != null)
      json['ifPresent'] = ifPresent;
    return json;
  }
2458 2459
}

2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476
/// 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:
///
2477
///  * [ObjectFlagProperty], which provides similar functionality but accepts
2478 2479 2480
///    only one flag, and is preferred if there is only one entry.
///  * [IterableProperty], which provides similar functionality describing
///    the values a collection of objects.
2481
class FlagsSummary<T> extends DiagnosticsProperty<Map<String, T?>> {
2482 2483 2484 2485 2486 2487 2488
  /// 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(
    String name,
2489
    Map<String, T?> value, {
2490
    String? ifEmpty,
2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507
    bool showName = true,
    bool showSeparator = true,
    DiagnosticLevel level  = DiagnosticLevel.info,
  }) : assert(value != null),
       assert(showName != null),
       assert(showSeparator != null),
       assert(level != null),
       super(
         name,
         value,
         ifEmpty: ifEmpty,
         showName: showName,
         showSeparator: showSeparator,
         level: level,
       );

  @override
2508
  Map<String, T?> get value => super.value!;
2509 2510 2511

  @override
  String valueToString({TextTreeConfiguration? parentConfiguration}) {
2512 2513
    assert(value != null);
    if (!_hasNonNullEntry() && ifEmpty != null)
2514
      return ifEmpty!;
2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538

    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 {
    if (!_hasNonNullEntry() && ifEmpty == null)
      return DiagnosticLevel.hidden;
    return super.level;
  }

  @override
2539 2540
  Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
    final Map<String, Object?> json = super.toJsonMap(delegate);
2541 2542 2543 2544 2545
    if (value.isNotEmpty)
      json['values'] = _formattedValues().toList();
    return json;
  }

2546
  bool _hasNonNullEntry() => value.values.any((T? o) => o != null);
2547 2548 2549 2550 2551 2552 2553 2554

  // An iterable of each entry's description in [value].
  //
  // For a non-null value, its description is its key.
  //
  // For a null value, it is omitted unless `includeEmtpy` is true and
  // [ifEntryNull] contains a corresponding description.
  Iterable<String> _formattedValues() sync* {
2555
    for (final MapEntry<String, T?> entry in value.entries) {
2556 2557 2558 2559 2560 2561 2562
      if (entry.value != null) {
        yield entry.key;
      }
    }
  }
}

2563 2564 2565 2566 2567
/// 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.
2568
typedef ComputePropertyValueCallback<T> = T? Function();
2569 2570 2571

/// Property with a [value] of type [T].
///
2572
/// If the default `value.toString()` does not provide an adequate description
2573
/// of the value, specify `description` defining a custom description.
2574 2575 2576
///
/// The [showSeparator] property indicates whether a separator should be placed
/// between the property [name] and its [value].
2577
class DiagnosticsProperty<T> extends DiagnosticsNode {
2578 2579
  /// Create a diagnostics property.
  ///
2580 2581 2582 2583 2584
  /// The [showName], [showSeparator], [style], [missingIfNull], and [level]
  /// arguments must not be null.
  ///
  /// The [level] argument is just a suggestion and can be overridden if
  /// something else about the property causes it to have a lower or higher
2585 2586
  /// level. For example, if the property value is null and [missingIfNull] is
  /// true, [level] is raised to [DiagnosticLevel.warning].
2587
  DiagnosticsProperty(
2588 2589 2590 2591
    String? name,
    T? value, {
    String? description,
    String? ifNull,
2592
    this.ifEmpty,
2593 2594 2595
    bool showName = true,
    bool showSeparator = true,
    this.defaultValue = kNoDefaultValue,
2596
    this.tooltip,
2597
    this.missingIfNull = false,
2598
    String? linePrefix,
2599 2600 2601
    this.expandableValue = false,
    this.allowWrap = true,
    this.allowNameWrap = true,
2602 2603
    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
    DiagnosticLevel level = DiagnosticLevel.info,
2604
  }) : assert(showName != null),
2605 2606
       assert(showSeparator != null),
       assert(style != null),
2607
       assert(level != null),
2608
       _description = description,
2609 2610 2611
       _valueComputed = true,
       _value = value,
       _computeValue = null,
2612 2613
       ifNull = ifNull ?? (missingIfNull ? 'MISSING' : null),
       _defaultLevel = level,
2614 2615 2616 2617 2618
       super(
         name: name,
         showName: showName,
         showSeparator: showSeparator,
         style: style,
2619
         linePrefix: linePrefix,
2620 2621 2622 2623 2624 2625
      );

  /// Property with a [value] that is computed only when needed.
  ///
  /// Use if computing the property [value] may throw an exception or is
  /// expensive.
2626
  ///
2627 2628 2629 2630 2631 2632 2633
  /// The [showName], [showSeparator], [style], [missingIfNull], and [level]
  /// arguments must not be null.
  ///
  /// The [level] argument is just a suggestion and can be overridden if
  /// if something else about the property causes it to have a lower or higher
  /// level. For example, if calling `computeValue` throws an exception, [level]
  /// will always return [DiagnosticLevel.error].
2634
  DiagnosticsProperty.lazy(
2635
    String? name,
2636
    ComputePropertyValueCallback<T> computeValue, {
2637 2638
    String? description,
    String? ifNull,
2639
    this.ifEmpty,
2640 2641 2642
    bool showName = true,
    bool showSeparator = true,
    this.defaultValue = kNoDefaultValue,
2643
    this.tooltip,
2644
    this.missingIfNull = false,
2645 2646 2647
    this.expandableValue = false,
    this.allowWrap = true,
    this.allowNameWrap = true,
2648 2649
    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
    DiagnosticLevel level = DiagnosticLevel.info,
2650
  }) : assert(showName != null),
2651
       assert(showSeparator != null),
2652
       assert(defaultValue == kNoDefaultValue || defaultValue is T?),
2653
       assert(missingIfNull != null),
2654
       assert(style != null),
2655
       assert(level != null),
2656
       _description = description,
2657 2658 2659
       _valueComputed = false,
       _value = null,
       _computeValue = computeValue,
2660
       _defaultLevel = level,
2661
       ifNull = ifNull ?? (missingIfNull ? 'MISSING' : null),
2662 2663 2664 2665 2666 2667 2668
       super(
         name: name,
         showName: showName,
         showSeparator: showSeparator,
         style: style,
       );

2669
  final String? _description;
2670

2671 2672 2673 2674 2675 2676 2677 2678 2679 2680
  /// Whether to expose properties and children of the value as properties and
  /// children.
  final bool expandableValue;

  @override
  final bool allowWrap;

  @override
  final bool allowNameWrap;

2681
  @override
2682 2683 2684
  Map<String, Object?> toJsonMap(DiagnosticsSerializationDelegate delegate) {
    final T? v = value;
    List<Map<String, Object?>>? properties;
2685
    if (delegate.expandPropertyValues && delegate.includeProperties && v is Diagnosticable && getProperties().isEmpty) {
2686 2687 2688 2689 2690 2691 2692 2693
      // 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,
      );
    }
2694
    final Map<String, Object?> json = super.toJsonMap(delegate);
2695 2696 2697
    if (properties != null) {
      json['properties'] = properties;
    }
2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710
    if (defaultValue != kNoDefaultValue)
      json['defaultValue'] = defaultValue.toString();
    if (ifEmpty != null)
      json['ifEmpty'] = ifEmpty;
    if (ifNull != null)
      json['ifNull'] = ifNull;
    if (tooltip != null)
      json['tooltip'] = tooltip;
    json['missingIfNull'] = missingIfNull;
    if (exception != null)
      json['exception'] = exception.toString();
    json['propertyType'] = propertyType.toString();
    json['defaultLevel'] = describeEnum(_defaultLevel);
2711
    if (value is Diagnosticable || value is DiagnosticsNode)
2712
      json['isDiagnosticableValue'] = true;
2713 2714 2715 2716 2717
    if (v is num)
      // Workaround for https://github.com/flutter/flutter/issues/39937#issuecomment-529558033.
      // JSON.stringify replaces infinity and NaN with null.
      json['value'] = v.isFinite ? v :  v.toString();
    if (value is String || value is bool || value == null)
2718
      json['value'] = value;
2719 2720 2721
    return json;
  }

2722 2723
  /// Returns a string representation of the property value.
  ///
2724
  /// Subclasses should override this method instead of [toDescription] to
2725 2726 2727
  /// customize how property values are converted to strings.
  ///
  /// Overriding this method ensures that behavior controlling how property
2728 2729 2730
  /// 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].
2731 2732 2733 2734
  ///
  /// `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.
2735 2736
  String valueToString({ TextTreeConfiguration? parentConfiguration }) {
    final T? v = value;
2737 2738 2739
    // 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.
2740
    return v is DiagnosticableTree ? v.toStringShort() : v.toString();
2741
  }
2742 2743

  @override
2744
  String toDescription({ TextTreeConfiguration? parentConfiguration }) {
2745
    if (_description != null)
2746
      return _addTooltip(_description!);
2747 2748 2749 2750

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

2751
    if (ifNull != null && value == null)
2752
      return _addTooltip(ifNull!);
2753

2754
    String result = valueToString(parentConfiguration: parentConfiguration);
2755
    if (result.isEmpty && ifEmpty != null)
2756
      result = ifEmpty!;
2757
    return _addTooltip(result);
2758 2759
  }

2760 2761 2762 2763 2764 2765 2766
  /// If a [tooltip] is specified, add the tooltip it to the end of `text`
  /// enclosing it parenthesis to disambiguate the tooltip from the rest of
  /// the text.
  ///
  /// `text` must not be null.
  String _addTooltip(String text) {
    assert(text != null);
2767 2768 2769 2770
    return tooltip == null ? text : '$text ($tooltip)';
  }

  /// Description if the property [value] is null.
2771
  final String? ifNull;
2772 2773

  /// Description if the property description would otherwise be empty.
2774
  final String? ifEmpty;
2775 2776 2777 2778 2779 2780 2781

  /// 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.
2782
  final String? tooltip;
2783

2784 2785 2786 2787
  /// Whether a [value] of null causes the property to have [level]
  /// [DiagnosticLevel.warning] warning that the property is missing a [value].
  final bool missingIfNull;

2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798
  /// 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".
2799 2800 2801 2802 2803 2804
  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],
2805
  /// [value] returns null and the exception thrown can be found via the
2806 2807 2808 2809 2810
  /// [exception] property.
  ///
  /// See also:
  ///
  ///  * [valueToString], which converts the property value to a string.
2811
  @override
2812
  T? get value {
2813 2814 2815 2816
    _maybeCacheValue();
    return _value;
  }

2817
  T? _value;
2818 2819 2820

  bool _valueComputed;

2821
  Object? _exception;
2822 2823 2824 2825

  /// Exception thrown if accessing the property [value] threw an exception.
  ///
  /// Returns null if computing the property value did not throw an exception.
2826
  Object? get exception {
2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837
    _maybeCacheValue();
    return _exception;
  }

  void _maybeCacheValue() {
    if (_valueComputed)
      return;

    _valueComputed = true;
    assert(_computeValue != null);
    try {
2838
      _value = _computeValue!();
2839 2840 2841 2842 2843 2844
    } catch (exception) {
      _exception = exception;
      _value = null;
    }
  }

2845 2846 2847
  /// If the [value] of the property equals [defaultValue] the priority [level]
  /// of the property is downgraded to [DiagnosticLevel.fine] as the property
  /// value is uninteresting.
2848
  ///
2849 2850
  /// The [defaultValue] is [kNoDefaultValue] by default. Otherwise it must be of
  /// type `T?`.
2851
  final Object? defaultValue;
2852

2853
  final DiagnosticLevel _defaultLevel;
2854

2855 2856
  /// Priority level of the diagnostic used to control which diagnostics should
  /// be shown and filtered.
2857
  ///
2858 2859 2860 2861 2862 2863 2864
  /// The property level defaults to the value specified by the `level`
  /// constructor argument. The level is raised to [DiagnosticLevel.error] if
  /// an [exception] was thrown getting the property [value]. The level is
  /// raised to [DiagnosticLevel.warning] if the property [value] is null and
  /// the property is not allowed to be null due to [missingIfNull]. The
  /// priority level is lowered to [DiagnosticLevel.fine] if the property
  /// [value] equals [defaultValue].
2865
  @override
2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880
  DiagnosticLevel get level {
    if (_defaultLevel == DiagnosticLevel.hidden)
      return _defaultLevel;

    if (exception != null)
      return DiagnosticLevel.error;

    if (value == null && missingIfNull)
      return DiagnosticLevel.warning;

    // Use a low level when the value matches the default value.
    if (defaultValue != kNoDefaultValue && value == defaultValue)
      return DiagnosticLevel.fine;

    return _defaultLevel;
2881 2882
  }

2883
  final ComputePropertyValueCallback<T>? _computeValue;
2884 2885

  @override
2886 2887
  List<DiagnosticsNode> getProperties() {
    if (expandableValue) {
2888
      final T? object = value;
2889 2890 2891
      if (object is DiagnosticsNode) {
        return object.getProperties();
      }
2892
      if (object is Diagnosticable) {
2893 2894 2895 2896 2897
        return object.toDiagnosticsNode(style: style).getProperties();
      }
    }
    return const <DiagnosticsNode>[];
  }
2898 2899

  @override
2900 2901
  List<DiagnosticsNode> getChildren() {
    if (expandableValue) {
2902
      final T? object = value;
2903 2904 2905
      if (object is DiagnosticsNode) {
        return object.getChildren();
      }
2906
      if (object is Diagnosticable) {
2907 2908 2909 2910 2911
        return object.toDiagnosticsNode(style: style).getChildren();
      }
    }
    return const <DiagnosticsNode>[];
  }
2912 2913
}

2914 2915
/// [DiagnosticsNode] that lazily calls the associated [Diagnosticable] [value]
/// to implement [getChildren] and [getProperties].
2916
class DiagnosticableNode<T extends Diagnosticable> extends DiagnosticsNode {
2917
  /// Create a diagnostics describing a [Diagnosticable] value.
2918 2919 2920
  ///
  /// The [value] argument must not be null.
  DiagnosticableNode({
2921 2922 2923
    String? name,
    required this.value,
    required DiagnosticsTreeStyle? style,
2924 2925
  }) : assert(value != null),
       super(
2926 2927 2928 2929 2930
         name: name,
         style: style,
       );

  @override
2931
  final T value;
2932

2933
  DiagnosticPropertiesBuilder? _cachedBuilder;
2934

2935 2936 2937
  /// Retrieve the [DiagnosticPropertiesBuilder] of current node.
  ///
  /// It will cache the result to prevent duplicate operation.
2938
  DiagnosticPropertiesBuilder? get builder {
2939
    if (kReleaseMode) {
2940
      return null;
2941 2942 2943 2944
    } else {
      assert(() {
        if (_cachedBuilder == null) {
          _cachedBuilder = DiagnosticPropertiesBuilder();
2945
          value.debugFillProperties(_cachedBuilder!);
2946 2947 2948 2949
        }
        return true;
      }());
      return _cachedBuilder;
2950 2951 2952
    }
  }

2953 2954
  @override
  DiagnosticsTreeStyle get style {
2955
    return kReleaseMode ? DiagnosticsTreeStyle.none : super.style ?? builder!.defaultDiagnosticsTreeStyle;
2956
  }
2957 2958

  @override
2959
  String? get emptyBodyDescription => (kReleaseMode || kProfileMode) ? '' : builder!.emptyBodyDescription;
2960 2961

  @override
2962
  List<DiagnosticsNode> getProperties() => (kReleaseMode || kProfileMode) ? const <DiagnosticsNode>[] : builder!.properties;
2963 2964

  @override
2965 2966
  List<DiagnosticsNode> getChildren() {
    return const<DiagnosticsNode>[];
2967 2968 2969
  }

  @override
2970
  String toDescription({ TextTreeConfiguration? parentConfiguration }) {
2971 2972 2973 2974 2975 2976
    String result = '';
    assert(() {
      result = value.toStringShort();
      return true;
    }());
    return result;
2977 2978 2979
  }
}

2980
/// [DiagnosticsNode] for an instance of [DiagnosticableTree].
2981 2982 2983
class DiagnosticableTreeNode extends DiagnosticableNode<DiagnosticableTree> {
  /// Creates a [DiagnosticableTreeNode].
  DiagnosticableTreeNode({
2984 2985 2986
    String? name,
    required DiagnosticableTree value,
    required DiagnosticsTreeStyle? style,
2987 2988 2989 2990 2991
  }) : super(
         name: name,
         value: value,
         style: style,
       );
2992 2993

  @override
2994
  List<DiagnosticsNode> getChildren() => value.debugDescribeChildren();
2995 2996
}

2997
/// Returns a 5 character long hexadecimal string generated from
2998
/// [Object.hashCode]'s 20 least-significant bits.
2999
String shortHash(Object? object) {
3000 3001 3002 3003
  return object.hashCode.toUnsigned(20).toRadixString(16).padLeft(5, '0');
}

/// Returns a summary of the runtime type and hash code of `object`.
3004 3005 3006 3007 3008 3009 3010 3011
///
/// 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.
3012
String describeIdentity(Object? object) => '${objectRuntimeType(object, '<optimized out>')}#${shortHash(object)}';
3013

3014 3015 3016 3017 3018
// This method exists as a workaround for https://github.com/dart-lang/sdk/issues/30021
/// Returns a short description of an enum value.
///
/// Strips off the enum class name from the `enumEntry.toString()`.
///
3019
/// {@tool snippet}
3020 3021 3022 3023 3024 3025
///
/// ```dart
/// enum Day {
///   monday, tuesday, wednesday, thursday, friday, saturday, sunday
/// }
///
3026
/// void validateDescribeEnum() {
3027 3028 3029 3030
///   assert(Day.monday.toString() == 'Day.monday');
///   assert(describeEnum(Day.monday) == 'monday');
/// }
/// ```
3031
/// {@end-tool}
3032 3033 3034
String describeEnum(Object enumEntry) {
  final String description = enumEntry.toString();
  final int indexOfDot = description.indexOf('.');
3035 3036 3037 3038
  assert(
    indexOfDot != -1 && indexOfDot < description.length - 1,
    'The provided object "$enumEntry" is not an enum.',
  );
3039 3040 3041
  return description.substring(indexOfDot + 1);
}

3042
/// Builder to accumulate properties and configuration used to assemble a
3043
/// [DiagnosticsNode] from a [Diagnosticable] object.
3044
class DiagnosticPropertiesBuilder {
3045 3046 3047 3048 3049 3050 3051
  /// Creates a [DiagnosticPropertiesBuilder] with [properties] initialize to
  /// an empty array.
  DiagnosticPropertiesBuilder() : properties = <DiagnosticsNode>[];

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

3052 3053
  /// Add a property to the list of properties.
  void add(DiagnosticsNode property) {
3054
    assert(() {
3055
      properties.add(property);
3056 3057
      return true;
    }());
3058 3059 3060
  }

  /// List of properties accumulated so far.
3061
  final List<DiagnosticsNode> properties;
3062 3063 3064 3065 3066

  /// 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.
3067
  String? emptyBodyDescription;
3068 3069
}

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

3073 3074
/// A mixin class for providing string and [DiagnosticsNode] debug
/// representations describing the properties of an object.
3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096
///
/// 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.
3097
mixin Diagnosticable {
3098 3099 3100 3101 3102
  /// A brief description of this object, usually just the [runtimeType] and the
  /// [hashCode].
  ///
  /// See also:
  ///
3103 3104
  ///  * [toString], for a detailed description of the object.
  String toStringShort() => describeIdentity(this);
3105

3106
  @override
3107
  String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
3108
    String? fullString;
3109 3110 3111 3112 3113
    assert(() {
      fullString = toDiagnosticsNode(style: DiagnosticsTreeStyle.singleLine).toString(minLevel: minLevel);
      return true;
    }());
    return fullString ?? toStringShort();
3114 3115
  }

3116
  /// Returns a debug representation of the object that is used by debugging
3117
  /// tools and by [DiagnosticsNode.toStringDeep].
3118
  ///
3119
  /// Leave [name] as null if there is not a meaningful description of the
3120
  /// relationship between the this node and its parent.
3121
  ///
3122 3123 3124
  /// 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.
3125
  DiagnosticsNode toDiagnosticsNode({ String? name, DiagnosticsTreeStyle? style }) {
3126
    return DiagnosticableNode<Diagnosticable>(
3127
      name: name,
3128
      value: this,
3129 3130
      style: style,
    );
3131 3132
  }

3133 3134 3135 3136
  /// Add additional properties associated with the node.
  ///
  /// Use the most specific [DiagnosticsProperty] existing subclass to describe
  /// each property instead of the [DiagnosticsProperty] base class. There are
3137
  /// only a small number of [DiagnosticsProperty] subclasses each covering a
3138 3139
  /// common use case. Consider what values a property is relevant for users
  /// debugging as users debugging large trees are overloaded with information.
3140
  /// Common named parameters in [DiagnosticsNode] subclasses help filter when
3141 3142
  /// and how properties are displayed.
  ///
3143
  /// `defaultValue`, `showName`, `showSeparator`, and `level` keep string
3144 3145 3146 3147
  /// representations of diagnostics terse and hide properties when they are not
  /// very useful.
  ///
  ///  * Use `defaultValue` any time the default value of a property is
3148 3149
  ///    uninteresting. For example, specify a default value of null any time
  ///    a property being null does not indicate an error.
3150
  ///  * Avoid specifying the `level` parameter unless the result you want
3151
  ///    cannot be achieved by using the `defaultValue` parameter or using
3152 3153 3154 3155 3156
  ///    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
3157
  ///    DiagnosticsProperty<Object>('child(3, 4)', null, ifNull: 'is null', showSeparator: false).toString()
3158 3159 3160 3161
  ///    ```
  ///    Shows using `showSeparator` to get output `child(3, 4) is null` which
  ///    is more polished than `child(3, 4): is null`.
  ///    ```dart
3162
  ///    DiagnosticsProperty<IconData>('icon', icon, ifNull: '<empty>', showName: false)).toString()
3163 3164 3165 3166 3167 3168 3169 3170
  ///    ```
  ///    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.
  ///
3171
  /// ## DiagnosticsProperty subclasses for primitive types
3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182
  ///
  ///  * [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
3183
  ///    `DiagnosticsProperty<bool>` instead of using [FlagProperty] as the
3184 3185
  ///    output is more verbose but unambiguous.
  ///
3186
  /// ## Other important [DiagnosticsProperty] variants
3187 3188 3189 3190 3191 3192 3193 3194 3195
  ///
  ///  * [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.
3196 3197 3198 3199
  ///  * [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].
3200
  ///
3201
  /// If none of these subclasses apply, use the [DiagnosticsProperty]
3202 3203 3204
  /// 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
3205
  /// `toString` method implementation works fine using [DiagnosticsProperty]
3206 3207
  /// directly.
  ///
3208
  /// {@tool snippet}
3209 3210
  ///
  /// This example shows best practices for implementing [debugFillProperties]
3211
  /// illustrating use of all common [DiagnosticsProperty] subclasses and all
3212 3213 3214
  /// common [DiagnosticsProperty] parameters.
  ///
  /// ```dart
3215
  /// class ExampleObject extends ExampleSuperclass {
3216 3217 3218
  ///
  ///   // ...various members and properties...
  ///
3219
  ///   @override
3220
  ///   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
3221
  ///     // Always add properties from the base class first.
3222
  ///     super.debugFillProperties(properties);
3223 3224 3225
  ///
  ///     // Omit the property name 'message' when displaying this String property
  ///     // as it would just add visual noise.
3226
  ///     properties.add(StringProperty('message', message, showName: false));
3227
  ///
3228
  ///     properties.add(DoubleProperty('stepWidth', stepWidth));
3229 3230
  ///
  ///     // A scale of 1.0 does nothing so should be hidden.
3231
  ///     properties.add(DoubleProperty('scale', scale, defaultValue: 1.0));
3232 3233 3234
  ///
  ///     // If the hitTestExtent matches the paintExtent, it is just set to its
  ///     // default value so is not relevant.
3235
  ///     properties.add(DoubleProperty('hitTestExtent', hitTestExtent, defaultValue: paintExtent));
3236
  ///
3237
  ///     // maxWidth of double.infinity indicates the width is unconstrained and
3238
  ///     // so maxWidth has no impact.,
3239
  ///     properties.add(DoubleProperty('maxWidth', maxWidth, defaultValue: double.infinity));
3240 3241 3242 3243
  ///
  ///     // 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.
3244
  ///     properties.add(PercentProperty(
3245 3246 3247 3248 3249 3250 3251
  ///       'progress',
  ///       progress,
  ///       showName: false,
  ///       ifNull: '<indeterminate>',
  ///     ));
  ///
  ///     // Most text fields have maxLines set to 1.
3252
  ///     properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
3253 3254 3255
  ///
  ///     // Specify the unit as otherwise it would be unclear that time is in
  ///     // milliseconds.
3256
  ///     properties.add(IntProperty('duration', duration.inMilliseconds, unit: 'ms'));
3257 3258 3259 3260
  ///
  ///     // 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.
3261
  ///     properties.add(DoubleProperty(
3262 3263 3264 3265 3266 3267 3268
  ///       'device pixel ratio',
  ///       ui.window.devicePixelRatio,
  ///       tooltip: 'physical pixels per logical pixel',
  ///     ));
  ///
  ///     // Displaying the depth value would be distracting. Instead only display
  ///     // if the depth value is missing.
3269
  ///     properties.add(ObjectFlagProperty<int>('depth', depth, ifNull: 'no depth'));
3270 3271
  ///
  ///     // bool flag that is only shown when the value is true.
3272
  ///     properties.add(FlagProperty('using primary controller', value: primary));
3273
  ///
3274
  ///     properties.add(FlagProperty(
3275 3276 3277 3278 3279 3280 3281
  ///       'isCurrent',
  ///       value: isCurrent,
  ///       ifTrue: 'active',
  ///       ifFalse: 'inactive',
  ///       showName: false,
  ///     ));
  ///
3282
  ///     properties.add(DiagnosticsProperty<bool>('keepAlive', keepAlive));
3283 3284 3285 3286
  ///
  ///     // 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.
3287
  ///     properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
3288
  ///
3289 3290
  ///     properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
  ///     properties.add(EnumProperty<ImageRepeat>('repeat', repeat, defaultValue: ImageRepeat.noRepeat));
3291 3292
  ///
  ///     // Warn users when the widget is missing but do not show the value.
3293
  ///     properties.add(ObjectFlagProperty<Widget>('widget', widget, ifNull: 'no widget'));
3294
  ///
3295
  ///     properties.add(IterableProperty<BoxShadow>(
3296 3297 3298 3299 3300 3301 3302
  ///       'boxShadow',
  ///       boxShadow,
  ///       defaultValue: null,
  ///       style: style,
  ///     ));
  ///
  ///     // Getting the value of size throws an exception unless hasSize is true.
3303
  ///     properties.add(DiagnosticsProperty<Size>.lazy(
3304 3305 3306 3307 3308 3309 3310 3311 3312
  ///       '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.
3313
  ///     properties.add(TransformProperty('transform', transform));
3314 3315 3316 3317 3318 3319 3320
  ///
  ///     // 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.
3321
  ///     properties.add(DiagnosticsProperty<Color>('color', color));
3322 3323 3324
  ///
  ///     // Use a custom description to generate a more terse summary than the
  ///     // `toString` method on the map class.
3325
  ///     properties.add(DiagnosticsProperty<Map<Listenable, VoidCallback>>(
3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336
  ///       'handles',
  ///       handles,
  ///       description: handles != null ?
  ///       '${handles.length} active client${ handles.length == 1 ? "" : "s" }' :
  ///       null,
  ///       ifNull: 'no notifications ever received',
  ///       showName: false,
  ///     ));
  ///   }
  /// }
  /// ```
3337
  /// {@end-tool}
3338
  ///
3339
  /// Used by [toDiagnosticsNode] and [toString].
3340 3341
  @protected
  @mustCallSuper
3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354
  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:
///
3355
///  * [DiagnosticableTreeMixin], a mixin that implements this class.
3356
///  * [Diagnosticable], which should be used instead of this class to
3357
///    provide diagnostics for objects without children.
3358
abstract class DiagnosticableTree with Diagnosticable {
3359 3360 3361 3362 3363 3364 3365 3366 3367
  /// 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.
  ///
3368 3369
  /// `joiner` specifies the string which is place between each part obtained
  /// from [debugFillProperties]. Passing a string such as `'\n '` will result
3370
  /// in a multiline string that indents the properties of the object below its
3371 3372 3373 3374
  /// name (as per [toString]).
  ///
  /// `minLevel` specifies the minimum [DiagnosticLevel] for properties included
  /// in the output.
3375 3376 3377 3378 3379
  ///
  /// See also:
  ///
  ///  * [toString], for a brief description of the object.
  ///  * [toStringDeep], for a description of the subtree rooted at this object.
3380
  String toStringShallow({
3381 3382
    String joiner = ', ',
    DiagnosticLevel minLevel = DiagnosticLevel.debug,
3383
  }) {
3384
    String? shallowString;
3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397
    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;
    }());
3398
    return shallowString ?? toString();
3399 3400 3401 3402
  }

  /// Returns a string representation of this node and its descendants.
  ///
3403 3404 3405 3406 3407
  /// `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.
  ///
3408 3409 3410 3411 3412
  /// `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.
3413 3414 3415 3416 3417 3418
  ///
  /// 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.
3419
  String toStringDeep({
3420
    String prefixLineOne = '',
3421
    String? prefixOtherLines,
3422
    DiagnosticLevel minLevel = DiagnosticLevel.debug,
3423 3424
  }) {
    return toDiagnosticsNode().toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines, minLevel: minLevel);
3425 3426 3427 3428 3429 3430
  }

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

  @override
3431
  DiagnosticsNode toDiagnosticsNode({ String? name, DiagnosticsTreeStyle? style }) {
3432
    return DiagnosticableTreeNode(
3433 3434 3435 3436 3437
      name: name,
      value: this,
      style: style,
    );
  }
3438

3439 3440 3441
  /// Returns a list of [DiagnosticsNode] objects describing this node's
  /// children.
  ///
3442
  /// Children that are offstage should be added with `style` set to
3443 3444
  /// [DiagnosticsTreeStyle.offstage] to indicate that they are offstage.
  ///
3445 3446 3447 3448 3449
  /// The list must not contain any null entries. If there are explicit null
  /// children to report, consider [new DiagnosticsNode.message] or
  /// [DiagnosticsProperty<Object>] as possible [DiagnosticsNode] objects to
  /// provide.
  ///
3450 3451
  /// Used by [toStringDeep], [toDiagnosticsNode] and [toStringShallow].
  ///
3452 3453 3454
  /// See also:
  ///
  ///  * [RenderTable.debugDescribeChildren], which provides high quality custom
3455
  ///    descriptions for its child nodes.
3456
  @protected
3457 3458 3459
  List<DiagnosticsNode> debugDescribeChildren() => const <DiagnosticsNode>[];
}

3460
/// A mixin that helps dump string and [DiagnosticsNode] representations of trees.
3461
///
3462 3463
/// This mixin is identical to class [DiagnosticableTree].
mixin DiagnosticableTreeMixin implements DiagnosticableTree {
3464
  @override
3465
  String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
3466
    return toDiagnosticsNode(style: DiagnosticsTreeStyle.singleLine).toString(minLevel: minLevel);
3467
  }
3468 3469

  @override
3470
  String toStringShallow({
3471 3472
    String joiner = ', ',
    DiagnosticLevel minLevel = DiagnosticLevel.debug,
3473
  }) {
3474
    String? shallowString;
3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487
    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;
    }());
3488
    return shallowString ?? toString();
3489 3490 3491
  }

  @override
3492
  String toStringDeep({
3493
    String prefixLineOne = '',
3494
    String? prefixOtherLines,
3495
    DiagnosticLevel minLevel = DiagnosticLevel.debug,
3496 3497
  }) {
    return toDiagnosticsNode().toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines, minLevel: minLevel);
3498 3499 3500 3501 3502 3503
  }

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

  @override
3504
  DiagnosticsNode toDiagnosticsNode({ String? name, DiagnosticsTreeStyle? style }) {
3505
    return DiagnosticableTreeNode(
3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516
      name: name,
      value: this,
      style: style,
    );
  }

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

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) { }
3517
}
3518 3519 3520 3521 3522 3523 3524 3525 3526 3527


/// [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({
3528
    String? name,
3529 3530 3531
    DiagnosticsTreeStyle style = DiagnosticsTreeStyle.whitespace,
    bool showName = true,
    bool showSeparator = true,
3532
    String? linePrefix,
3533
    this.value,
3534
    String? description,
3535 3536 3537 3538 3539 3540 3541 3542 3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554
    this.level = DiagnosticLevel.info,
    this.allowTruncate = false,
    List<DiagnosticsNode> children = const<DiagnosticsNode>[],
    List<DiagnosticsNode> properties = const <DiagnosticsNode>[],
  }) : _description = description,
       _children = children,
       _properties = properties,
    super(
    name: name,
    style: style,
    showName: showName && name != null,
    showSeparator: showSeparator,
    linePrefix: linePrefix,
  );

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

  @override
  final DiagnosticLevel level;
3555
  final String? _description;
3556
  @override
3557
  final Object? value;
3558 3559 3560 3561 3562 3563 3564 3565 3566 3567 3568

  @override
  final bool allowTruncate;

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

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

  @override
3569
  String? toDescription({TextTreeConfiguration? parentConfiguration}) => _description;
3570
}
3571 3572 3573 3574 3575 3576 3577 3578 3579 3580 3581 3582 3583 3584 3585 3586 3587 3588 3589 3590 3591 3592 3593

/// 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.
3594
  Map<String, Object?> additionalNodeProperties(DiagnosticsNode node);
3595 3596 3597 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 3625 3626 3627 3628 3629 3630 3631

  /// 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.
3632
  List<DiagnosticsNode> truncateNodesList(List<DiagnosticsNode> nodes, DiagnosticsNode? owner);
3633 3634 3635 3636 3637 3638 3639 3640 3641 3642 3643 3644 3645 3646 3647 3648 3649 3650 3651 3652 3653 3654 3655 3656 3657 3658 3659 3660 3661 3662 3663 3664 3665 3666

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

3667
  /// Whether properties that have a [Diagnosticable] as value should be
3668 3669 3670 3671 3672 3673 3674 3675 3676 3677 3678 3679 3680 3681 3682 3683 3684 3685
  /// 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
3686 3687
  Map<String, Object?> additionalNodeProperties(DiagnosticsNode node) {
    return const <String, Object?>{};
3688 3689 3690 3691 3692 3693 3694 3695 3696 3697 3698 3699 3700 3701 3702 3703 3704 3705 3706 3707 3708 3709 3710 3711 3712 3713 3714
  }

  @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
3715
  List<DiagnosticsNode> truncateNodesList(List<DiagnosticsNode> nodes, DiagnosticsNode? owner) {
3716 3717 3718 3719
    return nodes;
  }

  @override
3720
  DiagnosticsSerializationDelegate copyWith({int? subtreeDepth, bool? includeProperties}) {
3721 3722 3723 3724 3725 3726
    return _DefaultDiagnosticsSerializationDelegate(
      subtreeDepth: subtreeDepth ?? this.subtreeDepth,
      includeProperties: includeProperties ?? this.includeProperties,
    );
  }
}