blob: f9f630ee615cdfe5f66c2d05e9078f795873e927 [file] [log] [blame]
// 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';
import 'constants.dart';
import 'object.dart';
/// A object representation of a frame from a stack trace.
///
/// {@tool snippet}
///
/// 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({
required this.number,
required this.column,
required this.line,
required this.packageScheme,
required this.package,
required this.packagePath,
this.className = '',
required this.method,
this.isConstructor = false,
required this.source,
}) : 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>',
);
/// 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: '...',
);
/// 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')
.where((String line) => line.isNotEmpty)
.map(fromStackTraceLine)
// 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.
.whereType<StackFrame>()
.toList();
}
static StackFrame? _parseWebFrame(String line) {
if (kDebugMode) {
return _parseWebDebugFrame(line);
} else {
return _parseWebNonDebugFrame(line);
}
}
static StackFrame _parseWebDebugFrame(String line) {
// This RegExp is only partially correct for flutter run/test differences.
// https://github.com/flutter/flutter/issues/52685
final bool hasPackage = line.startsWith('package');
final RegExp parser = hasPackage
? RegExp(r'^(package.+) (\d+):(\d+)\s+(.+)$')
: RegExp(r'^(.+) (\d+):(\d+)\s+(.+)$');
Match? match = parser.firstMatch(line);
assert(match != null, 'Expected $line to match $parser.');
match = match!;
String package = '<unknown>';
String packageScheme = '<unknown>';
String packagePath = '<unknown>';
if (hasPackage) {
packageScheme = 'package';
final Uri packageUri = Uri.parse(match.group(1)!);
package = packageUri.pathSegments[0];
packagePath = packageUri.path.replaceFirst('${packageUri.pathSegments[0]}/', '');
}
return StackFrame(
number: -1,
packageScheme: packageScheme,
package: package,
packagePath: packagePath,
line: int.parse(match.group(2)!),
column: int.parse(match.group(3)!),
className: '<unknown>',
method: match.group(4)!,
source: line,
);
}
// 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.
static StackFrame? _parseWebNonDebugFrame(String line) {
final Match? match = _webNonDebugFramePattern.firstMatch(line);
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;
}
final List<String> classAndMethod = match.group(1)!.split('.');
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,
);
}
/// Parses a single [StackFrame] from a single line of a [StackTrace].
static StackFrame? fromStackTraceLine(String line) {
assert(line != null);
if (line == '<asynchronous suspension>') {
return asynchronousSuspension;
} else if (line == '...') {
return stackOverFlowElision;
}
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 '
'that propagates non-standard stack traces to the framework, such as during tests.',
);
// Web frames.
if (!line.startsWith('#')) {
return _parseWebFrame(line);
}
final RegExp parser = RegExp(r'^#(\d+) +(.+) \((.+?):?(\d+){0,1}:?(\d+){0,1}\)$');
Match? match = parser.firstMatch(line);
assert(match != null, 'Expected $line to match $parser.');
match = match!;
bool isConstructor = false;
String className = '';
String method = match.group(2)!.replaceAll('.<anonymous closure>', '');
if (method.startsWith('new')) {
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>';
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];
}
final Uri packageUri = Uri.parse(match.group(3)!);
String package = '<unknown>';
String packagePath = packageUri.path;
if (packageUri.scheme == 'dart' || packageUri.scheme == 'package') {
package = packageUri.pathSegments[0];
packagePath = packageUri.path.replaceFirst('${packageUri.pathSegments[0]}/', '');
}
return StackFrame(
number: int.parse(match.group(1)!),
className: className,
method: method,
packageScheme: packageUri.scheme,
package: package,
packagePath: packagePath,
line: match.group(4) == null ? -1 : int.parse(match.group(4)!),
column: match.group(5) == null ? -1 : int.parse(match.group(5)!),
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) {
if (other.runtimeType != runtimeType)
return false;
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;
}
@override
String toString() => '${objectRuntimeType(this, 'StackFrame')}(#$number, $packageScheme:$package/$packagePath:$line:$column, className: $className, method: $method)';
}