assertions.dart 14.4 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 8 9 10
import 'print.dart';

/// Signature for [FlutterError.onException] handler.
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 36 37 38 39 40 41 42 43 44
    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).
  ///
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 105 106

  /// 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;
}

107 108
/// Error class used to report Flutter-specific assertion failures and
/// contract violations.
109
class FlutterError extends AssertionError {
110 111 112 113 114 115 116 117
  /// 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.
118
  FlutterError(this.message);
119

120 121
  /// The message associated with this error.
  ///
122 123 124 125 126 127 128 129 130 131
  /// 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.
132 133 134
  ///
  /// All sentences in the error should be correctly punctuated (i.e.,
  /// do end the error message with a period).
135
  @override
136
  final String message;
137 138

  @override
139
  String toString() => message;
140 141 142

  /// Called whenever the Flutter framework catches an error.
  ///
143
  /// The default behavior is to call [dumpErrorToConsole].
144 145 146 147 148 149 150 151 152 153 154 155 156
  ///
  /// 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;

157 158 159 160 161 162 163 164
  /// 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;
  }

165
  static const int _kWrapWidth = 100;
166

167 168 169 170 171
  /// 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].
  ///
172 173
  /// Subsequent calls only dump the first line of the exception, unless
  /// `forceReport` is set to true (in which case it dumps the verbose message).
174
  ///
175 176 177
  /// 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).
  ///
178 179
  /// The default behavior for the [onError] handler is to call this function.
  static void dumpErrorToConsole(FlutterErrorDetails details, { bool forceReport: false }) {
180 181
    assert(details != null);
    assert(details.exception != null);
182
    bool reportError = details.silent != true; // could be null
183 184 185 186 187
    assert(() {
      // In checked mode, we ignore the "silent" flag.
      reportError = true;
      return true;
    });
188
    if (!reportError && !forceReport)
189
      return;
190
    if (_errorCount == 0 || forceReport) {
191
      final String header = '\u2550\u2550\u2561 EXCEPTION CAUGHT BY ${details.library} \u255E'.toUpperCase();
192
      final String footer = '\u2550' * _kWrapWidth;
193
      debugPrint('$header${"\u2550" * (footer.length - header.length)}');
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
      final String verb = 'thrown${ details.context != null ? " ${details.context}" : ""}';
      if (details.exception is NullThrownError) {
        debugPrint('The null value was $verb.', wrapWidth: _kWrapWidth);
      } else if (details.exception is num) {
        debugPrint('The number ${details.exception} was $verb.', wrapWidth: _kWrapWidth);
      } 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';
        }
        debugPrint('The following $errorName was $verb:', wrapWidth: _kWrapWidth);
        debugPrint('${details.exception}', wrapWidth: _kWrapWidth);
      }
213
      Iterable<String> stackLines = (details.stack != null) ? details.stack.toString().trimRight().split('\n') : null;
214
      if ((details.exception is AssertionError) && (details.exception is! FlutterError)) {
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
        bool ourFault = true;
        if (stackLines != null) {
          List<String> stackList = stackLines.take(2).toList();
          if (stackList.length >= 2) {
            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: _kWrapWidth);
          debugPrint('In either case, please report this assertion by filing a bug on GitHub:', wrapWidth: _kWrapWidth);
          debugPrint('  https://github.com/flutter/flutter/issues/new');
        }
238 239
      }
      if (details.stack != null) {
240
        debugPrint('\nWhen the exception was thrown, this was the stack:', wrapWidth: _kWrapWidth);
241 242 243 244 245
        if (details.stackFilter != null) {
          stackLines = details.stackFilter(stackLines);
        } else {
          stackLines = defaultStackFilter(stackLines);
        }
246 247 248 249 250 251 252
        for (String line in stackLines)
          debugPrint(line, wrapWidth: _kWrapWidth);
      }
      if (details.informationCollector != null) {
        StringBuffer information = new StringBuffer();
        details.informationCollector(information);
        debugPrint('\n$information', wrapWidth: _kWrapWidth);
253
      }
254
      debugPrint(footer);
255
    } else {
256
      debugPrint('Another exception was thrown: ${details.exception.toString().split("\n")[0]}');
257 258 259 260
    }
    _errorCount += 1;
  }

261 262 263 264 265 266 267 268 269 270 271 272
  /// 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) {
273
    const List<String> filteredPackages = const <String>[
274 275 276 277
      'dart:async-patch',
      'dart:async',
      'package:stack_trace',
    ];
278 279
    const List<String> filteredClasses = const <String>[
      '_AssertionError',
280
      '_FakeAsync',
281
      '_FrameCallbackEntry',
282 283 284
    ];
    final RegExp stackParser = new RegExp(r'^#[0-9]+ +([^.]+).* \(([^/]*)/[^:]+:[0-9]+(?::[0-9]+)?\)$');
    final RegExp packageParser = new RegExp(r'^([^:]+):(.+)$');
285
    final List<String> result = <String>[];
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
    final List<String> skipped = <String>[];
    for (String line in frames) {
      Match match = stackParser.firstMatch(line);
      if (match != null) {
        assert(match.groupCount == 2);
        if (filteredPackages.contains(match.group(2))) {
          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);
    }
307
    if (skipped.length == 1) {
308 309 310 311 312 313 314 315 316 317 318 319 320 321
      result.add('(elided one frame from ${skipped.single})');
    } else if (skipped.length > 1) {
      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;
  }

322 323 324 325 326 327 328
  /// 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);
  }
329
}
330 331 332 333 334 335 336 337

/// 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.
338 339 340 341 342
///
/// The `label` argument, if present, will be printed before the stack.
void debugPrintStack({ String label, int maxFrames }) {
  if (label != null)
    debugPrint(label);
343
  Iterable<String> lines = StackTrace.current.toString().trimRight().split('\n');
344 345 346 347
  if (maxFrames != null)
    lines = lines.take(maxFrames);
  debugPrint(FlutterError.defaultStackFilter(lines).join('\n'));
}