blob: 41d545e3ae4d7b30af84c74874fd0ea2364f15ba [file] [log] [blame]
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. 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:path/path.dart' as path;
import 'trace.dart';
import 'unparsed_frame.dart';
// #1 Foo._bar (file:///home/nweiz/code/stuff.dart:42:21)
// #1 Foo._bar (file:///home/nweiz/code/stuff.dart:42)
// #1 Foo._bar (file:///home/nweiz/code/stuff.dart)
final _vmFrame = RegExp(r'^#\d+\s+(\S.*) \((.+?)((?::\d+){0,2})\)$');
// at Object.stringify (native)
// at VW.call$0 (https://example.com/stuff.dart.js:560:28)
// at VW.call$0 (eval as fn
// (https://example.com/stuff.dart.js:560:28), efn:3:28)
// at https://example.com/stuff.dart.js:560:28
final _v8Frame =
RegExp(r'^\s*at (?:(\S.*?)(?: \[as [^\]]+\])? \((.*)\)|(.*))$');
// https://example.com/stuff.dart.js:560:28
// https://example.com/stuff.dart.js:560
final _v8UrlLocation = RegExp(r'^(.*?):(\d+)(?::(\d+))?$|native$');
// eval as function (https://example.com/stuff.dart.js:560:28), efn:3:28
// eval as function (https://example.com/stuff.dart.js:560:28)
// eval as function (eval as otherFunction
// (https://example.com/stuff.dart.js:560:28))
final _v8EvalLocation =
RegExp(r'^eval at (?:\S.*?) \((.*)\)(?:, .*?:\d+:\d+)?$');
// anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
// anonymous/<@https://example.com/stuff.js line 693 > eval:3:40
final _firefoxEvalLocation =
RegExp(r'(\S+)@(\S+) line (\d+) >.* (Function|eval):\d+:\d+');
// .VW.call$0@https://example.com/stuff.dart.js:560
// .VW.call$0("arg")@https://example.com/stuff.dart.js:560
// .VW.call$0/name<@https://example.com/stuff.dart.js:560
// .VW.call$0@https://example.com/stuff.dart.js:560:36
// https://example.com/stuff.dart.js:560
final _firefoxSafariFrame = RegExp(r'^'
r'(?:' // Member description. Not present in some Safari frames.
r'([^@(/]*)' // The actual name of the member.
r'(?:\(.*\))?' // Arguments to the member, sometimes captured by Firefox.
r'((?:/[^/]*)*)' // Extra characters indicating a nested closure.
r'(?:\(.*\))?' // Arguments to the closure.
r'@'
r')?'
r'(.*?)' // The frame's URL.
r':'
r'(\d*)' // The line number. Empty in Safari if it's unknown.
r'(?::(\d*))?' // The column number. Not present in older browsers and
// empty in Safari if it's unknown.
r'$');
// foo/bar.dart 10:11 Foo._bar
// foo/bar.dart 10:11 (anonymous function).dart.fn
// https://dart.dev/foo/bar.dart Foo._bar
// data:... 10:11 Foo._bar
final _friendlyFrame = RegExp(r'^(\S+)(?: (\d+)(?::(\d+))?)?\s+([^\d].*)$');
/// A regular expression that matches asynchronous member names generated by the
/// VM.
final _asyncBody = RegExp(r'<(<anonymous closure>|[^>]+)_async_body>');
final _initialDot = RegExp(r'^\.');
/// A single stack frame. Each frame points to a precise location in Dart code.
class Frame {
/// The URI of the file in which the code is located.
///
/// This URI will usually have the scheme `dart`, `file`, `http`, or `https`.
final Uri uri;
/// The line number on which the code location is located.
///
/// This can be null, indicating that the line number is unknown or
/// unimportant.
final int? line;
/// The column number of the code location.
///
/// This can be null, indicating that the column number is unknown or
/// unimportant.
final int? column;
/// The name of the member in which the code location occurs.
///
/// Anonymous closures are represented as `<fn>` in this member string.
final String? member;
/// Whether this stack frame comes from the Dart core libraries.
bool get isCore => uri.scheme == 'dart';
/// Returns a human-friendly description of the library that this stack frame
/// comes from.
///
/// This will usually be the string form of [uri], but a relative URI will be
/// used if possible. Data URIs will be truncated.
String get library {
if (uri.scheme == 'data') return 'data:...';
return path.prettyUri(uri);
}
/// Returns the name of the package this stack frame comes from, or `null` if
/// this stack frame doesn't come from a `package:` URL.
String? get package {
if (uri.scheme != 'package') return null;
return uri.path.split('/').first;
}
/// A human-friendly description of the code location.
String get location {
if (line == null) return library;
if (column == null) return '$library $line';
return '$library $line:$column';
}
/// Returns a single frame of the current stack.
///
/// By default, this will return the frame above the current method. If
/// [level] is `0`, it will return the current method's frame; if [level] is
/// higher than `1`, it will return higher frames.
factory Frame.caller([int level = 1]) {
if (level < 0) {
throw ArgumentError('Argument [level] must be greater than or equal '
'to 0.');
}
return Trace.current(level + 1).frames.first;
}
/// Parses a string representation of a Dart VM stack frame.
factory Frame.parseVM(String frame) => _catchFormatException(frame, () {
// The VM sometimes folds multiple stack frames together and replaces
// them with "...".
if (frame == '...') {
return Frame(Uri(), null, null, '...');
}
var match = _vmFrame.firstMatch(frame);
if (match == null) return UnparsedFrame(frame);
// Get the pieces out of the regexp match. Function, URI and line should
// always be found. The column is optional.
var member = match[1]!
.replaceAll(_asyncBody, '<async>')
.replaceAll('<anonymous closure>', '<fn>');
var uri = match[2]!.startsWith('<data:')
? Uri.dataFromString('')
: Uri.parse(match[2]!);
var lineAndColumn = match[3]!.split(':');
var line =
lineAndColumn.length > 1 ? int.parse(lineAndColumn[1]) : null;
var column =
lineAndColumn.length > 2 ? int.parse(lineAndColumn[2]) : null;
return Frame(uri, line, column, member);
});
/// Parses a string representation of a Chrome/V8 stack frame.
factory Frame.parseV8(String frame) => _catchFormatException(frame, () {
var match = _v8Frame.firstMatch(frame);
if (match == null) return UnparsedFrame(frame);
// v8 location strings can be arbitrarily-nested, since it adds a layer
// of nesting for each eval performed on that line.
Frame parseLocation(String location, String member) {
var evalMatch = _v8EvalLocation.firstMatch(location);
while (evalMatch != null) {
location = evalMatch[1]!;
evalMatch = _v8EvalLocation.firstMatch(location);
}
if (location == 'native') {
return Frame(Uri.parse('native'), null, null, member);
}
var urlMatch = _v8UrlLocation.firstMatch(location);
if (urlMatch == null) return UnparsedFrame(frame);
final uri = _uriOrPathToUri(urlMatch[1]!);
final line = int.parse(urlMatch[2]!);
final column = urlMatch[3] != null ? int.parse(urlMatch[3]!) : null;
return Frame(uri, line, column, member);
}
// V8 stack frames can be in two forms.
if (match[2] != null) {
// The first form looks like " at FUNCTION (LOCATION)". V8 proper
// lists anonymous functions within eval as "<anonymous>", while IE10
// lists them as "Anonymous function".
return parseLocation(
match[2]!,
match[1]!
.replaceAll('<anonymous>', '<fn>')
.replaceAll('Anonymous function', '<fn>')
.replaceAll('(anonymous function)', '<fn>'));
} else {
// The second form looks like " at LOCATION", and is used for
// anonymous functions.
return parseLocation(match[3]!, '<fn>');
}
});
/// Parses a string representation of a JavaScriptCore stack trace.
factory Frame.parseJSCore(String frame) => Frame.parseV8(frame);
/// Parses a string representation of an IE stack frame.
///
/// IE10+ frames look just like V8 frames. Prior to IE10, stack traces can't
/// be retrieved.
factory Frame.parseIE(String frame) => Frame.parseV8(frame);
/// Parses a Firefox 'eval' or 'function' stack frame.
///
/// for example:
/// anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
/// anonymous/<@https://example.com/stuff.js line 693 > eval:3:40
factory Frame._parseFirefoxEval(String frame) =>
_catchFormatException(frame, () {
final match = _firefoxEvalLocation.firstMatch(frame);
if (match == null) return UnparsedFrame(frame);
var member = match[1]!.replaceAll('/<', '');
final uri = _uriOrPathToUri(match[2]!);
final line = int.parse(match[3]!);
if (member.isEmpty || member == 'anonymous') {
member = '<fn>';
}
return Frame(uri, line, null, member);
});
/// Parses a string representation of a Firefox stack frame.
factory Frame.parseFirefox(String frame) => _catchFormatException(frame, () {
var match = _firefoxSafariFrame.firstMatch(frame);
if (match == null) return UnparsedFrame(frame);
if (match[3]!.contains(' line ')) {
return Frame._parseFirefoxEval(frame);
}
// Normally this is a URI, but in a jsshell trace it can be a path.
var uri = _uriOrPathToUri(match[3]!);
var member = match[1];
if (member != null) {
member +=
List.filled('/'.allMatches(match[2]!).length, '.<fn>').join();
if (member == '') member = '<fn>';
// Some Firefox members have initial dots. We remove them for
// consistency with other platforms.
member = member.replaceFirst(_initialDot, '');
} else {
member = '<fn>';
}
var line = match[4] == '' ? null : int.parse(match[4]!);
var column =
match[5] == null || match[5] == '' ? null : int.parse(match[5]!);
return Frame(uri, line, column, member);
});
/// Parses a string representation of a Safari 6.0 stack frame.
@Deprecated('Use Frame.parseSafari instead.')
factory Frame.parseSafari6_0(String frame) => Frame.parseFirefox(frame);
/// Parses a string representation of a Safari 6.1+ stack frame.
@Deprecated('Use Frame.parseSafari instead.')
factory Frame.parseSafari6_1(String frame) => Frame.parseFirefox(frame);
/// Parses a string representation of a Safari stack frame.
factory Frame.parseSafari(String frame) => Frame.parseFirefox(frame);
/// Parses this package's string representation of a stack frame.
factory Frame.parseFriendly(String frame) => _catchFormatException(frame, () {
var match = _friendlyFrame.firstMatch(frame);
if (match == null) {
throw FormatException(
"Couldn't parse package:stack_trace stack trace line '$frame'.");
}
// Fake truncated data urls generated by the friendly stack trace format
// cause Uri.parse to throw an exception so we have to special case
// them.
var uri = match[1] == 'data:...'
? Uri.dataFromString('')
: Uri.parse(match[1]!);
// If there's no scheme, this is a relative URI. We should interpret it as
// relative to the current working directory.
if (uri.scheme == '') {
uri = path.toUri(path.absolute(path.fromUri(uri)));
}
var line = match[2] == null ? null : int.parse(match[2]!);
var column = match[3] == null ? null : int.parse(match[3]!);
return Frame(uri, line, column, match[4]);
});
/// A regular expression matching an absolute URI.
static final _uriRegExp = RegExp(r'^[a-zA-Z][-+.a-zA-Z\d]*://');
/// A regular expression matching a Windows path.
static final _windowsRegExp = RegExp(r'^([a-zA-Z]:[\\/]|\\\\)');
/// Converts [uriOrPath], which can be a URI, a Windows path, or a Posix path,
/// to a URI (absolute if possible).
static Uri _uriOrPathToUri(String uriOrPath) {
if (uriOrPath.contains(_uriRegExp)) {
return Uri.parse(uriOrPath);
} else if (uriOrPath.contains(_windowsRegExp)) {
return Uri.file(uriOrPath, windows: true);
} else if (uriOrPath.startsWith('/')) {
return Uri.file(uriOrPath, windows: false);
}
// As far as I've seen, Firefox and V8 both always report absolute paths in
// their stack frames. However, if we do get a relative path, we should
// handle it gracefully.
if (uriOrPath.contains('\\')) return path.windows.toUri(uriOrPath);
return Uri.parse(uriOrPath);
}
/// Runs [body] and returns its result.
///
/// If [body] throws a [FormatException], returns an [UnparsedFrame] with
/// [text] instead.
static Frame _catchFormatException(String text, Frame Function() body) {
try {
return body();
} on FormatException catch (_) {
return UnparsedFrame(text);
}
}
Frame(this.uri, this.line, this.column, this.member);
@override
String toString() => '$location in $member';
}