assertions.dart 17.8 KB
Newer Older
1 2 3 4
// 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.

5
import 'basic_types.dart';
6 7
import 'print.dart';

8
/// Signature for [FlutterError.onError] handler.
9 10
typedef void FlutterExceptionHandler(FlutterErrorDetails details);

11 12 13
/// Signature for [FlutterErrorDetails.informationCollector] callback
/// and other callbacks that collect information into a string buffer.
typedef void InformationCollector(StringBuffer information);
14 15 16 17 18 19 20 21 22 23

/// 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].
24 25 26 27
  ///
  /// The [exception] must not be null; other arguments can be left to
  /// their default values. (`throw null` results in a
  /// [NullThrownError] exception.)
28 29 30 31 32
  const FlutterErrorDetails({
    this.exception,
    this.stack,
    this.library: 'Flutter framework',
    this.context,
33
    this.stackFilter,
34 35
    this.informationCollector,
    this.silent: false
36
  });
37 38 39 40 41 42 43 44

  /// 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).
  ///
45 46 47
  /// StackTrace objects are opaque except for their [toString] function.
  ///
  /// If this field is not null, then the [stackFilter] callback, if any, will
48
  /// be called with the result of calling [toString] on this object and
49 50 51 52
  /// 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].
53 54 55 56 57 58 59 60 61 62 63
  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;

64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
  /// 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;

80
  /// A callback which, when called with a [StringBuffer] will write to that buffer
81 82 83
  /// information that could help with debugging the problem.
  ///
  /// Information collector callbacks can be expensive, so the generated information
84
  /// should be cached, rather than the callback being called multiple times.
85 86 87 88
  ///
  /// The text written to the information argument may contain newlines but should
  /// not end with a newline.
  final InformationCollector informationCollector;
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104

  /// 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;
105 106 107

  /// Converts the [exception] to a string.
  ///
108 109 110
  /// 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.
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
  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;
  }
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174

  @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();
  }
175 176
}

177 178
/// Error class used to report Flutter-specific assertion failures and
/// contract violations.
179
class FlutterError extends AssertionError {
180 181 182 183 184 185 186 187
  /// 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.
188
  FlutterError(String message) : super(message);
189

190 191
  /// The message associated with this error.
  ///
192 193 194 195 196 197 198 199 200 201
  /// 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.
202 203 204
  ///
  /// All sentences in the error should be correctly punctuated (i.e.,
  /// do end the error message with a period).
205
  @override
206
  String get message => super.message;
207 208

  @override
209
  String toString() => message;
210 211 212

  /// Called whenever the Flutter framework catches an error.
  ///
213
  /// The default behavior is to call [dumpErrorToConsole].
214 215 216 217 218 219 220 221 222 223 224 225 226
  ///
  /// 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;

227 228 229 230 231 232 233 234
  /// 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;
  }

235 236 237 238 239
  /// 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;
240

241 242 243 244 245
  /// 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].
  ///
246 247
  /// Subsequent calls only dump the first line of the exception, unless
  /// `forceReport` is set to true (in which case it dumps the verbose message).
248
  ///
249 250 251
  /// 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).
  ///
252 253
  /// The default behavior for the [onError] handler is to call this function.
  static void dumpErrorToConsole(FlutterErrorDetails details, { bool forceReport: false }) {
254 255
    assert(details != null);
    assert(details.exception != null);
256
    bool reportError = details.silent != true; // could be null
257 258 259 260
    assert(() {
      // In checked mode, we ignore the "silent" flag.
      reportError = true;
      return true;
261
    }());
262
    if (!reportError && !forceReport)
263
      return;
264
    if (_errorCount == 0 || forceReport) {
265
      final String header = '\u2550\u2550\u2561 EXCEPTION CAUGHT BY ${details.library} \u255E'.toUpperCase();
266
      final String footer = '\u2550' * wrapWidth;
267
      debugPrint('$header${"\u2550" * (footer.length - header.length)}');
268 269
      final String verb = 'thrown${ details.context != null ? " ${details.context}" : ""}';
      if (details.exception is NullThrownError) {
270
        debugPrint('The null value was $verb.', wrapWidth: wrapWidth);
271
      } else if (details.exception is num) {
272
        debugPrint('The number ${details.exception} was $verb.', wrapWidth: wrapWidth);
273 274 275 276 277 278 279 280 281 282 283
      } 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';
        }
284 285 286 287 288 289 290
        // 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);
291
        debugPrint('The following $errorName was $verb:\n$message', wrapWidth: wrapWidth);
292
      }
293
      Iterable<String> stackLines = (details.stack != null) ? details.stack.toString().trimRight().split('\n') : null;
294
      if ((details.exception is AssertionError) && (details.exception is! FlutterError)) {
295 296
        bool ourFault = true;
        if (stackLines != null) {
297
          final List<String> stackList = stackLines.take(2).toList();
298
          if (stackList.length >= 2) {
299
            // TODO(ianh): This has bitrotted and is no longer matching. https://github.com/flutter/flutter/issues/4021
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
            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 '
315 316
                     'and fix the underlying cause.', wrapWidth: wrapWidth);
          debugPrint('In either case, please report this assertion by filing a bug on GitHub:', wrapWidth: wrapWidth);
317 318
          debugPrint('  https://github.com/flutter/flutter/issues/new');
        }
319 320
      }
      if (details.stack != null) {
321
        debugPrint('\nWhen the exception was thrown, this was the stack:', wrapWidth: wrapWidth);
322 323 324 325 326
        if (details.stackFilter != null) {
          stackLines = details.stackFilter(stackLines);
        } else {
          stackLines = defaultStackFilter(stackLines);
        }
327
        for (String line in stackLines)
328
          debugPrint(line, wrapWidth: wrapWidth);
329 330
      }
      if (details.informationCollector != null) {
331
        final StringBuffer information = new StringBuffer();
332
        details.informationCollector(information);
333
        debugPrint('\n${information.toString().trimRight()}', wrapWidth: wrapWidth);
334
      }
335
      debugPrint(footer);
336
    } else {
337
      debugPrint('Another exception was thrown: ${details.exceptionAsString().split("\n")[0].trimLeft()}');
338 339 340 341
    }
    _errorCount += 1;
  }

342 343 344 345 346 347 348 349 350 351 352 353
  /// 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) {
354
    const List<String> filteredPackages = const <String>[
355 356 357 358
      'dart:async-patch',
      'dart:async',
      'package:stack_trace',
    ];
359 360
    const List<String> filteredClasses = const <String>[
      '_AssertionError',
361
      '_FakeAsync',
362
      '_FrameCallbackEntry',
363
    ];
364
    final RegExp stackParser = new RegExp(r'^#[0-9]+ +([^.]+).* \(([^/\\]*)[/\\].+:[0-9]+(?::[0-9]+)?\)$');
365
    final RegExp packageParser = new RegExp(r'^([^:]+):(.+)$');
366
    final List<String> result = <String>[];
367 368
    final List<String> skipped = <String>[];
    for (String line in frames) {
369
      final Match match = stackParser.firstMatch(line);
370 371 372
      if (match != null) {
        assert(match.groupCount == 2);
        if (filteredPackages.contains(match.group(2))) {
373
          final Match packageMatch = packageParser.firstMatch(match.group(2));
374 375 376 377 378 379 380 381 382 383 384 385 386 387
          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);
    }
388
    if (skipped.length == 1) {
389 390
      result.add('(elided one frame from ${skipped.single})');
    } else if (skipped.length > 1) {
391
      final List<String> where = new Set<String>.from(skipped).toList()..sort();
392 393 394 395 396 397 398 399 400 401 402
      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;
  }

403 404 405 406 407 408 409
  /// 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);
  }
410
}
411 412 413 414 415 416 417 418

/// 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.
419 420 421 422 423
///
/// The `label` argument, if present, will be printed before the stack.
void debugPrintStack({ String label, int maxFrames }) {
  if (label != null)
    debugPrint(label);
424
  Iterable<String> lines = StackTrace.current.toString().trimRight().split('\n');
425 426 427 428
  if (maxFrames != null)
    lines = lines.take(maxFrames);
  debugPrint(FlutterError.defaultStackFilter(lines).join('\n'));
}