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 | |
5 | import 'package:meta/meta.dart' ; |
6 | |
7 | import 'basic_types.dart'; |
8 | import 'constants.dart'; |
9 | import 'diagnostics.dart'; |
10 | import 'print.dart'; |
11 | import 'stack_frame.dart'; |
12 | |
13 | export 'basic_types.dart' show IterableFilter; |
14 | export 'diagnostics.dart' show DiagnosticLevel, DiagnosticPropertiesBuilder, DiagnosticsNode, DiagnosticsTreeStyle; |
15 | export '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. |
27 | typedef FlutterExceptionHandler = void Function(FlutterErrorDetails details); |
28 | |
29 | /// Signature for [DiagnosticPropertiesBuilder] transformer. |
30 | typedef DiagnosticPropertiesTransformer = Iterable<DiagnosticsNode> Function(Iterable<DiagnosticsNode> properties); |
31 | |
32 | /// Signature for [FlutterErrorDetails.informationCollector] callback |
33 | /// and other callbacks that collect information describing an error. |
34 | typedef 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. |
42 | typedef 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 |
50 | class 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]. |
99 | abstract 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. |
122 | class 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 | |
168 | abstract 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( |
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]. |
261 | class 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]. |
300 | class 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]. |
336 | class 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. |
356 | class 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. |
393 | class 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. |
763 | class 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.
|
1196 | void 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.
|
1224 | class 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 |
|
1277 | class _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 |
|