| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:meta/meta.dart'; |
| |
| import 'basic_types.dart'; |
| import 'constants.dart'; |
| import 'diagnostics.dart'; |
| import 'print.dart'; |
| import 'stack_frame.dart'; |
| |
| /// Signature for [FlutterError.onError] handler. |
| typedef FlutterExceptionHandler = void Function(FlutterErrorDetails details); |
| |
| /// Signature for [DiagnosticPropertiesBuilder] transformer. |
| typedef DiagnosticPropertiesTransformer = Iterable<DiagnosticsNode> Function(Iterable<DiagnosticsNode> properties); |
| |
| /// Signature for [FlutterErrorDetails.informationCollector] callback |
| /// and other callbacks that collect information describing an error. |
| typedef InformationCollector = Iterable<DiagnosticsNode> Function(); |
| |
| /// Partial information from a stack frame for stack filtering purposes. |
| /// |
| /// See also: |
| /// |
| /// * [RepetitiveStackFrameFilter], which uses this class to compare against [StackFrame]s. |
| @immutable |
| class PartialStackFrame { |
| /// Creates a new [PartialStackFrame] instance. All arguments are required and |
| /// must not be null. |
| const PartialStackFrame({ |
| @required this.package, |
| @required this.className, |
| @required this.method, |
| }) : assert(className != null), |
| assert(method != null), |
| assert(package != null); |
| |
| /// An `<asynchronous suspension>` line in a stack trace. |
| static const PartialStackFrame asynchronousSuspension = PartialStackFrame( |
| package: '', |
| className: '', |
| method: 'asynchronous suspension', |
| ); |
| |
| /// The package to match, e.g. `package:flutter/src/foundation/assertions.dart`, |
| /// or `dart:ui/window.dart`. |
| final Pattern package; |
| |
| /// The class name for the method. |
| /// |
| /// On web, this is ignored, since class names are not available. |
| /// |
| /// On all platforms, top level methods should use the empty string. |
| final String className; |
| |
| /// The method name for this frame line. |
| /// |
| /// On web, private methods are wrapped with `[]`. |
| final String method; |
| |
| /// Tests whether the [StackFrame] matches the information in this |
| /// [PartialStackFrame]. |
| bool matches(StackFrame stackFrame) { |
| final String stackFramePackage = '${stackFrame.packageScheme}:${stackFrame.package}/${stackFrame.packagePath}'; |
| // Ideally this wouldn't be necessary. |
| // TODO(dnfield): https://github.com/dart-lang/sdk/issues/40117 |
| if (kIsWeb) { |
| return package.allMatches(stackFramePackage).isNotEmpty |
| && stackFrame.method == (method.startsWith('_') ? '[$method]' : method); |
| } |
| return package.allMatches(stackFramePackage).isNotEmpty |
| && stackFrame.method == method |
| && stackFrame.className == className; |
| } |
| } |
| |
| /// A class that filters stack frames for additional filtering on |
| /// [FlutterError.defaultStackFilter]. |
| abstract class StackFilter { |
| /// A const constructor to allow subclasses to be const. |
| const StackFilter(); |
| |
| /// Filters the list of [StackFrame]s by updating corrresponding indices in |
| /// `reasons`. |
| /// |
| /// To elide a frame or number of frames, set the string |
| void filter(List<StackFrame> stackFrames, List<String> reasons); |
| } |
| |
| |
| /// A [StackFilter] that filters based on repeating lists of |
| /// [PartialStackFrame]s. |
| /// |
| /// See also: |
| /// |
| /// * [FlutterError.addDefaultStackFilter], a method to register additional |
| /// stack filters for [FlutterError.defaultStackFilter]. |
| /// * [StackFrame], a class that can help with parsing stack frames. |
| /// * [PartialStackFrame], a class that helps match partial method information |
| /// to a stack frame. |
| class RepetitiveStackFrameFilter extends StackFilter { |
| /// Creates a new RepetitiveStackFrameFilter. All parameters are required and must not be |
| /// null. |
| const RepetitiveStackFrameFilter({ |
| @required this.frames, |
| @required this.replacement, |
| }) : assert(frames != null), |
| assert(replacement != null); |
| |
| /// The shape of this repetative stack pattern. |
| final List<PartialStackFrame> frames; |
| |
| /// The number of frames in this pattern. |
| int get numFrames => frames.length; |
| |
| /// The string to replace the frames with. |
| /// |
| /// If the same replacement string is used multiple times in a row, the |
| /// [FlutterError.defaultStackFilter] will simply update a counter after this |
| /// line rather than repeating it. |
| final String replacement; |
| |
| List<String> get _replacements => List<String>.filled(numFrames, replacement); |
| |
| @override |
| void filter(List<StackFrame> stackFrames, List<String> reasons) { |
| for (int index = 0; index < stackFrames.length - numFrames; index += 1) { |
| if (_matchesFrames(stackFrames.skip(index).take(numFrames).toList())) { |
| reasons.setRange(index, index + numFrames, _replacements); |
| index += numFrames - 1; |
| } |
| } |
| } |
| |
| bool _matchesFrames(List<StackFrame> stackFrames) { |
| if (stackFrames.length < numFrames) { |
| return false; |
| } |
| for (int index = 0; index < stackFrames.length; index++) { |
| if (!frames[index].matches(stackFrames[index])) { |
| return false; |
| } |
| } |
| return true; |
| } |
| } |
| |
| abstract class _ErrorDiagnostic extends DiagnosticsProperty<List<Object>> { |
| /// This constructor provides a reliable hook for a kernel transformer to find |
| /// error messages that need to be rewritten to include object references for |
| /// interactive display of errors. |
| _ErrorDiagnostic( |
| String message, { |
| DiagnosticsTreeStyle style = DiagnosticsTreeStyle.flat, |
| DiagnosticLevel level = DiagnosticLevel.info, |
| }) : assert(message != null), |
| super( |
| null, |
| <Object>[message], |
| showName: false, |
| showSeparator: false, |
| defaultValue: null, |
| style: style, |
| level: level, |
| ); |
| |
| /// In debug builds, a kernel transformer rewrites calls to the default |
| /// constructors for [ErrorSummary], [ErrorDetails], and [ErrorHint] to use |
| /// this constructor. |
| // |
| // ```dart |
| // _ErrorDiagnostic('Element $element must be $color') |
| // ``` |
| // Desugars to: |
| // ```dart |
| // _ErrorDiagnostic.fromParts(<Object>['Element ', element, ' must be ', color]) |
| // ``` |
| // |
| // Slightly more complex case: |
| // ```dart |
| // _ErrorDiagnostic('Element ${element.runtimeType} must be $color') |
| // ``` |
| // Desugars to: |
| //```dart |
| // _ErrorDiagnostic.fromParts(<Object>[ |
| // 'Element ', |
| // DiagnosticsProperty(null, element, description: element.runtimeType?.toString()), |
| // ' must be ', |
| // color, |
| // ]) |
| // ``` |
| _ErrorDiagnostic._fromParts( |
| List<Object> messageParts, { |
| DiagnosticsTreeStyle style = DiagnosticsTreeStyle.flat, |
| DiagnosticLevel level = DiagnosticLevel.info, |
| }) : assert(messageParts != null), |
| super( |
| null, |
| messageParts, |
| showName: false, |
| showSeparator: false, |
| defaultValue: null, |
| style: style, |
| level: level, |
| ); |
| |
| @override |
| String valueToString({ TextTreeConfiguration parentConfiguration }) { |
| return value.join(''); |
| } |
| } |
| |
| /// An explanation of the problem and its cause, any information that may help |
| /// track down the problem, background information, etc. |
| /// |
| /// Use [ErrorDescription] for any part of an error message where neither |
| /// [ErrorSummary] or [ErrorHint] is appropriate. |
| /// |
| /// See also: |
| /// |
| /// * [ErrorSummary], which provides a short (one line) description of the |
| /// problem that was detected. |
| /// * [ErrorHint], which provides specific, non-obvious advice that may be |
| /// applicable. |
| /// * [FlutterError], which is the most common place to use an |
| /// [ErrorDescription]. |
| class ErrorDescription extends _ErrorDiagnostic { |
| /// A lint enforces that this constructor can only be called with a string |
| /// literal to match the limitations of the Dart Kernel transformer that |
| /// optionally extracts out objects referenced using string interpolation in |
| /// the message passed in. |
| /// |
| /// The message will display with the same text regardless of whether the |
| /// kernel transformer is used. The kernel transformer is required so that |
| /// debugging tools can provide interactive displays of objects described by |
| /// the error. |
| ErrorDescription(String message) : super(message, level: DiagnosticLevel.info); |
| |
| /// Calls to the default constructor may be rewritten to use this constructor |
| /// in debug mode using a kernel transformer. |
| // ignore: unused_element |
| ErrorDescription._fromParts(List<Object> messageParts) : super._fromParts(messageParts, level: DiagnosticLevel.info); |
| } |
| |
| /// A short (one line) description of the problem that was detected. |
| /// |
| /// Error summaries from the same source location should have little variance, |
| /// so that they can be recognized as related. For example, they shouldn't |
| /// include hash codes. |
| /// |
| /// A [FlutterError] must start with an [ErrorSummary] and may not contain |
| /// multiple summaries. |
| /// |
| /// See also: |
| /// |
| /// * [ErrorDescription], which provides an explanation of the problem and its |
| /// cause, any information that may help track down the problem, background |
| /// information, etc. |
| /// * [ErrorHint], which provides specific, non-obvious advice that may be |
| /// applicable. |
| /// * [FlutterError], which is the most common place to use an [ErrorSummary]. |
| class ErrorSummary extends _ErrorDiagnostic { |
| /// A lint enforces that this constructor can only be called with a string |
| /// literal to match the limitations of the Dart Kernel transformer that |
| /// optionally extracts out objects referenced using string interpolation in |
| /// the message passed in. |
| /// |
| /// The message will display with the same text regardless of whether the |
| /// kernel transformer is used. The kernel transformer is required so that |
| /// debugging tools can provide interactive displays of objects described by |
| /// the error. |
| ErrorSummary(String message) : super(message, level: DiagnosticLevel.summary); |
| |
| /// Calls to the default constructor may be rewritten to use this constructor |
| /// in debug mode using a kernel transformer. |
| // ignore: unused_element |
| ErrorSummary._fromParts(List<Object> messageParts) : super._fromParts(messageParts, level: DiagnosticLevel.summary); |
| } |
| |
| /// An [ErrorHint] provides specific, non-obvious advice that may be applicable. |
| /// |
| /// If your message provides obvious advice that is always applicable it is an |
| /// [ErrorDescription] not a hint. |
| /// |
| /// See also: |
| /// |
| /// * [ErrorSummary], which provides a short (one line) description of the |
| /// problem that was detected. |
| /// * [ErrorDescription], which provides an explanation of the problem and its |
| /// cause, any information that may help track down the problem, background |
| /// information, etc. |
| /// * [FlutterError], which is the most common place to use an [ErrorHint]. |
| class ErrorHint extends _ErrorDiagnostic { |
| /// A lint enforces that this constructor can only be called with a string |
| /// literal to match the limitations of the Dart Kernel transformer that |
| /// optionally extracts out objects referenced using string interpolation in |
| /// the message passed in. |
| /// |
| /// The message will display with the same text regardless of whether the |
| /// kernel transformer is used. The kernel transformer is required so that |
| /// debugging tools can provide interactive displays of objects described by |
| /// the error. |
| ErrorHint(String message) : super(message, level:DiagnosticLevel.hint); |
| |
| /// Calls to the default constructor may be rewritten to use this constructor |
| /// in debug mode using a kernel transformer. |
| // ignore: unused_element |
| ErrorHint._fromParts(List<Object> messageParts) : super._fromParts(messageParts, level:DiagnosticLevel.hint); |
| } |
| |
| /// An [ErrorSpacer] creates an empty [DiagnosticsNode], that can be used to |
| /// tune the spacing between other [DiagnosticsNode] objects. |
| class ErrorSpacer extends DiagnosticsProperty<void> { |
| /// Creates an empty space to insert into a list of [DiagnosticsNode] objects |
| /// typically within a [FlutterError] object. |
| ErrorSpacer() : super( |
| '', |
| null, |
| description: '', |
| showName: false, |
| ); |
| } |
| |
| /// Class for information provided to [FlutterExceptionHandler] callbacks. |
| /// |
| /// See [FlutterError.onError]. |
| class FlutterErrorDetails with Diagnosticable { |
| /// Creates a [FlutterErrorDetails] object with the given arguments setting |
| /// the object's properties. |
| /// |
| /// The framework calls this constructor when catching an exception that will |
| /// subsequently be reported using [FlutterError.onError]. |
| /// |
| /// The [exception] must not be null; other arguments can be left to |
| /// their default values. (`throw null` results in a |
| /// [NullThrownError] exception.) |
| const FlutterErrorDetails({ |
| this.exception, |
| this.stack, |
| this.library = 'Flutter framework', |
| this.context, |
| this.stackFilter, |
| this.informationCollector, |
| this.silent = false, |
| }); |
| |
| /// Transformers to transform [DiagnosticsNode] in [DiagnosticPropertiesBuilder] |
| /// into a more descriptive form. |
| /// |
| /// There are layers that attach certain [DiagnosticsNode] into |
| /// [FlutterErrorDetails] that require knowledge from other layers to parse. |
| /// To correctly interpret those [DiagnosticsNode], register transformers in |
| /// the layers that possess the knowledge. |
| /// |
| /// See also: |
| /// |
| /// * [WidgetsBinding.initInstances], which registers its transformer. |
| static final List<DiagnosticPropertiesTransformer> propertiesTransformers = |
| <DiagnosticPropertiesTransformer>[]; |
| |
| /// The exception. Often this will be an [AssertionError], maybe specifically |
| /// a [FlutterError]. However, this could be any value at all. |
| final dynamic exception; |
| |
| /// The stack trace from where the [exception] was thrown (as opposed to where |
| /// it was caught). |
| /// |
| /// StackTrace objects are opaque except for their [toString] function. |
| /// |
| /// If this field is not null, then the [stackFilter] callback, if any, will |
| /// be called with the result of calling [toString] on this object and |
| /// splitting that result on line breaks. If there's no [stackFilter] |
| /// callback, then [FlutterError.defaultStackFilter] is used instead. That |
| /// function expects the stack to be in the format used by |
| /// [StackTrace.toString]. |
| final StackTrace stack; |
| |
| /// A human-readable brief name describing the library that caught the error |
| /// message. This is used by the default error handler in the header dumped to |
| /// the console. |
| final String library; |
| |
| /// A human-readable description of where the error was caught (as opposed to |
| /// where it was thrown). |
| /// |
| /// The string should be in a form that will make sense in English when |
| /// following the word "thrown", as in "thrown while obtaining the image from |
| /// the network" (for the context "while obtaining the image from the |
| /// network"). |
| final DiagnosticsNode context; |
| |
| /// A callback which filters the [stack] trace. Receives an iterable of |
| /// strings representing the frames encoded in the way that |
| /// [StackTrace.toString()] provides. Should return an iterable of lines to |
| /// output for the stack. |
| /// |
| /// If this is not provided, then [FlutterError.dumpErrorToConsole] will use |
| /// [FlutterError.defaultStackFilter] instead. |
| /// |
| /// If the [FlutterError.defaultStackFilter] behavior is desired, then the |
| /// callback should manually call that function. That function expects the |
| /// incoming list to be in the [StackTrace.toString()] format. The output of |
| /// that function, however, does not always follow this format. |
| /// |
| /// This won't be called if [stack] is null. |
| final IterableFilter<String> stackFilter; |
| |
| /// A callback which, when called with a [StringBuffer] will write to that buffer |
| /// information that could help with debugging the problem. |
| /// |
| /// Information collector callbacks can be expensive, so the generated information |
| /// should be cached, rather than the callback being called multiple times. |
| /// |
| /// The text written to the information argument may contain newlines but should |
| /// not end with a newline. |
| final InformationCollector informationCollector; |
| |
| /// Whether this error should be ignored by the default error reporting |
| /// behavior in release mode. |
| /// |
| /// If this is false, the default, then the default error handler will always |
| /// dump this error to the console. |
| /// |
| /// If this is true, then the default error handler would only dump this error |
| /// to the console in checked mode. In release mode, the error is ignored. |
| /// |
| /// This is used by certain exception handlers that catch errors that could be |
| /// triggered by environmental conditions (as opposed to logic errors). For |
| /// example, the HTTP library sets this flag so as to not report every 404 |
| /// error to the console on end-user devices, while still allowing a custom |
| /// error handler to see the errors even in release builds. |
| final bool silent; |
| |
| /// Converts the [exception] to a string. |
| /// |
| /// This applies some additional logic to make [AssertionError] exceptions |
| /// prettier, to handle exceptions that stringify to empty strings, to handle |
| /// objects that don't inherit from [Exception] or [Error], and so forth. |
| String exceptionAsString() { |
| String longMessage; |
| if (exception is AssertionError) { |
| // Regular _AssertionErrors thrown by assert() put the message last, after |
| // some code snippets. This leads to ugly messages. To avoid this, we move |
| // the assertion message up to before the code snippets, separated by a |
| // newline, if we recognize that format is being used. |
| final Object message = exception.message; |
| final String fullMessage = exception.toString(); |
| if (message is String && message != fullMessage) { |
| if (fullMessage.length > message.length) { |
| final int position = fullMessage.lastIndexOf(message); |
| if (position == fullMessage.length - message.length && |
| position > 2 && |
| fullMessage.substring(position - 2, position) == ': ') { |
| // Add a linebreak so that the filename at the start of the |
| // assertion message is always on its own line. |
| String body = fullMessage.substring(0, position - 2); |
| final int splitPoint = body.indexOf(' Failed assertion:'); |
| if (splitPoint >= 0) { |
| body = '${body.substring(0, splitPoint)}\n${body.substring(splitPoint + 1)}'; |
| } |
| longMessage = '${message.trimRight()}\n$body'; |
| } |
| } |
| } |
| longMessage ??= fullMessage; |
| } else if (exception is String) { |
| longMessage = exception as String; |
| } else if (exception is Error || exception is Exception) { |
| longMessage = exception.toString(); |
| } else { |
| longMessage = ' ${exception.toString()}'; |
| } |
| longMessage = longMessage.trimRight(); |
| if (longMessage.isEmpty) |
| longMessage = ' <no message available>'; |
| return longMessage; |
| } |
| |
| Diagnosticable _exceptionToDiagnosticable() { |
| if (exception is FlutterError) { |
| return exception as FlutterError; |
| } |
| if (exception is AssertionError && exception.message is FlutterError) { |
| return exception.message as FlutterError; |
| } |
| return null; |
| } |
| |
| /// Returns a short (one line) description of the problem that was detected. |
| /// |
| /// If the exception contains an [ErrorSummary] that summary is used, |
| /// otherwise the summary is inferred from the string representation of the |
| /// exception. |
| /// |
| /// In release mode, this always returns a [DiagnosticsNode.message] with a |
| /// formatted version of the exception. |
| DiagnosticsNode get summary { |
| String formatException() => exceptionAsString().split('\n')[0].trimLeft(); |
| if (kReleaseMode) { |
| return DiagnosticsNode.message(formatException()); |
| } |
| final Diagnosticable diagnosticable = _exceptionToDiagnosticable(); |
| DiagnosticsNode summary; |
| if (diagnosticable != null) { |
| final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
| debugFillProperties(builder); |
| summary = builder.properties.firstWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.summary, orElse: () => null); |
| } |
| return summary ?? ErrorSummary(formatException()); |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| final DiagnosticsNode verb = ErrorDescription('thrown${ context != null ? ErrorDescription(" $context") : ""}'); |
| final Diagnosticable diagnosticable = _exceptionToDiagnosticable(); |
| if (exception is NullThrownError) { |
| properties.add(ErrorDescription('The null value was $verb.')); |
| } else if (exception is num) { |
| properties.add(ErrorDescription('The number $exception was $verb.')); |
| } else { |
| DiagnosticsNode errorName; |
| if (exception is AssertionError) { |
| errorName = ErrorDescription('assertion'); |
| } else if (exception is String) { |
| errorName = ErrorDescription('message'); |
| } else if (exception is Error || exception is Exception) { |
| errorName = ErrorDescription('${exception.runtimeType}'); |
| } else { |
| errorName = ErrorDescription('${exception.runtimeType} object'); |
| } |
| properties.add(ErrorDescription('The following $errorName was $verb:')); |
| if (diagnosticable != null) { |
| diagnosticable.debugFillProperties(properties); |
| } else { |
| // Many exception classes put their type at the head of their message. |
| // This is redundant with the way we display exceptions, so attempt to |
| // strip out that header when we see it. |
| final String prefix = '${exception.runtimeType}: '; |
| String message = exceptionAsString(); |
| if (message.startsWith(prefix)) |
| message = message.substring(prefix.length); |
| properties.add(ErrorSummary(message)); |
| } |
| } |
| |
| if (stack != null) { |
| if (exception is AssertionError && diagnosticable == null) { |
| // After popping off any dart: stack frames, are there at least two more |
| // stack frames coming from package flutter? |
| // |
| // If not: Error is in user code (user violated assertion in framework). |
| // If so: Error is in Framework. We either need an assertion higher up |
| // in the stack, or we've violated our own assertions. |
| final List<StackFrame> stackFrames = StackFrame.fromStackTrace(stack) |
| .skipWhile((StackFrame frame) => frame.packageScheme == 'dart') |
| .toList(); |
| final bool ourFault = stackFrames.length >= 2 |
| && stackFrames[0].package == 'flutter' |
| && stackFrames[1].package == 'flutter'; |
| if (ourFault) { |
| properties.add(ErrorSpacer()); |
| properties.add(ErrorHint( |
| 'Either the assertion indicates an error in the framework itself, or we should ' |
| 'provide substantially more information in this error message to help you determine ' |
| 'and fix the underlying cause.\n' |
| 'In either case, please report this assertion by filing a bug on GitHub:\n' |
| ' https://github.com/flutter/flutter/issues/new?template=BUG.md' |
| )); |
| } |
| } |
| properties.add(ErrorSpacer()); |
| properties.add(DiagnosticsStackTrace('When the exception was thrown, this was the stack', stack, stackFilter: stackFilter)); |
| } |
| if (informationCollector != null) { |
| properties.add(ErrorSpacer()); |
| informationCollector().forEach(properties.add); |
| } |
| } |
| |
| @override |
| String toStringShort() { |
| return library != null ? 'Exception caught by $library' : 'Exception caught'; |
| } |
| |
| @override |
| String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { |
| return toDiagnosticsNode(style: DiagnosticsTreeStyle.error).toStringDeep(minLevel: minLevel); |
| } |
| |
| @override |
| DiagnosticsNode toDiagnosticsNode({ String name, DiagnosticsTreeStyle style }) { |
| return _FlutterErrorDetailsNode( |
| name: name, |
| value: this, |
| style: style, |
| ); |
| } |
| } |
| |
| /// Error class used to report Flutter-specific assertion failures and |
| /// contract violations. |
| /// |
| /// See also: |
| /// |
| /// * <https://flutter.dev/docs/testing/errors>, more information about error |
| /// handling in Flutter. |
| class FlutterError extends Error with DiagnosticableTreeMixin implements AssertionError { |
| /// Create an error message from a string. |
| /// |
| /// The message may have newlines in it. The first line should be a terse |
| /// description of the error, e.g. "Incorrect GlobalKey usage" or "setState() |
| /// or markNeedsBuild() called during build". Subsequent lines should contain |
| /// substantial additional information, ideally sufficient to develop a |
| /// correct solution to the problem. |
| /// |
| /// In some cases, when a FlutterError is reported to the user, only the first |
| /// line is included. For example, Flutter will typically only fully report |
| /// the first exception at runtime, displaying only the first line of |
| /// subsequent errors. |
| /// |
| /// All sentences in the error should be correctly punctuated (i.e., |
| /// do end the error message with a period). |
| /// |
| /// This constructor defers to the [new FlutterError.fromParts] constructor. |
| /// The first line is wrapped in an implied [ErrorSummary], and subsequent |
| /// lines are wrapped in implied [ErrorDescription]s. Consider using the |
| /// [new FlutterError.fromParts] constructor to provide more detail, e.g. |
| /// using [ErrorHint]s or other [DiagnosticsNode]s. |
| factory FlutterError(String message) { |
| final List<String> lines = message.split('\n'); |
| return FlutterError.fromParts(<DiagnosticsNode>[ |
| ErrorSummary(lines.first), |
| ...lines.skip(1).map<DiagnosticsNode>((String line) => ErrorDescription(line)), |
| ]); |
| } |
| |
| /// Create an error message from a list of [DiagnosticsNode]s. |
| /// |
| /// By convention, there should be exactly one [ErrorSummary] in the list, |
| /// and it should be the first entry. |
| /// |
| /// Other entries are typically [ErrorDescription]s (for material that is |
| /// always applicable for this error) and [ErrorHint]s (for material that may |
| /// be sometimes useful, but may not always apply). Other [DiagnosticsNode] |
| /// subclasses, such as [DiagnosticsStackTrace], may |
| /// also be used. |
| FlutterError.fromParts(this.diagnostics) : assert(diagnostics.isNotEmpty, FlutterError.fromParts(<DiagnosticsNode>[ErrorSummary('Empty FlutterError')])) { |
| assert( |
| diagnostics.first.level == DiagnosticLevel.summary, |
| FlutterError.fromParts(<DiagnosticsNode>[ |
| ErrorSummary('FlutterError is missing a summary.'), |
| ErrorDescription( |
| 'All FlutterError objects should start with a short (one line) ' |
| 'summary description of the problem that was detected.' |
| ), |
| DiagnosticsProperty<FlutterError>('Malformed', this, expandableValue: true, showSeparator: false, style: DiagnosticsTreeStyle.whitespace), |
| ErrorDescription( |
| '\nThis error should still help you solve your problem, ' |
| 'however please also report this malformed error in the ' |
| 'framework by filing a bug on GitHub:\n' |
| ' https://github.com/flutter/flutter/issues/new?template=BUG.md' |
| ), |
| ], |
| )); |
| assert(() { |
| final Iterable<DiagnosticsNode> summaries = diagnostics.where((DiagnosticsNode node) => node.level == DiagnosticLevel.summary); |
| if (summaries.length > 1) { |
| final List<DiagnosticsNode> message = <DiagnosticsNode>[ |
| ErrorSummary('FlutterError contained multiple error summaries.'), |
| ErrorDescription( |
| 'All FlutterError objects should have only a single short ' |
| '(one line) summary description of the problem that was ' |
| 'detected.' |
| ), |
| DiagnosticsProperty<FlutterError>('Malformed', this, expandableValue: true, showSeparator: false, style: DiagnosticsTreeStyle.whitespace), |
| ErrorDescription('\nThe malformed error has ${summaries.length} summaries.'), |
| ]; |
| int i = 1; |
| for (final DiagnosticsNode summary in summaries) { |
| message.add(DiagnosticsProperty<DiagnosticsNode>('Summary $i', summary, expandableValue : true)); |
| i += 1; |
| } |
| message.add(ErrorDescription( |
| '\nThis error should still help you solve your problem, ' |
| 'however please also report this malformed error in the ' |
| 'framework by filing a bug on GitHub:\n' |
| ' https://github.com/flutter/flutter/issues/new?template=BUG.md' |
| )); |
| throw FlutterError.fromParts(message); |
| } |
| return true; |
| }()); |
| } |
| |
| /// The information associated with this error, in structured form. |
| /// |
| /// The first node is typically an [ErrorSummary] giving a short description |
| /// of the problem, suitable for an index of errors, a log, etc. |
| /// |
| /// Subsequent nodes should give information specific to this error. Typically |
| /// these will be [ErrorDescription]s or [ErrorHint]s, but they could be other |
| /// objects also. For instance, an error relating to a timer could include a |
| /// stack trace of when the timer was scheduled using the |
| /// [DiagnosticsStackTrace] class. |
| final List<DiagnosticsNode> diagnostics; |
| |
| /// The message associated with this error. |
| /// |
| /// This is generated by serializing the [diagnostics]. |
| @override |
| String get message => toString(); |
| |
| /// Called whenever the Flutter framework catches an error. |
| /// |
| /// The default behavior is to call [presentError]. |
| /// |
| /// You can set this to your own function to override this default behavior. |
| /// For example, you could report all errors to your server. |
| /// |
| /// If the error handler throws an exception, it will not be caught by the |
| /// Flutter framework. |
| /// |
| /// Set this to null to silently catch and ignore errors. This is not |
| /// recommended. |
| static FlutterExceptionHandler onError = (FlutterErrorDetails details) => presentError(details); |
| |
| /// Called whenever the Flutter framework wants to present an error to the |
| /// users. |
| /// |
| /// The default behavior is to call [dumpErrorToConsole]. |
| /// |
| /// Plugins can override how an error is to be presented to the user. For |
| /// example, the structured errors service extension sets its own method when |
| /// the extension is enabled. If you want to change how Flutter responds to an |
| /// error, use [onError] instead. |
| static FlutterExceptionHandler presentError = dumpErrorToConsole; |
| |
| static int _errorCount = 0; |
| |
| /// Resets the count of errors used by [dumpErrorToConsole] to decide whether |
| /// to show a complete error message or an abbreviated one. |
| /// |
| /// After this is called, the next error message will be shown in full. |
| static void resetErrorCount() { |
| _errorCount = 0; |
| } |
| |
| /// The width to which [dumpErrorToConsole] will wrap lines. |
| /// |
| /// This can be used to ensure strings will not exceed the length at which |
| /// they will wrap, e.g. when placing ASCII art diagrams in messages. |
| static const int wrapWidth = 100; |
| |
| /// Prints the given exception details to the console. |
| /// |
| /// The first time this is called, it dumps a very verbose message to the |
| /// console using [debugPrint]. |
| /// |
| /// Subsequent calls only dump the first line of the exception, unless |
| /// `forceReport` is set to true (in which case it dumps the verbose message). |
| /// |
| /// Call [resetErrorCount] to cause this method to go back to acting as if it |
| /// had not been called before (so the next message is verbose again). |
| /// |
| /// The default behavior for the [onError] handler is to call this function. |
| static void dumpErrorToConsole(FlutterErrorDetails details, { bool forceReport = false }) { |
| assert(details != null); |
| assert(details.exception != null); |
| bool reportError = details.silent != true; // could be null |
| assert(() { |
| // In checked mode, we ignore the "silent" flag. |
| reportError = true; |
| return true; |
| }()); |
| if (!reportError && !forceReport) |
| return; |
| if (_errorCount == 0 || forceReport) { |
| debugPrint( |
| TextTreeRenderer( |
| wrapWidth: wrapWidth, |
| wrapWidthProperties: wrapWidth, |
| maxDescendentsTruncatableNode: 5, |
| ).render(details.toDiagnosticsNode(style: DiagnosticsTreeStyle.error)).trimRight(), |
| ); |
| } else { |
| debugPrint('Another exception was thrown: ${details.summary}'); |
| } |
| _errorCount += 1; |
| } |
| |
| static final List<StackFilter> _stackFilters = <StackFilter>[]; |
| |
| /// Adds a stack filtering function to [defaultStackFilter]. |
| /// |
| /// For example, the framework adds common patterns of element building to |
| /// elide tree-walking patterns in the stacktrace. |
| /// |
| /// Added filters are checked in order of addition. The first matching filter |
| /// wins, and subsequent filters will not be checked. |
| static void addDefaultStackFilter(StackFilter filter) { |
| _stackFilters.add(filter); |
| } |
| |
| /// Converts a stack to a string that is more readable by omitting stack |
| /// frames that correspond to Dart internals. |
| /// |
| /// This is the default filter used by [dumpErrorToConsole] if the |
| /// [FlutterErrorDetails] object has no [FlutterErrorDetails.stackFilter] |
| /// callback. |
| /// |
| /// This function expects its input to be in the format used by |
| /// [StackTrace.toString()]. The output of this function is similar to that |
| /// format but the frame numbers will not be consecutive (frames are elided) |
| /// and the final line may be prose rather than a stack frame. |
| static Iterable<String> defaultStackFilter(Iterable<String> frames) { |
| final Map<String, int> removedPackagesAndClasses = <String, int>{ |
| 'dart:async-patch': 0, |
| 'dart:async': 0, |
| 'package:stack_trace': 0, |
| 'class _AssertionError': 0, |
| 'class _FakeAsync': 0, |
| 'class _FrameCallbackEntry': 0, |
| 'class _Timer': 0, |
| 'class _RawReceivePortImpl': 0, |
| }; |
| int skipped = 0; |
| |
| final List<StackFrame> parsedFrames = StackFrame.fromStackString(frames.join('\n')); |
| |
| for (int index = 0; index < parsedFrames.length; index += 1) { |
| final StackFrame frame = parsedFrames[index]; |
| final String className = 'class ${frame.className}'; |
| final String package = '${frame.packageScheme}:${frame.package}'; |
| if (removedPackagesAndClasses.containsKey(className)) { |
| skipped += 1; |
| removedPackagesAndClasses[className] += 1; |
| parsedFrames.removeAt(index); |
| index -= 1; |
| } else if (removedPackagesAndClasses.containsKey(package)) { |
| skipped += 1; |
| removedPackagesAndClasses[package] += 1; |
| parsedFrames.removeAt(index); |
| index -= 1; |
| } |
| } |
| final List<String> reasons = List<String>(parsedFrames.length); |
| for (final StackFilter filter in _stackFilters) { |
| filter.filter(parsedFrames, reasons); |
| } |
| |
| final List<String> result = <String>[]; |
| |
| // Collapse duplicated reasons. |
| for (int index = 0; index < parsedFrames.length; index += 1) { |
| final int start = index; |
| while (index < reasons.length - 1 && reasons[index] != null && reasons[index + 1] == reasons[index]) { |
| index++; |
| } |
| String suffix = ''; |
| if (reasons[index] != null) { |
| if (index != start) { |
| suffix = ' (${index - start + 2} frames)'; |
| } else { |
| suffix = ' (1 frame)'; |
| } |
| } |
| final String resultLine = '${reasons[index] ?? parsedFrames[index].source}$suffix'; |
| result.add(resultLine); |
| } |
| |
| // Only include packages we actually elided from. |
| final List<String> where = <String>[ |
| for (MapEntry<String, int> entry in removedPackagesAndClasses.entries) |
| if (entry.value > 0) |
| entry.key |
| ]..sort(); |
| if (skipped == 1) { |
| result.add('(elided one frame from ${where.single})'); |
| } else if (skipped > 1) { |
| if (where.length > 1) |
| where[where.length - 1] = 'and ${where.last}'; |
| if (where.length > 2) { |
| result.add('(elided $skipped frames from ${where.join(", ")})'); |
| } else { |
| result.add('(elided $skipped frames from ${where.join(" ")})'); |
| } |
| } |
| return result; |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| diagnostics?.forEach(properties.add); |
| } |
| |
| @override |
| String toStringShort() => 'FlutterError'; |
| |
| @override |
| String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { |
| if (kReleaseMode) { |
| final Iterable<_ErrorDiagnostic> errors = diagnostics.whereType<_ErrorDiagnostic>(); |
| return errors.isNotEmpty ? errors.first.valueToString() : toStringShort(); |
| } |
| // Avoid wrapping lines. |
| final TextTreeRenderer renderer = TextTreeRenderer(wrapWidth: 4000000000); |
| return diagnostics.map((DiagnosticsNode node) => renderer.render(node).trimRight()).join('\n'); |
| } |
| |
| /// Calls [onError] with the given details, unless it is null. |
| static void reportError(FlutterErrorDetails details) { |
| assert(details != null); |
| assert(details.exception != null); |
| if (onError != null) |
| onError(details); |
| } |
| } |
| |
| /// Dump the stack to the console using [debugPrint] and |
| /// [FlutterError.defaultStackFilter]. |
| /// |
| /// If the `stackTrace` parameter is null, the [StackTrace.current] is used to |
| /// obtain the stack. |
| /// |
| /// The `maxFrames` argument can be given to limit the stack to the given number |
| /// of lines before filtering is applied. By default, all stack lines are |
| /// included. |
| /// |
| /// The `label` argument, if present, will be printed before the stack. |
| void debugPrintStack({StackTrace stackTrace, String label, int maxFrames}) { |
| if (label != null) |
| debugPrint(label); |
| stackTrace ??= StackTrace.current; |
| Iterable<String> lines = stackTrace.toString().trimRight().split('\n'); |
| if (kIsWeb && lines.isNotEmpty) { |
| // Remove extra call to StackTrace.current for web platform. |
| // TODO(ferhat): remove when https://github.com/flutter/flutter/issues/37635 |
| // is addressed. |
| lines = lines.skipWhile((String line) { |
| return line.contains('StackTrace.current') || |
| line.contains('dart-sdk/lib/_internal') || |
| line.contains('dart:sdk_internal'); |
| }); |
| } |
| if (maxFrames != null) |
| lines = lines.take(maxFrames); |
| debugPrint(FlutterError.defaultStackFilter(lines).join('\n')); |
| } |
| |
| /// Diagnostic with a [StackTrace] [value] suitable for displaying stack traces |
| /// as part of a [FlutterError] object. |
| class DiagnosticsStackTrace extends DiagnosticsBlock { |
| /// Creates a diagnostic for a stack trace. |
| /// |
| /// [name] describes a name the stacktrace is given, e.g. |
| /// `When the exception was thrown, this was the stack`. |
| /// [stackFilter] provides an optional filter to use to filter which frames |
| /// are included. If no filter is specified, [FlutterError.defaultStackFilter] |
| /// is used. |
| /// [showSeparator] indicates whether to include a ':' after the [name]. |
| DiagnosticsStackTrace( |
| String name, |
| StackTrace stack, { |
| IterableFilter<String> stackFilter, |
| bool showSeparator = true, |
| }) : super( |
| name: name, |
| value: stack, |
| properties: stack == null |
| ? <DiagnosticsNode>[] |
| : (stackFilter ?? FlutterError.defaultStackFilter)(stack.toString().trimRight().split('\n')) |
| .map<DiagnosticsNode>(_createStackFrame) |
| .toList(), |
| style: DiagnosticsTreeStyle.flat, |
| showSeparator: showSeparator, |
| allowTruncate: true, |
| ); |
| |
| /// Creates a diagnostic describing a single frame from a StackTrace. |
| DiagnosticsStackTrace.singleFrame( |
| String name, { |
| @required String frame, |
| bool showSeparator = true, |
| }) : super( |
| name: name, |
| properties: <DiagnosticsNode>[_createStackFrame(frame)], |
| style: DiagnosticsTreeStyle.whitespace, |
| showSeparator: showSeparator, |
| ); |
| |
| static DiagnosticsNode _createStackFrame(String frame) { |
| return DiagnosticsNode.message(frame, allowWrap: false); |
| } |
| } |
| |
| class _FlutterErrorDetailsNode extends DiagnosticableNode<FlutterErrorDetails> { |
| _FlutterErrorDetailsNode({ |
| String name, |
| @required FlutterErrorDetails value, |
| @required DiagnosticsTreeStyle style, |
| }) : super( |
| name: name, |
| value: value, |
| style: style, |
| ); |
| |
| @override |
| DiagnosticPropertiesBuilder get builder { |
| final DiagnosticPropertiesBuilder builder = super.builder; |
| if (builder == null){ |
| return null; |
| } |
| Iterable<DiagnosticsNode> properties = builder.properties; |
| for (final DiagnosticPropertiesTransformer transformer in FlutterErrorDetails.propertiesTransformers) { |
| properties = transformer(properties); |
| } |
| return DiagnosticPropertiesBuilder.fromProperties(properties.toList()); |
| } |
| } |