stack_frame.dart 10.4 KB
Newer Older
1 2 3 4 5 6 7 8
// 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 'dart:ui' show hashValues;

import 'package:meta/meta.dart';

9
import 'constants.dart';
10 11
import 'object.dart';

12 13
/// A object representation of a frame from a stack trace.
///
14
/// {@tool snippet}
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
///
/// This example creates a traversable list of parsed [StackFrame] objects from
/// the current [StackTrace].
///
/// ```dart
/// final List<StackFrame> currentFrames = StackFrame.fromStackTrace(StackTrace.current);
/// ```
/// {@end-tool}
@immutable
class StackFrame {
  /// Creates a new StackFrame instance.
  ///
  /// All parameters must not be null. The [className] may be the empty string
  /// if there is no class (e.g. for a top level library method).
  const StackFrame({
30 31 32 33 34 35
    required this.number,
    required this.column,
    required this.line,
    required this.packageScheme,
    required this.package,
    required this.packagePath,
36
    this.className = '',
37
    required this.method,
38
    this.isConstructor = false,
39
    required this.source,
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
  })  : assert(number != null),
        assert(column != null),
        assert(line != null),
        assert(method != null),
        assert(packageScheme != null),
        assert(package != null),
        assert(packagePath != null),
        assert(className != null),
        assert(isConstructor != null),
        assert(source != null);

  /// A stack frame representing an asynchronous suspension.
  static const StackFrame asynchronousSuspension = StackFrame(
    number: -1,
    column: -1,
    line: -1,
    method: 'asynchronous suspension',
    packageScheme: '',
    package: '',
    packagePath: '',
    source: '<asynchronous suspension>',
  );

63 64 65 66 67 68 69 70 71 72 73 74
  /// A stack frame representing a Dart elided stack overflow frame.
  static const StackFrame stackOverFlowElision = StackFrame(
    number: -1,
    column: -1,
    line: -1,
    method: '...',
    packageScheme: '',
    package: '',
    packagePath: '',
    source: '...',
  );

75 76 77 78 79 80 81 82 83 84 85 86 87 88
  /// Parses a list of [StackFrame]s from a [StackTrace] object.
  ///
  /// This is normally useful with [StackTrace.current].
  static List<StackFrame> fromStackTrace(StackTrace stack) {
    assert(stack != null);
    return fromStackString(stack.toString());
  }

  /// Parses a list of [StackFrame]s from the [StackTrace.toString] method.
  static List<StackFrame> fromStackString(String stack) {
    assert(stack != null);
    return stack
        .trim()
        .split('\n')
89
        .where((String line) => line.isNotEmpty)
90
        .map(fromStackTraceLine)
91 92 93
        // On the Web in non-debug builds the stack trace includes the exception
        // message that precedes the stack trace itself. fromStackTraceLine will
        // return null in that case. We will skip it here.
94
        .whereType<StackFrame>()
95 96 97
        .toList();
  }

98
  static StackFrame? _parseWebFrame(String line) {
99 100 101 102 103 104 105 106
    if (kDebugMode) {
      return _parseWebDebugFrame(line);
    } else {
      return _parseWebNonDebugFrame(line);
    }
  }

  static StackFrame _parseWebDebugFrame(String line) {
107 108
    // This RegExp is only partially correct for flutter run/test differences.
    // https://github.com/flutter/flutter/issues/52685
109 110
    final bool hasPackage = line.startsWith('package');
    final RegExp parser = hasPackage
111
        ? RegExp(r'^(package.+) (\d+):(\d+)\s+(.+)$')
112
        : RegExp(r'^(.+) (\d+):(\d+)\s+(.+)$');
113
    Match? match = parser.firstMatch(line);
Dan Field's avatar
Dan Field committed
114
    assert(match != null, 'Expected $line to match $parser.');
115
    match = match!;
116 117 118 119 120 121

    String package = '<unknown>';
    String packageScheme = '<unknown>';
    String packagePath = '<unknown>';
    if (hasPackage) {
      packageScheme = 'package';
122
      final Uri packageUri = Uri.parse(match.group(1)!);
123
      package = packageUri.pathSegments[0];
124
      packagePath = packageUri.path.replaceFirst('${packageUri.pathSegments[0]}/', '');
125 126 127 128 129 130 131
    }

    return StackFrame(
      number: -1,
      packageScheme: packageScheme,
      package: package,
      packagePath: packagePath,
132 133
      line: int.parse(match.group(2)!),
      column: int.parse(match.group(3)!),
134
      className: '<unknown>',
135
      method: match.group(4)!,
136 137 138 139
      source: line,
    );
  }

140 141 142 143 144 145 146 147
  // Non-debug builds do not point to dart code but compiled JavaScript, so
  // line numbers are meaningless. We only attempt to parse the class and
  // method name, which is more or less readable in profile builds, and
  // minified in release builds.
  static final RegExp _webNonDebugFramePattern = RegExp(r'^\s*at ([^\s]+).*$');

  // Parses `line` as a stack frame in profile and release Web builds. If not
  // recognized as a stack frame, returns null.
148 149
  static StackFrame? _parseWebNonDebugFrame(String line) {
    final Match? match = _webNonDebugFramePattern.firstMatch(line);
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
    if (match == null) {
      // On the Web in non-debug builds the stack trace includes the exception
      // message that precedes the stack trace itself. Example:
      //
      // TypeError: Cannot read property 'hello$0' of null
      //    at _GalleryAppState.build$1 (http://localhost:8080/main.dart.js:149790:13)
      //    at StatefulElement.build$0 (http://localhost:8080/main.dart.js:129138:37)
      //    at StatefulElement.performRebuild$0 (http://localhost:8080/main.dart.js:129032:23)
      //
      // Instead of crashing when a line is not recognized as a stack frame, we
      // return null. The caller, such as fromStackString, can then just skip
      // this frame.
      return null;
    }

165
    final List<String> classAndMethod = match.group(1)!.split('.');
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
    final String className = classAndMethod.length > 1 ? classAndMethod.first : '<unknown>';
    final String method = classAndMethod.length > 1
      ? classAndMethod.skip(1).join('.')
      : classAndMethod.single;

    return StackFrame(
      number: -1,
      packageScheme: '<unknown>',
      package: '<unknown>',
      packagePath: '<unknown>',
      line: -1,
      column: -1,
      className: className,
      method: method,
      source: line,
    );
  }

184
  /// Parses a single [StackFrame] from a single line of a [StackTrace].
185
  static StackFrame? fromStackTraceLine(String line) {
186 187 188
    assert(line != null);
    if (line == '<asynchronous suspension>') {
      return asynchronousSuspension;
189 190
    } else if (line == '...') {
      return stackOverFlowElision;
191 192
    }

193 194 195 196
    assert(
      line != '===== asynchronous gap ===========================',
      'Got a stack frame from package:stack_trace, where a vm or web frame was expected. '
      'This can happen if FlutterError.demangleStackTrace was not set in an environment '
197
      'that propagates non-standard stack traces to the framework, such as during tests.',
198 199
    );

200 201 202 203 204 205
    // Web frames.
    if (!line.startsWith('#')) {
      return _parseWebFrame(line);
    }

    final RegExp parser = RegExp(r'^#(\d+) +(.+) \((.+?):?(\d+){0,1}:?(\d+){0,1}\)$');
206
    Match? match = parser.firstMatch(line);
207
    assert(match != null, 'Expected $line to match $parser.');
208
    match = match!;
209 210 211

    bool isConstructor = false;
    String className = '';
212
    String method = match.group(2)!.replaceAll('.<anonymous closure>', '');
213
    if (method.startsWith('new')) {
214 215 216
      final List<String> methodParts = method.split(' ');
      // Sometimes a web frame will only read "new" and have no class name.
      className = methodParts.length > 1 ? method.split(' ')[1] : '<unknown>';
217 218 219 220 221 222 223 224 225 226 227 228 229
      method = '';
      if (className.contains('.')) {
        final List<String> parts  = className.split('.');
        className = parts[0];
        method = parts[1];
      }
      isConstructor = true;
    } else if (method.contains('.')) {
      final List<String> parts = method.split('.');
      className = parts[0];
      method = parts[1];
    }

230
    final Uri packageUri = Uri.parse(match.group(3)!);
231 232 233 234
    String package = '<unknown>';
    String packagePath = packageUri.path;
    if (packageUri.scheme == 'dart' || packageUri.scheme == 'package') {
      package = packageUri.pathSegments[0];
235
      packagePath = packageUri.path.replaceFirst('${packageUri.pathSegments[0]}/', '');
236 237 238
    }

    return StackFrame(
239
      number: int.parse(match.group(1)!),
240 241 242 243 244
      className: className,
      method: method,
      packageScheme: packageUri.scheme,
      package: package,
      packagePath: packagePath,
245 246
      line: match.group(4) == null ? -1 : int.parse(match.group(4)!),
      column: match.group(5) == null ? -1 : int.parse(match.group(5)!),
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
      isConstructor: isConstructor,
      source: line,
    );
  }

  /// The original source of this stack frame.
  final String source;

  /// The zero-indexed frame number.
  ///
  /// This value may be -1 to indicate an unknown frame number.
  final int number;

  /// The scheme of the package for this frame, e.g. "dart" for
  /// dart:core/errors_patch.dart or "package" for
  /// package:flutter/src/widgets/text.dart.
  ///
  /// The path property refers to the source file.
  final String packageScheme;

  /// The package for this frame, e.g. "core" for
  /// dart:core/errors_patch.dart or "flutter" for
  /// package:flutter/src/widgets/text.dart.
  final String package;

  /// The path of the file for this frame, e.g. "errors_patch.dart" for
  /// dart:core/errors_patch.dart or "src/widgets/text.dart" for
  /// package:flutter/src/widgets/text.dart.
  final String packagePath;

  /// The source line number.
  final int line;

  /// The source column number.
  final int column;

  /// The class name, if any, for this frame.
  ///
  /// This may be null for top level methods in a library or anonymous closure
  /// methods.
  final String className;

  /// The method name for this frame.
  ///
  /// This will be an empty string if the stack frame is from the default
  /// constructor.
  final String method;

  /// Whether or not this was thrown from a constructor.
  final bool isConstructor;

  @override
  int get hashCode => hashValues(number, package, line, column, className, method, source);

  @override
  bool operator ==(Object other) {
303
    if (other.runtimeType != runtimeType)
304
      return false;
305 306 307 308 309 310 311 312
    return other is StackFrame
        && other.number == number
        && other.package == package
        && other.line == line
        && other.column == column
        && other.className == className
        && other.method == method
        && other.source == source;
313 314 315
  }

  @override
316
  String toString() => '${objectRuntimeType(this, 'StackFrame')}(#$number, $packageScheme:$package/$packagePath:$line:$column, className: $className, method: $method)';
317
}