stack_frame.dart 9.95 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

  /// 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>',
  );

52 53 54 55 56 57 58 59 60 61 62 63
  /// 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: '...',
  );

64 65 66 67 68 69 70 71 72 73 74 75
  /// Parses a list of [StackFrame]s from a [StackTrace] object.
  ///
  /// This is normally useful with [StackTrace.current].
  static List<StackFrame> fromStackTrace(StackTrace stack) {
    return fromStackString(stack.toString());
  }

  /// Parses a list of [StackFrame]s from the [StackTrace.toString] method.
  static List<StackFrame> fromStackString(String stack) {
    return stack
        .trim()
        .split('\n')
76
        .where((String line) => line.isNotEmpty)
77
        .map(fromStackTraceLine)
78 79 80
        // 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.
81
        .whereType<StackFrame>()
82 83 84
        .toList();
  }

85
  static StackFrame? _parseWebFrame(String line) {
86 87 88 89 90 91 92 93
    if (kDebugMode) {
      return _parseWebDebugFrame(line);
    } else {
      return _parseWebNonDebugFrame(line);
    }
  }

  static StackFrame _parseWebDebugFrame(String line) {
94 95
    // This RegExp is only partially correct for flutter run/test differences.
    // https://github.com/flutter/flutter/issues/52685
96 97
    final bool hasPackage = line.startsWith('package');
    final RegExp parser = hasPackage
98
        ? RegExp(r'^(package.+) (\d+):(\d+)\s+(.+)$')
99
        : RegExp(r'^(.+) (\d+):(\d+)\s+(.+)$');
100
    Match? match = parser.firstMatch(line);
Dan Field's avatar
Dan Field committed
101
    assert(match != null, 'Expected $line to match $parser.');
102
    match = match!;
103 104 105 106 107 108

    String package = '<unknown>';
    String packageScheme = '<unknown>';
    String packagePath = '<unknown>';
    if (hasPackage) {
      packageScheme = 'package';
109
      final Uri packageUri = Uri.parse(match.group(1)!);
110
      package = packageUri.pathSegments[0];
111
      packagePath = packageUri.path.replaceFirst('${packageUri.pathSegments[0]}/', '');
112 113 114 115 116 117 118
    }

    return StackFrame(
      number: -1,
      packageScheme: packageScheme,
      package: package,
      packagePath: packagePath,
119 120
      line: int.parse(match.group(2)!),
      column: int.parse(match.group(3)!),
121
      className: '<unknown>',
122
      method: match.group(4)!,
123 124 125 126
      source: line,
    );
  }

127 128 129 130 131 132 133 134
  // 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.
135 136
  static StackFrame? _parseWebNonDebugFrame(String line) {
    final Match? match = _webNonDebugFramePattern.firstMatch(line);
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
    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;
    }

152
    final List<String> classAndMethod = match.group(1)!.split('.');
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
    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,
    );
  }

171
  /// Parses a single [StackFrame] from a single line of a [StackTrace].
172
  static StackFrame? fromStackTraceLine(String line) {
173 174
    if (line == '<asynchronous suspension>') {
      return asynchronousSuspension;
175 176
    } else if (line == '...') {
      return stackOverFlowElision;
177 178
    }

179 180 181 182
    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 '
183
      'that propagates non-standard stack traces to the framework, such as during tests.',
184 185
    );

186 187 188 189 190 191
    // Web frames.
    if (!line.startsWith('#')) {
      return _parseWebFrame(line);
    }

    final RegExp parser = RegExp(r'^#(\d+) +(.+) \((.+?):?(\d+){0,1}:?(\d+){0,1}\)$');
192
    Match? match = parser.firstMatch(line);
193
    assert(match != null, 'Expected $line to match $parser.');
194
    match = match!;
195 196 197

    bool isConstructor = false;
    String className = '';
198
    String method = match.group(2)!.replaceAll('.<anonymous closure>', '');
199
    if (method.startsWith('new')) {
200 201 202
      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>';
203 204 205 206 207 208 209 210 211 212 213 214 215
      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];
    }

216
    final Uri packageUri = Uri.parse(match.group(3)!);
217 218 219 220
    String package = '<unknown>';
    String packagePath = packageUri.path;
    if (packageUri.scheme == 'dart' || packageUri.scheme == 'package') {
      package = packageUri.pathSegments[0];
221
      packagePath = packageUri.path.replaceFirst('${packageUri.pathSegments[0]}/', '');
222 223 224
    }

    return StackFrame(
225
      number: int.parse(match.group(1)!),
226 227 228 229 230
      className: className,
      method: method,
      packageScheme: packageUri.scheme,
      package: package,
      packagePath: packagePath,
231 232
      line: match.group(4) == null ? -1 : int.parse(match.group(4)!),
      column: match.group(5) == null ? -1 : int.parse(match.group(5)!),
233 234 235 236 237 238 239 240 241 242 243 244 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
      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
285
  int get hashCode => Object.hash(number, package, line, column, className, method, source);
286 287 288

  @override
  bool operator ==(Object other) {
289
    if (other.runtimeType != runtimeType) {
290
      return false;
291
    }
292 293 294 295 296 297 298 299
    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;
300 301 302
  }

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