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

7
import 'constants.dart';
8 9
import 'object.dart';

10 11
/// A object representation of a frame from a stack trace.
///
12
/// {@tool snippet}
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
///
/// 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({
28 29 30 31 32 33
    required this.number,
    required this.column,
    required this.line,
    required this.packageScheme,
    required this.package,
    required this.packagePath,
34
    this.className = '',
35
    required this.method,
36
    this.isConstructor = false,
37
    required this.source,
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
  })  : 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>',
  );

61 62 63 64 65 66 67 68 69 70 71 72
  /// 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: '...',
  );

73 74 75 76 77 78 79 80 81 82 83 84 85 86
  /// 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')
87
        .where((String line) => line.isNotEmpty)
88
        .map(fromStackTraceLine)
89 90 91
        // 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.
92
        .whereType<StackFrame>()
93 94 95
        .toList();
  }

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

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

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

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

138 139 140 141 142 143 144 145
  // 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.
146 147
  static StackFrame? _parseWebNonDebugFrame(String line) {
    final Match? match = _webNonDebugFramePattern.firstMatch(line);
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
    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;
    }

163
    final List<String> classAndMethod = match.group(1)!.split('.');
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
    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,
    );
  }

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

191 192 193 194
    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 '
195
      'that propagates non-standard stack traces to the framework, such as during tests.',
196 197
    );

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

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

    bool isConstructor = false;
    String className = '';
210
    String method = match.group(2)!.replaceAll('.<anonymous closure>', '');
211
    if (method.startsWith('new')) {
212 213 214
      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>';
215 216 217 218 219 220 221 222 223 224 225 226 227
      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];
    }

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

    return StackFrame(
237
      number: int.parse(match.group(1)!),
238 239 240 241 242
      className: className,
      method: method,
      packageScheme: packageUri.scheme,
      package: package,
      packagePath: packagePath,
243 244
      line: match.group(4) == null ? -1 : int.parse(match.group(4)!),
      column: match.group(5) == null ? -1 : int.parse(match.group(5)!),
245 246 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
      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
297
  int get hashCode => Object.hash(number, package, line, column, className, method, source);
298 299 300

  @override
  bool operator ==(Object other) {
301
    if (other.runtimeType != runtimeType) {
302
      return false;
303
    }
304 305 306 307 308 309 310 311
    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;
312 313 314
  }

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