stack_frame.dart 10.5 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 82 83
        // TODO(polina-c): if one of lines was parsed to null, the entire stack trace
        // is in unexpected format and should be returned as is, without partial parsing.
        // https://github.com/flutter/flutter/issues/131877
84
        .whereType<StackFrame>()
85 86 87
        .toList();
  }

88 89 90 91
  /// Parses a single [StackFrame] from a line of a [StackTrace].
  ///
  /// Returns null if format is not as expected.
  static StackFrame? _tryParseWebFrame(String line) {
92
    if (kDebugMode) {
93
      return _tryParseWebDebugFrame(line);
94
    } else {
95
      return _tryParseWebNonDebugFrame(line);
96 97 98
    }
  }

99 100 101 102
  /// Parses a single [StackFrame] from a line of a [StackTrace].
  ///
  /// Returns null if format is not as expected.
  static StackFrame? _tryParseWebDebugFrame(String line) {
103 104
    // This RegExp is only partially correct for flutter run/test differences.
    // https://github.com/flutter/flutter/issues/52685
105 106
    final bool hasPackage = line.startsWith('package');
    final RegExp parser = hasPackage
107
        ? RegExp(r'^(package.+) (\d+):(\d+)\s+(.+)$')
108
        : RegExp(r'^(.+) (\d+):(\d+)\s+(.+)$');
109 110 111 112 113 114

    final Match? match = parser.firstMatch(line);

    if (match == null) {
      return null;
    }
115 116 117 118

    String package = '<unknown>';
    String packageScheme = '<unknown>';
    String packagePath = '<unknown>';
119

120 121
    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
  static StackFrame? _tryParseWebNonDebugFrame(String line) {
149
    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 186
  ///
  /// Returns null if format is not as expected.
187
  static StackFrame? fromStackTraceLine(String line) {
188 189
    if (line == '<asynchronous suspension>') {
      return asynchronousSuspension;
190 191
    } else if (line == '...') {
      return stackOverFlowElision;
192 193
    }

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

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

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

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

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

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

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

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