| // Copyright 2016 The Chromium 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 'basic_types.dart'; |
| import 'print.dart'; |
| |
| /// Signature for [FlutterError.onError] handler. |
| typedef void FlutterExceptionHandler(FlutterErrorDetails details); |
| |
| /// Signature for [FlutterErrorDetails.informationCollector] callback |
| /// and other callbacks that collect information into a string buffer. |
| typedef void InformationCollector(StringBuffer information); |
| |
| /// Class for information provided to [FlutterExceptionHandler] callbacks. |
| /// |
| /// See [FlutterError.onError]. |
| class FlutterErrorDetails { |
| /// 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 |
| }); |
| |
| /// 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). |
| final String 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 recognise that format is being used. |
| final String 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) == ': ') { |
| longMessage = '${message.trimRight()}\n${fullMessage.substring(0, position - 2)}'; |
| } |
| } |
| } |
| longMessage ??= fullMessage; |
| } else if (exception is String) { |
| longMessage = exception; |
| } 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; |
| } |
| |
| @override |
| String toString() { |
| final StringBuffer buffer = new StringBuffer(); |
| if ((library != null && library != '') || (context != null && context != '')) { |
| if (library != null && library != '') { |
| buffer.write('Error caught by $library'); |
| if (context != null && context != '') |
| buffer.write(', '); |
| } else { |
| buffer.writeln('Exception '); |
| } |
| if (context != null && context != '') |
| buffer.write('thrown $context'); |
| buffer.writeln('.'); |
| } else { |
| buffer.write('An error was caught.'); |
| } |
| buffer.writeln(exceptionAsString()); |
| if (informationCollector != null) |
| informationCollector(buffer); |
| if (stack != null) { |
| Iterable<String> stackLines = stack.toString().trimRight().split('\n'); |
| if (stackFilter != null) { |
| stackLines = stackFilter(stackLines); |
| } else { |
| stackLines = FlutterError.defaultStackFilter(stackLines); |
| } |
| buffer.writeAll(stackLines, '\n'); |
| } |
| return buffer.toString().trimRight(); |
| } |
| } |
| |
| /// Error class used to report Flutter-specific assertion failures and |
| /// contract violations. |
| class FlutterError extends AssertionError { |
| /// Creates a [FlutterError]. |
| /// |
| /// See [message] for details on the format that the message should |
| /// take. |
| /// |
| /// Include as much detail as possible in the full error message, |
| /// including specifics about the state of the app that might be |
| /// relevant to debugging the error. |
| FlutterError(String message) : super(message); |
| |
| /// The message associated with this error. |
| /// |
| /// 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). |
| @override |
| String get message => super.message; |
| |
| @override |
| String toString() => message; |
| |
| /// Called whenever the Flutter framework catches an error. |
| /// |
| /// The default behavior is to call [dumpErrorToConsole]. |
| /// |
| /// 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 = 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) { |
| final String header = '\u2550\u2550\u2561 EXCEPTION CAUGHT BY ${details.library} \u255E'.toUpperCase(); |
| final String footer = '\u2550' * wrapWidth; |
| debugPrint('$header${"\u2550" * (footer.length - header.length)}'); |
| final String verb = 'thrown${ details.context != null ? " ${details.context}" : ""}'; |
| if (details.exception is NullThrownError) { |
| debugPrint('The null value was $verb.', wrapWidth: wrapWidth); |
| } else if (details.exception is num) { |
| debugPrint('The number ${details.exception} was $verb.', wrapWidth: wrapWidth); |
| } else { |
| String errorName; |
| if (details.exception is AssertionError) { |
| errorName = 'assertion'; |
| } else if (details.exception is String) { |
| errorName = 'message'; |
| } else if (details.exception is Error || details.exception is Exception) { |
| errorName = '${details.exception.runtimeType}'; |
| } else { |
| errorName = '${details.exception.runtimeType} object'; |
| } |
| // 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 = '${details.exception.runtimeType}: '; |
| String message = details.exceptionAsString(); |
| if (message.startsWith(prefix)) |
| message = message.substring(prefix.length); |
| debugPrint('The following $errorName was $verb:\n$message', wrapWidth: wrapWidth); |
| } |
| Iterable<String> stackLines = (details.stack != null) ? details.stack.toString().trimRight().split('\n') : null; |
| if ((details.exception is AssertionError) && (details.exception is! FlutterError)) { |
| bool ourFault = true; |
| if (stackLines != null) { |
| final List<String> stackList = stackLines.take(2).toList(); |
| if (stackList.length >= 2) { |
| // TODO(ianh): This has bitrotted and is no longer matching. https://github.com/flutter/flutter/issues/4021 |
| final RegExp throwPattern = new RegExp(r'^#0 +_AssertionError._throwNew \(dart:.+\)$'); |
| final RegExp assertPattern = new RegExp(r'^#1 +[^(]+ \((.+?):([0-9]+)(?::[0-9]+)?\)$'); |
| if (throwPattern.hasMatch(stackList[0])) { |
| final Match assertMatch = assertPattern.firstMatch(stackList[1]); |
| if (assertMatch != null) { |
| assert(assertMatch.groupCount == 2); |
| final RegExp ourLibraryPattern = new RegExp(r'^package:flutter/'); |
| ourFault = ourLibraryPattern.hasMatch(assertMatch.group(1)); |
| } |
| } |
| } |
| } |
| if (ourFault) { |
| debugPrint('\nEither 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.', wrapWidth: wrapWidth); |
| debugPrint('In either case, please report this assertion by filing a bug on GitHub:', wrapWidth: wrapWidth); |
| debugPrint(' https://github.com/flutter/flutter/issues/new'); |
| } |
| } |
| if (details.stack != null) { |
| debugPrint('\nWhen the exception was thrown, this was the stack:', wrapWidth: wrapWidth); |
| if (details.stackFilter != null) { |
| stackLines = details.stackFilter(stackLines); |
| } else { |
| stackLines = defaultStackFilter(stackLines); |
| } |
| for (String line in stackLines) |
| debugPrint(line, wrapWidth: wrapWidth); |
| } |
| if (details.informationCollector != null) { |
| final StringBuffer information = new StringBuffer(); |
| details.informationCollector(information); |
| debugPrint('\n${information.toString().trimRight()}', wrapWidth: wrapWidth); |
| } |
| debugPrint(footer); |
| } else { |
| debugPrint('Another exception was thrown: ${details.exceptionAsString().split("\n")[0].trimLeft()}'); |
| } |
| _errorCount += 1; |
| } |
| |
| /// 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) { |
| const List<String> filteredPackages = const <String>[ |
| 'dart:async-patch', |
| 'dart:async', |
| 'package:stack_trace', |
| ]; |
| const List<String> filteredClasses = const <String>[ |
| '_AssertionError', |
| '_FakeAsync', |
| '_FrameCallbackEntry', |
| ]; |
| final RegExp stackParser = new RegExp(r'^#[0-9]+ +([^.]+).* \(([^/\\]*)[/\\].+:[0-9]+(?::[0-9]+)?\)$'); |
| final RegExp packageParser = new RegExp(r'^([^:]+):(.+)$'); |
| final List<String> result = <String>[]; |
| final List<String> skipped = <String>[]; |
| for (String line in frames) { |
| final Match match = stackParser.firstMatch(line); |
| if (match != null) { |
| assert(match.groupCount == 2); |
| if (filteredPackages.contains(match.group(2))) { |
| final Match packageMatch = packageParser.firstMatch(match.group(2)); |
| if (packageMatch != null && packageMatch.group(1) == 'package') { |
| skipped.add('package ${packageMatch.group(2)}'); // avoid "package package:foo" |
| } else { |
| skipped.add('package ${match.group(2)}'); |
| } |
| continue; |
| } |
| if (filteredClasses.contains(match.group(1))) { |
| skipped.add('class ${match.group(1)}'); |
| continue; |
| } |
| } |
| result.add(line); |
| } |
| if (skipped.length == 1) { |
| result.add('(elided one frame from ${skipped.single})'); |
| } else if (skipped.length > 1) { |
| final List<String> where = new Set<String>.from(skipped).toList()..sort(); |
| if (where.length > 1) |
| where[where.length - 1] = 'and ${where.last}'; |
| if (where.length > 2) { |
| result.add('(elided ${skipped.length} frames from ${where.join(", ")})'); |
| } else { |
| result.add('(elided ${skipped.length} frames from ${where.join(" ")})'); |
| } |
| } |
| return result; |
| } |
| |
| /// 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 current stack to the console using [debugPrint] and |
| /// [FlutterError.defaultStackFilter]. |
| /// |
| /// The current stack is obtained using [StackTrace.current]. |
| /// |
| /// The `maxFrames` argument can be given to limit the stack to the given number |
| /// of lines. By default, all non-filtered stack lines are shown. |
| /// |
| /// The `label` argument, if present, will be printed before the stack. |
| void debugPrintStack({ String label, int maxFrames }) { |
| if (label != null) |
| debugPrint(label); |
| Iterable<String> lines = StackTrace.current.toString().trimRight().split('\n'); |
| if (maxFrames != null) |
| lines = lines.take(maxFrames); |
| debugPrint(FlutterError.defaultStackFilter(lines).join('\n')); |
| } |