// 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 _v8JsFrame = RegExp(r'^\s*at (?:(\S.*?)(?: \[as [^\]]+\])? \((.*)\)|(.*))$'); // https://example.com/stuff.dart.js:560:28 // https://example.com/stuff.dart.js:560 // // Group 1: URI, required // Group 2: line number, required // Group 3: column number, optional final _v8JsUrlLocation = RegExp(r'^(.*?):(\d+)(?::(\d+))?$|native$'); // With names: // // at Error.f (wasm://wasm/0006d966:wasm-function[119]:0xbb13) // at g (wasm://wasm/0006d966:wasm-function[796]:0x143b4) // // Without names: // // at wasm://wasm/0005168a:wasm-function[119]:0xbb13 // at wasm://wasm/0005168a:wasm-function[796]:0x143b4 // // Matches named groups: // // - "member": optional, `Error.f` in the first example, NA in the second. // - "uri": `wasm://wasm/0006d966`. // - "index": `119`. // - "offset": (hex number) `bb13`. // // To avoid having multiple groups for the same part of the frame, this regex // matches unmatched parentheses after the member name. final _v8WasmFrame = RegExp(r'^\s*at (?:(?.+) )?' r'(?:\(?(?:(?\S+):wasm-function\[(?\d+)\]' r'\:0x(?[0-9a-fA-F]+))\)?)$'); // 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 _firefoxSafariJSFrame = 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'$'); // With names: // // g@http://localhost:8080/test.wasm:wasm-function[796]:0x143b4 // f@http://localhost:8080/test.wasm:wasm-function[795]:0x143a8 // main@http://localhost:8080/test.wasm:wasm-function[792]:0x14390 // // Without names: // // @http://localhost:8080/test.wasm:wasm-function[796]:0x143b4 // @http://localhost:8080/test.wasm:wasm-function[795]:0x143a8 // @http://localhost:8080/test.wasm:wasm-function[792]:0x14390 // // JSShell in the command line uses a different format, which this regex also // parses. // // With names: // // main@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[792]:0x14378 // // Without names: // // @/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[792]:0x14378 // // Matches named groups: // // - "member": Function name, may be empty: `g`. // - "uri": `http://localhost:8080/test.wasm`. // - "index": `796`. // - "offset": (in hex) `143b4`. final _firefoxWasmFrame = RegExp(r'^(?.*?)@(?:(?\S+).*?:wasm-function' r'\[(?\d+)\]:0x(?[0-9a-fA-F]+))$'); // With names: // // (Note: Lines below are literal text, e.g. is not a placeholder, it's a // part of the stack frame.) // // .wasm-function[g]@[wasm code] // .wasm-function[f]@[wasm code] // .wasm-function[main]@[wasm code] // // Without names: // // .wasm-function[796]@[wasm code] // .wasm-function[795]@[wasm code] // .wasm-function[792]@[wasm code] // // Matches named group "member": `g` or `796`. final _safariWasmFrame = RegExp(r'^.*?wasm-function\[(?.*)\]@\[wasm code\]$'); // 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'<(|[^>]+)_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 `` 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, '') .replaceAll('', ''); var uri = match[2]!.startsWith(' 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, () { // Try to match a Wasm frame first: the Wasm frame regex won't match a // JS frame but the JS frame regex may match a Wasm frame. var match = _v8WasmFrame.firstMatch(frame); if (match != null) { final member = match.namedGroup('member'); final uri = _uriOrPathToUri(match.namedGroup('uri')!); final functionIndex = match.namedGroup('index')!; final functionOffset = int.parse(match.namedGroup('offset')!, radix: 16); return Frame(uri, 1, functionOffset + 1, member ?? functionIndex); } match = _v8JsFrame.firstMatch(frame); if (match != null) { // v8 location strings can be arbitrarily-nested, since it adds a // layer of nesting for each eval performed on that line. Frame parseJsLocation(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 = _v8JsUrlLocation.firstMatch(location); if (urlMatch == null) return UnparsedFrame(frame); final uri = _uriOrPathToUri(urlMatch[1]!); final line = int.parse(urlMatch[2]!); final columnMatch = urlMatch[3]; final column = columnMatch != null ? int.parse(columnMatch) : 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 "", while // IE10 lists them as "Anonymous function". return parseJsLocation( match[2]!, match[1]! .replaceAll('', '') .replaceAll('Anonymous function', '') .replaceAll('(anonymous function)', '')); } else { // The second form looks like " at LOCATION", and is used for // anonymous functions. return parseJsLocation(match[3]!, ''); } } return UnparsedFrame(frame); }); /// 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 = ''; } return Frame(uri, line, null, member); }); /// Parses a string representation of a Firefox or Safari stack frame. factory Frame.parseFirefox(String frame) => _catchFormatException(frame, () { var match = _firefoxSafariJSFrame.firstMatch(frame); if (match != null) { 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, '.').join(); if (member == '') member = ''; // Some Firefox members have initial dots. We remove them for // consistency with other platforms. member = member.replaceFirst(_initialDot, ''); } else { member = ''; } 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); } match = _firefoxWasmFrame.firstMatch(frame); if (match != null) { final member = match.namedGroup('member')!; final uri = _uriOrPathToUri(match.namedGroup('uri')!); final functionIndex = match.namedGroup('index')!; final functionOffset = int.parse(match.namedGroup('offset')!, radix: 16); return Frame(uri, 1, functionOffset + 1, member.isNotEmpty ? member : functionIndex); } match = _safariWasmFrame.firstMatch(frame); if (match != null) { final member = match.namedGroup('member')!; return Frame(Uri(path: 'wasm code'), null, null, member); } return UnparsedFrame(frame); }); /// 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'; }