1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:meta/meta.dart';
6
7import 'basic_types.dart';
8import 'constants.dart';
9import 'diagnostics.dart';
10import 'print.dart';
11import 'stack_frame.dart';
12
13export 'basic_types.dart' show IterableFilter;
14export 'diagnostics.dart' show DiagnosticLevel, DiagnosticPropertiesBuilder, DiagnosticsNode, DiagnosticsTreeStyle;
15export 'stack_frame.dart' show StackFrame;
16
17// Examples can assume:
18// late String runtimeType;
19// late bool draconisAlive;
20// late bool draconisAmulet;
21// late Diagnosticable draconis;
22// void methodThatMayThrow() { }
23// class Trace implements StackTrace { late StackTrace vmTrace; }
24// class Chain implements StackTrace { Trace toTrace() => Trace(); }
25
26/// Signature for [FlutterError.onError] handler.
27typedef FlutterExceptionHandler = void Function(FlutterErrorDetails details);
28
29/// Signature for [DiagnosticPropertiesBuilder] transformer.
30typedef DiagnosticPropertiesTransformer = Iterable<DiagnosticsNode> Function(Iterable<DiagnosticsNode> properties);
31
32/// Signature for [FlutterErrorDetails.informationCollector] callback
33/// and other callbacks that collect information describing an error.
34typedef InformationCollector = Iterable<DiagnosticsNode> Function();
35
36/// Signature for a function that demangles [StackTrace] objects into a format
37/// that can be parsed by [StackFrame].
38///
39/// See also:
40///
41/// * [FlutterError.demangleStackTrace], which shows an example implementation.
42typedef StackTraceDemangler = StackTrace Function(StackTrace details);
43
44/// Partial information from a stack frame for stack filtering purposes.
45///
46/// See also:
47///
48/// * [RepetitiveStackFrameFilter], which uses this class to compare against [StackFrame]s.
49@immutable
50class PartialStackFrame {
51 /// Creates a new [PartialStackFrame] instance.
52 const PartialStackFrame({
53 required this.package,
54 required this.className,
55 required this.method,
56 });
57
58 /// An `<asynchronous suspension>` line in a stack trace.
59 static const PartialStackFrame asynchronousSuspension = PartialStackFrame(
60 package: '',
61 className: '',
62 method: 'asynchronous suspension',
63 );
64
65 /// The package to match, e.g. `package:flutter/src/foundation/assertions.dart`,
66 /// or `dart:ui/window.dart`.
67 final Pattern package;
68
69 /// The class name for the method.
70 ///
71 /// On web, this is ignored, since class names are not available.
72 ///
73 /// On all platforms, top level methods should use the empty string.
74 final String className;
75
76 /// The method name for this frame line.
77 ///
78 /// On web, private methods are wrapped with `[]`.
79 final String method;
80
81 /// Tests whether the [StackFrame] matches the information in this
82 /// [PartialStackFrame].
83 bool matches(StackFrame stackFrame) {
84 final String stackFramePackage = '${stackFrame.packageScheme}:${stackFrame.package}/${stackFrame.packagePath}';
85 // Ideally this wouldn't be necessary.
86 // TODO(dnfield): https://github.com/dart-lang/sdk/issues/40117
87 if (kIsWeb) {
88 return package.allMatches(stackFramePackage).isNotEmpty
89 && stackFrame.method == (method.startsWith('_') ? '[$method]' : method);
90 }
91 return package.allMatches(stackFramePackage).isNotEmpty
92 && stackFrame.method == method
93 && stackFrame.className == className;
94 }
95}
96
97/// A class that filters stack frames for additional filtering on
98/// [FlutterError.defaultStackFilter].
99abstract class StackFilter {
100 /// Abstract const constructor. This constructor enables subclasses to provide
101 /// const constructors so that they can be used in const expressions.
102 const StackFilter();
103
104 /// Filters the list of [StackFrame]s by updating corresponding indices in
105 /// `reasons`.
106 ///
107 /// To elide a frame or number of frames, set the string.
108 void filter(List<StackFrame> stackFrames, List<String?> reasons);
109}
110
111
112/// A [StackFilter] that filters based on repeating lists of
113/// [PartialStackFrame]s.
114///
115/// See also:
116///
117/// * [FlutterError.addDefaultStackFilter], a method to register additional
118/// stack filters for [FlutterError.defaultStackFilter].
119/// * [StackFrame], a class that can help with parsing stack frames.
120/// * [PartialStackFrame], a class that helps match partial method information
121/// to a stack frame.
122class RepetitiveStackFrameFilter extends StackFilter {
123 /// Creates a new RepetitiveStackFrameFilter. All parameters are required and must not be
124 /// null.
125 const RepetitiveStackFrameFilter({
126 required this.frames,
127 required this.replacement,
128 });
129
130 /// The shape of this repetitive stack pattern.
131 final List<PartialStackFrame> frames;
132
133 /// The number of frames in this pattern.
134 int get numFrames => frames.length;
135
136 /// The string to replace the frames with.
137 ///
138 /// If the same replacement string is used multiple times in a row, the
139 /// [FlutterError.defaultStackFilter] will insert a repeat count after this
140 /// line rather than repeating it.
141 final String replacement;
142
143 List<String> get _replacements => List<String>.filled(numFrames, replacement);
144
145 @override
146 void filter(List<StackFrame> stackFrames, List<String?> reasons) {
147 for (int index = 0; index < stackFrames.length - numFrames; index += 1) {
148 if (_matchesFrames(stackFrames.skip(index).take(numFrames).toList())) {
149 reasons.setRange(index, index + numFrames, _replacements);
150 index += numFrames - 1;
151 }
152 }
153 }
154
155 bool _matchesFrames(List<StackFrame> stackFrames) {
156 if (stackFrames.length < numFrames) {
157 return false;
158 }
159 for (int index = 0; index < stackFrames.length; index++) {
160 if (!frames[index].matches(stackFrames[index])) {
161 return false;
162 }
163 }
164 return true;
165 }
166}
167
168abstract class _ErrorDiagnostic extends DiagnosticsProperty<List<Object>> {
169 /// This constructor provides a reliable hook for a kernel transformer to find
170 /// error messages that need to be rewritten to include object references for
171 /// interactive display of errors.
172 _ErrorDiagnostic(
173 String message, {
174 DiagnosticsTreeStyle style = DiagnosticsTreeStyle.flat,
175 DiagnosticLevel level = DiagnosticLevel.info,
176 }) : super(
177 null,
178 <Object>[message],
179 showName: false,
180 showSeparator: false,
181 defaultValue: null,
182 style: style,
183 level: level,
184 );
185
186 /// In debug builds, a kernel transformer rewrites calls to the default
187 /// constructors for [ErrorSummary], [ErrorDescription], and [ErrorHint] to use
188 /// this constructor.
189 //
190 // ```dart
191 // _ErrorDiagnostic('Element $element must be $color')
192 // ```
193 // Desugars to:
194 // ```dart
195 // _ErrorDiagnostic.fromParts(['Element ', element, ' must be ', color])
196 // ```
197 //
198 // Slightly more complex case:
199 // ```dart
200 // _ErrorDiagnostic('Element ${element.runtimeType} must be $color')
201 // ```
202 // Desugars to:
203 //```dart
204 // _ErrorDiagnostic.fromParts([
205 // 'Element ',
206 // DiagnosticsProperty(null, element, description: element.runtimeType?.toString()),
207 // ' must be ',
208 // color,
209 // ])
210 // ```
211 _ErrorDiagnostic._fromParts(
212 List<Object> messageParts, {
213 DiagnosticsTreeStyle style = DiagnosticsTreeStyle.flat,
214 DiagnosticLevel level = DiagnosticLevel.info,
215 }) : super(
216 null,
217 messageParts,
218 showName: false,
219 showSeparator: false,
220 defaultValue: null,
221 style: style,
222 level: level,
223 );
224
225 @override
226 String toString({
227 TextTreeConfiguration? parentConfiguration,
228 DiagnosticLevel minLevel = DiagnosticLevel.info,
229 }) {
230 return valueToString(parentConfiguration: parentConfiguration);
231 }
232
233 @override
234 List<Object> get value => super.value!;
235
236 @override
237 String valueToString({ TextTreeConfiguration? parentConfiguration }) {
238 return value.join();
239 }
240}
241
242/// An explanation of the problem and its cause, any information that may help
243/// track down the problem, background information, etc.
244///
245/// Use [ErrorDescription] for any part of an error message where neither
246/// [ErrorSummary] or [ErrorHint] is appropriate.
247///
248/// In debug builds, values interpolated into the `message` are
249/// expanded and placed into [value], which is of type [List<Object>].
250/// This allows IDEs to examine values interpolated into error messages.
251///
252/// See also:
253///
254/// * [ErrorSummary], which provides a short (one line) description of the
255/// problem that was detected.
256/// * [ErrorHint], which provides specific, non-obvious advice that may be
257/// applicable.
258/// * [ErrorSpacer], which renders as a blank line.
259/// * [FlutterError], which is the most common place to use an
260/// [ErrorDescription].
261class ErrorDescription extends _ErrorDiagnostic {
262 /// A lint enforces that this constructor can only be called with a string
263 /// literal to match the limitations of the Dart Kernel transformer that
264 /// optionally extracts out objects referenced using string interpolation in
265 /// the message passed in.
266 ///
267 /// The message will display with the same text regardless of whether the
268 /// kernel transformer is used. The kernel transformer is required so that
269 /// debugging tools can provide interactive displays of objects described by
270 /// the error.
271 ErrorDescription(super.message) : super(level: DiagnosticLevel.info);
272
273 /// Calls to the default constructor may be rewritten to use this constructor
274 /// in debug mode using a kernel transformer.
275 // ignore: unused_element
276 ErrorDescription._fromParts(super.messageParts) : super._fromParts(level: DiagnosticLevel.info);
277}
278
279/// A short (one line) description of the problem that was detected.
280///
281/// Error summaries from the same source location should have little variance,
282/// so that they can be recognized as related. For example, they shouldn't
283/// include hash codes.
284///
285/// A [FlutterError] must start with an [ErrorSummary] and may not contain
286/// multiple summaries.
287///
288/// In debug builds, values interpolated into the `message` are
289/// expanded and placed into [value], which is of type [List<Object>].
290/// This allows IDEs to examine values interpolated into error messages.
291///
292/// See also:
293///
294/// * [ErrorDescription], which provides an explanation of the problem and its
295/// cause, any information that may help track down the problem, background
296/// information, etc.
297/// * [ErrorHint], which provides specific, non-obvious advice that may be
298/// applicable.
299/// * [FlutterError], which is the most common place to use an [ErrorSummary].
300class ErrorSummary extends _ErrorDiagnostic {
301 /// A lint enforces that this constructor can only be called with a string
302 /// literal to match the limitations of the Dart Kernel transformer that
303 /// optionally extracts out objects referenced using string interpolation in
304 /// the message passed in.
305 ///
306 /// The message will display with the same text regardless of whether the
307 /// kernel transformer is used. The kernel transformer is required so that
308 /// debugging tools can provide interactive displays of objects described by
309 /// the error.
310 ErrorSummary(super.message) : super(level: DiagnosticLevel.summary);
311
312 /// Calls to the default constructor may be rewritten to use this constructor
313 /// in debug mode using a kernel transformer.
314 // ignore: unused_element
315 ErrorSummary._fromParts(super.messageParts) : super._fromParts(level: DiagnosticLevel.summary);
316}
317
318/// An [ErrorHint] provides specific, non-obvious advice that may be applicable.
319///
320/// If your message provides obvious advice that is always applicable, it is an
321/// [ErrorDescription] not a hint.
322///
323/// In debug builds, values interpolated into the `message` are
324/// expanded and placed into [value], which is of type [List<Object>].
325/// This allows IDEs to examine values interpolated into error messages.
326///
327/// See also:
328///
329/// * [ErrorSummary], which provides a short (one line) description of the
330/// problem that was detected.
331/// * [ErrorDescription], which provides an explanation of the problem and its
332/// cause, any information that may help track down the problem, background
333/// information, etc.
334/// * [ErrorSpacer], which renders as a blank line.
335/// * [FlutterError], which is the most common place to use an [ErrorHint].
336class ErrorHint extends _ErrorDiagnostic {
337 /// A lint enforces that this constructor can only be called with a string
338 /// literal to match the limitations of the Dart Kernel transformer that
339 /// optionally extracts out objects referenced using string interpolation in
340 /// the message passed in.
341 ///
342 /// The message will display with the same text regardless of whether the
343 /// kernel transformer is used. The kernel transformer is required so that
344 /// debugging tools can provide interactive displays of objects described by
345 /// the error.
346 ErrorHint(super.message) : super(level:DiagnosticLevel.hint);
347
348 /// Calls to the default constructor may be rewritten to use this constructor
349 /// in debug mode using a kernel transformer.
350 // ignore: unused_element
351 ErrorHint._fromParts(super.messageParts) : super._fromParts(level:DiagnosticLevel.hint);
352}
353
354/// An [ErrorSpacer] creates an empty [DiagnosticsNode], that can be used to
355/// tune the spacing between other [DiagnosticsNode] objects.
356class ErrorSpacer extends DiagnosticsProperty<void> {
357 /// Creates an empty space to insert into a list of [DiagnosticsNode] objects
358 /// typically within a [FlutterError] object.
359 ErrorSpacer() : super(
360 '',
361 null,
362 description: '',
363 showName: false,
364 );
365}
366
367/// Class for information provided to [FlutterExceptionHandler] callbacks.
368///
369/// {@tool snippet}
370/// This is an example of using [FlutterErrorDetails] when calling
371/// [FlutterError.reportError].
372///
373/// ```dart
374/// void main() {
375/// try {
376/// // Try to do something!
377/// } catch (error) {
378/// // Catch & report error.
379/// FlutterError.reportError(FlutterErrorDetails(
380/// exception: error,
381/// library: 'Flutter test framework',
382/// context: ErrorSummary('while running async test code'),
383/// ));
384/// }
385/// }
386/// ```
387/// {@end-tool}
388///
389/// See also:
390///
391/// * [FlutterError.onError], which is called whenever the Flutter framework
392/// catches an error.
393class FlutterErrorDetails with Diagnosticable {
394 /// Creates a [FlutterErrorDetails] object with the given arguments setting
395 /// the object's properties.
396 ///
397 /// The framework calls this constructor when catching an exception that will
398 /// subsequently be reported using [FlutterError.onError].
399 const FlutterErrorDetails({
400 required this.exception,
401 this.stack,
402 this.library = 'Flutter framework',
403 this.context,
404 this.stackFilter,
405 this.informationCollector,
406 this.silent = false,
407 });
408
409 /// Creates a copy of the error details but with the given fields replaced
410 /// with new values.
411 FlutterErrorDetails copyWith({
412 DiagnosticsNode? context,
413 Object? exception,
414 InformationCollector? informationCollector,
415 String? library,
416 bool? silent,
417 StackTrace? stack,
418 IterableFilter<String>? stackFilter,
419 }) {
420 return FlutterErrorDetails(
421 context: context ?? this.context,
422 exception: exception ?? this.exception,
423 informationCollector: informationCollector ?? this.informationCollector,
424 library: library ?? this.library,
425 silent: silent ?? this.silent,
426 stack: stack ?? this.stack,
427 stackFilter: stackFilter ?? this.stackFilter,
428 );
429 }
430
431 /// Transformers to transform [DiagnosticsNode] in [DiagnosticPropertiesBuilder]
432 /// into a more descriptive form.
433 ///
434 /// There are layers that attach certain [DiagnosticsNode] into
435 /// [FlutterErrorDetails] that require knowledge from other layers to parse.
436 /// To correctly interpret those [DiagnosticsNode], register transformers in
437 /// the layers that possess the knowledge.
438 ///
439 /// See also:
440 ///
441 /// * [WidgetsBinding.initInstances], which registers its transformer.
442 static final List<DiagnosticPropertiesTransformer> propertiesTransformers =
443 <DiagnosticPropertiesTransformer>[];
444
445 /// The exception. Often this will be an [AssertionError], maybe specifically
446 /// a [FlutterError]. However, this could be any value at all.
447 final Object exception;
448
449 /// The stack trace from where the [exception] was thrown (as opposed to where
450 /// it was caught).
451 ///
452 /// StackTrace objects are opaque except for their [toString] function.
453 ///
454 /// If this field is not null, then the [stackFilter] callback, if any, will
455 /// be called with the result of calling [toString] on this object and
456 /// splitting that result on line breaks. If there's no [stackFilter]
457 /// callback, then [FlutterError.defaultStackFilter] is used instead. That
458 /// function expects the stack to be in the format used by
459 /// [StackTrace.toString].
460 final StackTrace? stack;
461
462 /// A human-readable brief name describing the library that caught the error
463 /// message. This is used by the default error handler in the header dumped to
464 /// the console.
465 final String? library;
466
467 /// A [DiagnosticsNode] that provides a human-readable description of where
468 /// the error was caught (as opposed to where it was thrown).
469 ///
470 /// The node, e.g. an [ErrorDescription], should be in a form that will make
471 /// sense in English when following the word "thrown", as in "thrown while
472 /// obtaining the image from the network" (for the context "while obtaining
473 /// the image from the network").
474 ///
475 /// {@tool snippet}
476 /// This is an example of using and [ErrorDescription] as the
477 /// [FlutterErrorDetails.context] when calling [FlutterError.reportError].
478 ///
479 /// ```dart
480 /// void maybeDoSomething() {
481 /// try {
482 /// // Try to do something!
483 /// } catch (error) {
484 /// // Catch & report error.
485 /// FlutterError.reportError(FlutterErrorDetails(
486 /// exception: error,
487 /// library: 'Flutter test framework',
488 /// context: ErrorDescription('while dispatching notifications for $runtimeType'),
489 /// ));
490 /// }
491 /// }
492 /// ```
493 /// {@end-tool}
494 ///
495 /// See also:
496 ///
497 /// * [ErrorDescription], which provides an explanation of the problem and
498 /// its cause, any information that may help track down the problem,
499 /// background information, etc.
500 /// * [ErrorSummary], which provides a short (one line) description of the
501 /// problem that was detected.
502 /// * [ErrorHint], which provides specific, non-obvious advice that may be
503 /// applicable.
504 /// * [FlutterError], which is the most common place to use
505 /// [FlutterErrorDetails].
506 final DiagnosticsNode? context;
507
508 /// A callback which filters the [stack] trace. Receives an iterable of
509 /// strings representing the frames encoded in the way that
510 /// [StackTrace.toString()] provides. Should return an iterable of lines to
511 /// output for the stack.
512 ///
513 /// If this is not provided, then [FlutterError.dumpErrorToConsole] will use
514 /// [FlutterError.defaultStackFilter] instead.
515 ///
516 /// If the [FlutterError.defaultStackFilter] behavior is desired, then the
517 /// callback should manually call that function. That function expects the
518 /// incoming list to be in the [StackTrace.toString()] format. The output of
519 /// that function, however, does not always follow this format.
520 ///
521 /// This won't be called if [stack] is null.
522 final IterableFilter<String>? stackFilter;
523
524 /// A callback which will provide information that could help with debugging
525 /// the problem.
526 ///
527 /// Information collector callbacks can be expensive, so the generated
528 /// information should be cached by the caller, rather than the callback being
529 /// called multiple times.
530 ///
531 /// The callback is expected to return an iterable of [DiagnosticsNode] objects,
532 /// typically implemented using `sync*` and `yield`.
533 ///
534 /// {@tool snippet}
535 /// In this example, the information collector returns two pieces of information,
536 /// one broadly-applicable statement regarding how the error happened, and one
537 /// giving a specific piece of information that may be useful in some cases but
538 /// may also be irrelevant most of the time (an argument to the method).
539 ///
540 /// ```dart
541 /// void climbElevator(int pid) {
542 /// try {
543 /// // ...
544 /// } catch (error, stack) {
545 /// FlutterError.reportError(FlutterErrorDetails(
546 /// exception: error,
547 /// stack: stack,
548 /// informationCollector: () => <DiagnosticsNode>[
549 /// ErrorDescription('This happened while climbing the space elevator.'),
550 /// ErrorHint('The process ID is: $pid'),
551 /// ],
552 /// ));
553 /// }
554 /// }
555 /// ```
556 /// {@end-tool}
557 ///
558 /// The following classes may be of particular use:
559 ///
560 /// * [ErrorDescription], for information that is broadly applicable to the
561 /// situation being described.
562 /// * [ErrorHint], for specific information that may not always be applicable
563 /// but can be helpful in certain situations.
564 /// * [DiagnosticsStackTrace], for reporting stack traces.
565 /// * [ErrorSpacer], for adding spaces (a blank line) between other items.
566 ///
567 /// For objects that implement [Diagnosticable] one may consider providing
568 /// additional information by yielding the output of the object's
569 /// [Diagnosticable.toDiagnosticsNode] method.
570 final InformationCollector? informationCollector;
571
572 /// Whether this error should be ignored by the default error reporting
573 /// behavior in release mode.
574 ///
575 /// If this is false, the default, then the default error handler will always
576 /// dump this error to the console.
577 ///
578 /// If this is true, then the default error handler would only dump this error
579 /// to the console in debug mode. In release mode, the error is ignored.
580 ///
581 /// This is used by certain exception handlers that catch errors that could be
582 /// triggered by environmental conditions (as opposed to logic errors). For
583 /// example, the HTTP library sets this flag so as to not report every 404
584 /// error to the console on end-user devices, while still allowing a custom
585 /// error handler to see the errors even in release builds.
586 final bool silent;
587
588 /// Converts the [exception] to a string.
589 ///
590 /// This applies some additional logic to make [AssertionError] exceptions
591 /// prettier, to handle exceptions that stringify to empty strings, to handle
592 /// objects that don't inherit from [Exception] or [Error], and so forth.
593 String exceptionAsString() {
594 String? longMessage;
595 if (exception is AssertionError) {
596 // Regular _AssertionErrors thrown by assert() put the message last, after
597 // some code snippets. This leads to ugly messages. To avoid this, we move
598 // the assertion message up to before the code snippets, separated by a
599 // newline, if we recognize that format is being used.
600 final Object? message = (exception as AssertionError).message;
601 final String fullMessage = exception.toString();
602 if (message is String && message != fullMessage) {
603 if (fullMessage.length > message.length) {
604 final int position = fullMessage.lastIndexOf(message);
605 if (position == fullMessage.length - message.length &&
606 position > 2 &&
607 fullMessage.substring(position - 2, position) == ': ') {
608 // Add a linebreak so that the filename at the start of the
609 // assertion message is always on its own line.
610 String body = fullMessage.substring(0, position - 2);
611 final int splitPoint = body.indexOf(' Failed assertion:');
612 if (splitPoint >= 0) {
613 body = '${body.substring(0, splitPoint)}\n${body.substring(splitPoint + 1)}';
614 }
615 longMessage = '${message.trimRight()}\n$body';
616 }
617 }
618 }
619 longMessage ??= fullMessage;
620 } else if (exception is String) {
621 longMessage = exception as String;
622 } else if (exception is Error || exception is Exception) {
623 longMessage = exception.toString();
624 } else {
625 longMessage = ' $exception';
626 }
627 longMessage = longMessage.trimRight();
628 if (longMessage.isEmpty) {
629 longMessage = ' <no message available>';
630 }
631 return longMessage;
632 }
633
634 Diagnosticable? _exceptionToDiagnosticable() {
635 final Object exception = this.exception;
636 if (exception is FlutterError) {
637 return exception;
638 }
639 if (exception is AssertionError && exception.message is FlutterError) {
640 return exception.message! as FlutterError;
641 }
642 return null;
643 }
644
645 /// Returns a short (one line) description of the problem that was detected.
646 ///
647 /// If the exception contains an [ErrorSummary] that summary is used,
648 /// otherwise the summary is inferred from the string representation of the
649 /// exception.
650 ///
651 /// In release mode, this always returns a [DiagnosticsNode.message] with a
652 /// formatted version of the exception.
653 DiagnosticsNode get summary {
654 String formatException() => exceptionAsString().split('\n')[0].trimLeft();
655 if (kReleaseMode) {
656 return DiagnosticsNode.message(formatException());
657 }
658 final Diagnosticable? diagnosticable = _exceptionToDiagnosticable();
659 DiagnosticsNode? summary;
660 if (diagnosticable != null) {
661 final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
662 debugFillProperties(builder);
663 summary = builder.properties.cast<DiagnosticsNode?>().firstWhere((DiagnosticsNode? node) => node!.level == DiagnosticLevel.summary, orElse: () => null);
664 }
665 return summary ?? ErrorSummary(formatException());
666 }
667
668 @override
669 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
670 super.debugFillProperties(properties);
671 final DiagnosticsNode verb = ErrorDescription('thrown${ context != null ? ErrorDescription(" $context") : ""}');
672 final Diagnosticable? diagnosticable = _exceptionToDiagnosticable();
673 if (exception is num) {
674 properties.add(ErrorDescription('The number $exception was $verb.'));
675 } else {
676 final DiagnosticsNode errorName;
677 if (exception is AssertionError) {
678 errorName = ErrorDescription('assertion');
679 } else if (exception is String) {
680 errorName = ErrorDescription('message');
681 } else if (exception is Error || exception is Exception) {
682 errorName = ErrorDescription('${exception.runtimeType}');
683 } else {
684 errorName = ErrorDescription('${exception.runtimeType} object');
685 }
686 properties.add(ErrorDescription('The following $errorName was $verb:'));
687 if (diagnosticable != null) {
688 diagnosticable.debugFillProperties(properties);
689 } else {
690 // Many exception classes put their type at the head of their message.
691 // This is redundant with the way we display exceptions, so attempt to
692 // strip out that header when we see it.
693 final String prefix = '${exception.runtimeType}: ';
694 String message = exceptionAsString();
695 if (message.startsWith(prefix)) {
696 message = message.substring(prefix.length);
697 }
698 properties.add(ErrorSummary(message));
699 }
700 }
701
702 if (stack != null) {
703 if (exception is AssertionError && diagnosticable == null) {
704 // After popping off any dart: stack frames, are there at least two more
705 // stack frames coming from package flutter?
706 //
707 // If not: Error is in user code (user violated assertion in framework).
708 // If so: Error is in Framework. We either need an assertion higher up
709 // in the stack, or we've violated our own assertions.
710 final List<StackFrame> stackFrames = StackFrame.fromStackTrace(FlutterError.demangleStackTrace(stack!))
711 .skipWhile((StackFrame frame) => frame.packageScheme == 'dart')
712 .toList();
713 final bool ourFault = stackFrames.length >= 2
714 && stackFrames[0].package == 'flutter'
715 && stackFrames[1].package == 'flutter';
716 if (ourFault) {
717 properties.add(ErrorSpacer());
718 properties.add(ErrorHint(
719 'Either the assertion indicates an error in the framework itself, or we should '
720 'provide substantially more information in this error message to help you determine '
721 'and fix the underlying cause.\n'
722 'In either case, please report this assertion by filing a bug on GitHub:\n'
723 ' https://github.com/flutter/flutter/issues/new?template=2_bug.yml',
724 ));
725 }
726 }
727 properties.add(ErrorSpacer());
728 properties.add(DiagnosticsStackTrace('When the exception was thrown, this was the stack', stack, stackFilter: stackFilter));
729 }
730 if (informationCollector != null) {
731 properties.add(ErrorSpacer());
732 informationCollector!().forEach(properties.add);
733 }
734 }
735
736 @override
737 String toStringShort() {
738 return library != null ? 'Exception caught by $library' : 'Exception caught';
739 }
740
741 @override
742 String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
743 return toDiagnosticsNode(style: DiagnosticsTreeStyle.error).toStringDeep(minLevel: minLevel);
744 }
745
746 @override
747 DiagnosticsNode toDiagnosticsNode({ String? name, DiagnosticsTreeStyle? style }) {
748 return _FlutterErrorDetailsNode(
749 name: name,
750 value: this,
751 style: style,
752 );
753 }
754}
755
756/// Error class used to report Flutter-specific assertion failures and
757/// contract violations.
758///
759/// See also:
760///
761/// * <https://flutter.dev/docs/testing/errors>, more information about error
762/// handling in Flutter.
763class FlutterError extends Error with DiagnosticableTreeMixin implements AssertionError {
764 /// Create an error message from a string.
765 ///
766 /// The message may have newlines in it. The first line should be a terse
767 /// description of the error, e.g. "Incorrect GlobalKey usage" or "setState()
768 /// or markNeedsBuild() called during build". Subsequent lines should contain
769 /// substantial additional information, ideally sufficient to develop a
770 /// correct solution to the problem.
771 ///
772 /// In some cases, when a [FlutterError] is reported to the user, only the first
773 /// line is included. For example, Flutter will typically only fully report
774 /// the first exception at runtime, displaying only the first line of
775 /// subsequent errors.
776 ///
777 /// All sentences in the error should be correctly punctuated (i.e.,
778 /// do end the error message with a period).
779 ///
780 /// This constructor defers to the [FlutterError.fromParts] constructor.
781 /// The first line is wrapped in an implied [ErrorSummary], and subsequent
782 /// lines are wrapped in implied [ErrorDescription]s. Consider using the
783 /// [FlutterError.fromParts] constructor to provide more detail, e.g.
784 /// using [ErrorHint]s or other [DiagnosticsNode]s.
785 factory FlutterError(String message) {
786 final List<String> lines = message.split('\n');
787 return FlutterError.fromParts(<DiagnosticsNode>[
788 ErrorSummary(lines.first),
789 ...lines.skip(1).map<DiagnosticsNode>((String line) => ErrorDescription(line)),
790 ]);
791 }
792
793 /// Create an error message from a list of [DiagnosticsNode]s.
794 ///
795 /// By convention, there should be exactly one [ErrorSummary] in the list,
796 /// and it should be the first entry.
797 ///
798 /// Other entries are typically [ErrorDescription]s (for material that is
799 /// always applicable for this error) and [ErrorHint]s (for material that may
800 /// be sometimes useful, but may not always apply). Other [DiagnosticsNode]
801 /// subclasses, such as [DiagnosticsStackTrace], may
802 /// also be used.
803 ///
804 /// When using an [ErrorSummary], [ErrorDescription]s, and [ErrorHint]s, in
805 /// debug builds, values interpolated into the `message` arguments of those
806 /// classes' constructors are expanded and placed into the
807 /// [DiagnosticsProperty.value] property of those objects (which is of type
808 /// [List<Object>]). This allows IDEs to examine values interpolated into
809 /// error messages.
810 ///
811 /// Alternatively, to include a specific [Diagnosticable] object into the
812 /// error message and have the object describe itself in detail (see
813 /// [DiagnosticsNode.toStringDeep]), consider calling
814 /// [Diagnosticable.toDiagnosticsNode] on that object and using that as one of
815 /// the values passed to this constructor.
816 ///
817 /// {@tool snippet}
818 /// In this example, an error is thrown in debug mode if certain conditions
819 /// are not met. The error message includes a description of an object that
820 /// implements the [Diagnosticable] interface, `draconis`.
821 ///
822 /// ```dart
823 /// void controlDraconis() {
824 /// assert(() {
825 /// if (!draconisAlive || !draconisAmulet) {
826 /// throw FlutterError.fromParts(<DiagnosticsNode>[
827 /// ErrorSummary('Cannot control Draconis in current state.'),
828 /// ErrorDescription('Draconis can only be controlled while alive and while the amulet is wielded.'),
829 /// if (!draconisAlive)
830 /// ErrorHint('Draconis is currently not alive.'),
831 /// if (!draconisAmulet)
832 /// ErrorHint('The Amulet of Draconis is currently not wielded.'),
833 /// draconis.toDiagnosticsNode(name: 'Draconis'),
834 /// ]);
835 /// }
836 /// return true;
837 /// }());
838 /// // ...
839 /// }
840 /// ```
841 /// {@end-tool}
842 FlutterError.fromParts(this.diagnostics) : assert(diagnostics.isNotEmpty, FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Empty FlutterError')])) {
843 assert(
844 diagnostics.first.level == DiagnosticLevel.summary,
845 FlutterError.fromParts(<DiagnosticsNode>[
846 ErrorSummary('FlutterError is missing a summary.'),
847 ErrorDescription(
848 'All FlutterError objects should start with a short (one line) '
849 'summary description of the problem that was detected.',
850 ),
851 DiagnosticsProperty<FlutterError>('Malformed', this, expandableValue: true, showSeparator: false, style: DiagnosticsTreeStyle.whitespace),
852 ErrorDescription(
853 '\nThis error should still help you solve your problem, '
854 'however please also report this malformed error in the '
855 'framework by filing a bug on GitHub:\n'
856 ' https://github.com/flutter/flutter/issues/new?template=2_bug.yml',
857 ),
858 ]),
859 );
860 assert(() {
861 final Iterable<DiagnosticsNode> summaries = diagnostics.where((DiagnosticsNode node) => node.level == DiagnosticLevel.summary);
862 if (summaries.length > 1) {
863 final List<DiagnosticsNode> message = <DiagnosticsNode>[
864 ErrorSummary('FlutterError contained multiple error summaries.'),
865 ErrorDescription(
866 'All FlutterError objects should have only a single short '
867 '(one line) summary description of the problem that was '
868 'detected.',
869 ),
870 DiagnosticsProperty<FlutterError>('Malformed', this, expandableValue: true, showSeparator: false, style: DiagnosticsTreeStyle.whitespace),
871 ErrorDescription('\nThe malformed error has ${summaries.length} summaries.'),
872 ];
873 int i = 1;
874 for (final DiagnosticsNode summary in summaries) {
875 message.add(DiagnosticsProperty<DiagnosticsNode>('Summary $i', summary, expandableValue : true));
876 i += 1;
877 }
878 message.add(ErrorDescription(
879 '\nThis error should still help you solve your problem, '
880 'however please also report this malformed error in the '
881 'framework by filing a bug on GitHub:\n'
882 ' https://github.com/flutter/flutter/issues/new?template=2_bug.yml',
883 ));
884 throw FlutterError.fromParts(message);
885 }
886 return true;
887 }());
888 }
889
890 /// The information associated with this error, in structured form.
891 ///
892 /// The first node is typically an [ErrorSummary] giving a short description
893 /// of the problem, suitable for an index of errors, a log, etc.
894 ///
895 /// Subsequent nodes should give information specific to this error. Typically
896 /// these will be [ErrorDescription]s or [ErrorHint]s, but they could be other
897 /// objects also. For instance, an error relating to a timer could include a
898 /// stack trace of when the timer was scheduled using the
899 /// [DiagnosticsStackTrace] class.
900 final List<DiagnosticsNode> diagnostics;
901
902 /// The message associated with this error.
903 ///
904 /// This is generated by serializing the [diagnostics].
905 @override
906 String get message => toString();
907
908 /// Called whenever the Flutter framework catches an error.
909 ///
910 /// The default behavior is to call [presentError].
911 ///
912 /// You can set this to your own function to override this default behavior.
913 /// For example, you could report all errors to your server. Consider calling
914 /// [presentError] from your custom error handler in order to see the logs in
915 /// the console as well.
916 ///
917 /// If the error handler throws an exception, it will not be caught by the
918 /// Flutter framework.
919 ///
920 /// Set this to null to silently catch and ignore errors. This is not
921 /// recommended.
922 ///
923 /// Do not call [onError] directly, instead, call [reportError], which
924 /// forwards to [onError] if it is not null.
925 ///
926 /// See also:
927 ///
928 /// * <https://flutter.dev/docs/testing/errors>, more information about error
929 /// handling in Flutter.
930 static FlutterExceptionHandler? onError = presentError;
931
932 /// Called by the Flutter framework before attempting to parse a [StackTrace].
933 ///
934 /// Some [StackTrace] implementations have a different [toString] format from
935 /// what the framework expects, like ones from `package:stack_trace`. To make
936 /// sure we can still parse and filter mangled [StackTrace]s, the framework
937 /// first calls this function to demangle them.
938 ///
939 /// This should be set in any environment that could propagate an unusual
940 /// stack trace to the framework. Otherwise, the default behavior is to assume
941 /// all stack traces are in a format usually generated by Dart.
942 ///
943 /// The following example demangles `package:stack_trace` traces by converting
944 /// them into VM traces, which the framework is able to parse:
945 ///
946 /// ```dart
947 /// FlutterError.demangleStackTrace = (StackTrace stack) {
948 /// // Trace and Chain are classes in package:stack_trace
949 /// if (stack is Trace) {
950 /// return stack.vmTrace;
951 /// }
952 /// if (stack is Chain) {
953 /// return stack.toTrace().vmTrace;
954 /// }
955 /// return stack;
956 /// };
957 /// ```
958 static StackTraceDemangler demangleStackTrace = _defaultStackTraceDemangler;
959
960 static StackTrace _defaultStackTraceDemangler(StackTrace stackTrace) => stackTrace;
961
962 /// Called whenever the Flutter framework wants to present an error to the
963 /// users.
964 ///
965 /// The default behavior is to call [dumpErrorToConsole].
966 ///
967 /// Plugins can override how an error is to be presented to the user. For
968 /// example, the structured errors service extension sets its own method when
969 /// the extension is enabled. If you want to change how Flutter responds to an
970 /// error, use [onError] instead.
971 static FlutterExceptionHandler presentError = dumpErrorToConsole;
972
973 static int _errorCount = 0;
974
975 /// Resets the count of errors used by [dumpErrorToConsole] to decide whether
976 /// to show a complete error message or an abbreviated one.
977 ///
978 /// After this is called, the next error message will be shown in full.
979 static void resetErrorCount() {
980 _errorCount = 0;
981 }
982
983 /// The width to which [dumpErrorToConsole] will wrap lines.
984 ///
985 /// This can be used to ensure strings will not exceed the length at which
986 /// they will wrap, e.g. when placing ASCII art diagrams in messages.
987 static const int wrapWidth = 100;
988
989 /// Prints the given exception details to the console.
990 ///
991 /// The first time this is called, it dumps a very verbose message to the
992 /// console using [debugPrint].
993 ///
994 /// Subsequent calls only dump the first line of the exception, unless
995 /// `forceReport` is set to true (in which case it dumps the verbose message).
996 ///
997 /// Call [resetErrorCount] to cause this method to go back to acting as if it
998 /// had not been called before (so the next message is verbose again).
999 ///
1000 /// The default behavior for the [onError] handler is to call this function.
1001 static void dumpErrorToConsole(FlutterErrorDetails details, { bool forceReport = false }) {
1002 bool isInDebugMode = false;
1003 assert(() {
1004 // In debug mode, we ignore the "silent" flag.
1005 isInDebugMode = true;
1006 return true;
1007 }());
1008 final bool reportError = isInDebugMode || !details.silent;
1009 if (!reportError && !forceReport) {
1010 return;
1011 }
1012 if (_errorCount == 0 || forceReport) {
1013 // Diagnostics is only available in debug mode. In profile and release modes fallback to plain print.
1014 if (isInDebugMode) {
1015 debugPrint(
1016 TextTreeRenderer(
1017 wrapWidthProperties: wrapWidth,
1018 maxDescendentsTruncatableNode: 5,
1019 ).render(details.toDiagnosticsNode(style: DiagnosticsTreeStyle.error)).trimRight(),
1020 );
1021 } else {
1022 debugPrintStack(
1023 stackTrace: details.stack,
1024 label: details.exception.toString(),
1025 maxFrames: 100,
1026 );
1027 }
1028 } else {
1029 debugPrint('Another exception was thrown: ${details.summary}');
1030 }
1031 _errorCount += 1;
1032 }
1033
1034 static final List<StackFilter> _stackFilters = <StackFilter>[];
1035
1036 /// Adds a stack filtering function to [defaultStackFilter].
1037 ///
1038 /// For example, the framework adds common patterns of element building to
1039 /// elide tree-walking patterns in the stack trace.
1040 ///
1041 /// Added filters are checked in order of addition. The first matching filter
1042 /// wins, and subsequent filters will not be checked.
1043 static void addDefaultStackFilter(StackFilter filter) {
1044 _stackFilters.add(filter);
1045 }
1046
1047 /// Converts a stack to a string that is more readable by omitting stack
1048 /// frames that correspond to Dart internals.
1049 ///
1050 /// This is the default filter used by [dumpErrorToConsole] if the
1051 /// [FlutterErrorDetails] object has no [FlutterErrorDetails.stackFilter]
1052 /// callback.
1053 ///
1054 /// This function expects its input to be in the format used by
1055 /// [StackTrace.toString()]. The output of this function is similar to that
1056 /// format but the frame numbers will not be consecutive (frames are elided)
1057 /// and the final line may be prose rather than a stack frame.
1058 static Iterable<String> defaultStackFilter(Iterable<String> frames) {
1059 final Map<String, int> removedPackagesAndClasses = <String, int>{
1060 'dart:async-patch': 0,
1061 'dart:async': 0,
1062 'package:stack_trace': 0,
1063 'class _AssertionError': 0,
1064 'class _FakeAsync': 0,
1065 'class _FrameCallbackEntry': 0,
1066 'class _Timer': 0,
1067 'class _RawReceivePortImpl': 0,
1068 };
1069 int skipped = 0;
1070
1071 final List<StackFrame> parsedFrames = StackFrame.fromStackString(frames.join('\n'));
1072
1073 for (int index = 0; index < parsedFrames.length; index += 1) {
1074 final StackFrame frame = parsedFrames[index];
1075 final String className = 'class ${frame.className}';
1076 final String package = '${frame.packageScheme}:${frame.package}';
1077 if (removedPackagesAndClasses.containsKey(className)) {
1078 skipped += 1;
1079 removedPackagesAndClasses.update(className, (int value) => value + 1);
1080 parsedFrames.removeAt(index);
1081 index -= 1;
1082 } else if (removedPackagesAndClasses.containsKey(package)) {
1083 skipped += 1;
1084 removedPackagesAndClasses.update(package, (int value) => value + 1);
1085 parsedFrames.removeAt(index);
1086 index -= 1;
1087 }
1088 }
1089 final List<String?> reasons = List<String?>.filled(parsedFrames.length, null);
1090 for (final StackFilter filter in _stackFilters) {
1091 filter.filter(parsedFrames, reasons);
1092 }
1093
1094 final List<String> result = <String>[];
1095
1096 // Collapse duplicated reasons.
1097 for (int index = 0; index < parsedFrames.length; index += 1) {
1098 final int start = index;
1099 while (index < reasons.length - 1 && reasons[index] != null && reasons[index + 1] == reasons[index]) {
1100 index++;
1101 }
1102 String suffix = '';
1103 if (reasons[index] != null) {
1104 if (index != start) {
1105 suffix = ' (${index - start + 2} frames)';
1106 } else {
1107 suffix = ' (1 frame)';
1108 }
1109 }
1110 final String resultLine = '${reasons[index] ?? parsedFrames[index].source}$suffix';
1111 result.add(resultLine);
1112 }
1113
1114 // Only include packages we actually elided from.
1115 final List<String> where = <String>[
1116 for (final MapEntry<String, int> entry in removedPackagesAndClasses.entries)
1117 if (entry.value > 0)
1118 entry.key,
1119 ]..sort();
1120 if (skipped == 1) {
1121 result.add('(elided one frame from ${where.single})');
1122 } else if (skipped > 1) {
1123 if (where.length > 1) {
1124 where[where.length - 1] = 'and ${where.last}';
1125 }
1126 if (where.length > 2) {
1127 result.add('(elided $skipped frames from ${where.join(", ")})');
1128 } else {
1129 result.add('(elided $skipped frames from ${where.join(" ")})');
1130 }
1131 }
1132 return result;
1133 }
1134
1135 @override
1136 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1137 diagnostics.forEach(properties.add);
1138 }
1139
1140 @override
1141 String toStringShort() => 'FlutterError';
1142
1143 @override
1144 String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
1145 if (kReleaseMode) {
1146 final Iterable<_ErrorDiagnostic> errors = diagnostics.whereType<_ErrorDiagnostic>();
1147 return errors.isNotEmpty ? errors.first.valueToString() : toStringShort();
1148 }
1149 // Avoid wrapping lines.
1150 final TextTreeRenderer renderer = TextTreeRenderer(wrapWidth: 4000000000);
1151 return diagnostics.map((DiagnosticsNode node) => renderer.render(node).trimRight()).join('\n');
1152 }
1153
1154 /// Calls [onError] with the given details, unless it is null.
1155 ///
1156 /// {@tool snippet}
1157 /// When calling this from a `catch` block consider annotating the method
1158 /// containing the `catch` block with
1159 /// `@pragma('vm:notify-debugger-on-exception')` to allow an attached debugger
1160 /// to treat the exception as unhandled. This means instead of executing the
1161 /// `catch` block, the debugger can break at the original source location from
1162 /// which the exception was thrown.
1163 ///
1164 /// ```dart
1165 /// @pragma('vm:notify-debugger-on-exception')
1166 /// void doSomething() {
1167 /// try {
1168 /// methodThatMayThrow();
1169 /// } catch (exception, stack) {
1170 /// FlutterError.reportError(FlutterErrorDetails(
1171 /// exception: exception,
1172 /// stack: stack,
1173 /// library: 'example library',
1174 /// context: ErrorDescription('while doing something'),
1175 /// ));
1176 /// }
1177 /// }
1178 /// ```
1179 /// {@end-tool}
1180 static void reportError(FlutterErrorDetails details) {
1181 onError?.call(details);
1182 }
1183}
1184
1185/// Dump the stack to the console using [debugPrint] and
1186/// [FlutterError.defaultStackFilter].
1187///
1188/// If the `stackTrace` parameter is null, the [StackTrace.current] is used to
1189/// obtain the stack.
1190///
1191/// The `maxFrames` argument can be given to limit the stack to the given number
1192/// of lines before filtering is applied. By default, all stack lines are
1193/// included.
1194///
1195/// The `label` argument, if present, will be printed before the stack.
1196void debugPrintStack({StackTrace? stackTrace, String? label, int? maxFrames}) {
1197 if (label != null) {
1198 debugPrint(label);
1199 }
1200 if (stackTrace == null) {
1201 stackTrace = StackTrace.current;
1202 } else {
1203 stackTrace = FlutterError.demangleStackTrace(stackTrace);
1204 }
1205 Iterable<String> lines = stackTrace.toString().trimRight().split('\n');
1206 if (kIsWeb && lines.isNotEmpty) {
1207 // Remove extra call to StackTrace.current for web platform.
1208 // TODO(ferhat): remove when https://github.com/flutter/flutter/issues/37635
1209 // is addressed.
1210 lines = lines.skipWhile((String line) {
1211 return line.contains('StackTrace.current') ||
1212 line.contains('dart-sdk/lib/_internal') ||
1213 line.contains('dart:sdk_internal');
1214 });
1215 }
1216 if (maxFrames != null) {
1217 lines = lines.take(maxFrames);
1218 }
1219 debugPrint(FlutterError.defaultStackFilter(lines).join('\n'));
1220}
1221
1222/// Diagnostic with a [StackTrace] [value] suitable for displaying stack traces
1223/// as part of a [FlutterError] object.
1224class DiagnosticsStackTrace extends DiagnosticsBlock {
1225 /// Creates a diagnostic for a stack trace.
1226 ///
1227 /// [name] describes a name the stack trace is given, e.g.
1228 /// `When the exception was thrown, this was the stack`.
1229 /// [stackFilter] provides an optional filter to use to filter which frames
1230 /// are included. If no filter is specified, [FlutterError.defaultStackFilter]
1231 /// is used.
1232 /// [showSeparator] indicates whether to include a ':' after the [name].
1233 DiagnosticsStackTrace(
1234 String name,
1235 StackTrace? stack, {
1236 IterableFilter<String>? stackFilter,
1237 super.showSeparator,
1238 }) : super(
1239 name: name,
1240 value: stack,
1241 properties: _applyStackFilter(stack, stackFilter),
1242 style: DiagnosticsTreeStyle.flat,
1243 allowTruncate: true,
1244 );
1245
1246 /// Creates a diagnostic describing a single frame from a StackTrace.
1247 DiagnosticsStackTrace.singleFrame(
1248 String name, {
1249 required String frame,
1250 super.showSeparator,
1251 }) : super(
1252 name: name,
1253 properties: <DiagnosticsNode>[_createStackFrame(frame)],
1254 style: DiagnosticsTreeStyle.whitespace,
1255 );
1256
1257 static List<DiagnosticsNode> _applyStackFilter(
1258 StackTrace? stack,
1259 IterableFilter<String>? stackFilter,
1260 ) {
1261 if (stack == null) {
1262 return <DiagnosticsNode>[];
1263 }
1264 final IterableFilter<String> filter = stackFilter ?? FlutterError.defaultStackFilter;
1265 final Iterable<String> frames = filter('${FlutterError.demangleStackTrace(stack)}'.trimRight().split('\n'));
1266 return frames.map<DiagnosticsNode>(_createStackFrame).toList();
1267 }
1268
1269 static DiagnosticsNode _createStackFrame(String frame) {
1270 return DiagnosticsNode.message(frame, allowWrap: false);
1271 }
1272
1273 @override
1274 bool get allowTruncate => false;
1275}
1276
1277class _FlutterErrorDetailsNode extends DiagnosticableNode<FlutterErrorDetails> {
1278 _FlutterErrorDetailsNode({
1279 super.name,
1280 required super.value,
1281 required super.style,
1282 });
1283
1284 @override
1285 DiagnosticPropertiesBuilder? get builder {
1286 final DiagnosticPropertiesBuilder? builder = super.builder;
1287 if (builder == null) {
1288 return null;
1289 }
1290 Iterable<DiagnosticsNode> properties = builder.properties;
1291 for (final DiagnosticPropertiesTransformer transformer in FlutterErrorDetails.propertiesTransformers) {
1292 properties = transformer(properties);
1293 }
1294 return DiagnosticPropertiesBuilder.fromProperties(properties.toList());
1295 }
1296}
1297