Line data Source code
1 : // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
2 : // for details. All rights reserved. Use of this source code is governed by a
3 : // BSD-style license that can be found in the LICENSE file.
4 :
5 : import 'package:path/path.dart' as path;
6 :
7 : import 'trace.dart';
8 : import 'unparsed_frame.dart';
9 :
10 : // #1 Foo._bar (file:///home/nweiz/code/stuff.dart:42:21)
11 : // #1 Foo._bar (file:///home/nweiz/code/stuff.dart:42)
12 : // #1 Foo._bar (file:///home/nweiz/code/stuff.dart)
13 33 : final _vmFrame = RegExp(r'^#\d+\s+(\S.*) \((.+?)((?::\d+){0,2})\)$');
14 :
15 : // at Object.stringify (native)
16 : // at VW.call$0 (https://example.com/stuff.dart.js:560:28)
17 : // at VW.call$0 (eval as fn
18 : // (https://example.com/stuff.dart.js:560:28), efn:3:28)
19 : // at https://example.com/stuff.dart.js:560:28
20 0 : final _v8Frame =
21 : RegExp(r'^\s*at (?:(\S.*?)(?: \[as [^\]]+\])? \((.*)\)|(.*))$');
22 :
23 : // https://example.com/stuff.dart.js:560:28
24 : // https://example.com/stuff.dart.js:560
25 0 : final _v8UrlLocation = RegExp(r'^(.*?):(\d+)(?::(\d+))?$|native$');
26 :
27 : // eval as function (https://example.com/stuff.dart.js:560:28), efn:3:28
28 : // eval as function (https://example.com/stuff.dart.js:560:28)
29 : // eval as function (eval as otherFunction
30 : // (https://example.com/stuff.dart.js:560:28))
31 0 : final _v8EvalLocation =
32 : RegExp(r'^eval at (?:\S.*?) \((.*)\)(?:, .*?:\d+:\d+)?$');
33 :
34 : // anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
35 : // anonymous/<@https://example.com/stuff.js line 693 > eval:3:40
36 0 : final _firefoxEvalLocation =
37 : RegExp(r'(\S+)@(\S+) line (\d+) >.* (Function|eval):\d+:\d+');
38 :
39 : // .VW.call$0@https://example.com/stuff.dart.js:560
40 : // .VW.call$0("arg")@https://example.com/stuff.dart.js:560
41 : // .VW.call$0/name<@https://example.com/stuff.dart.js:560
42 : // .VW.call$0@https://example.com/stuff.dart.js:560:36
43 : // https://example.com/stuff.dart.js:560
44 0 : final _firefoxSafariFrame = RegExp(r'^'
45 : r'(?:' // Member description. Not present in some Safari frames.
46 : r'([^@(/]*)' // The actual name of the member.
47 : r'(?:\(.*\))?' // Arguments to the member, sometimes captured by Firefox.
48 : r'((?:/[^/]*)*)' // Extra characters indicating a nested closure.
49 : r'(?:\(.*\))?' // Arguments to the closure.
50 : r'@'
51 : r')?'
52 : r'(.*?)' // The frame's URL.
53 : r':'
54 : r'(\d*)' // The line number. Empty in Safari if it's unknown.
55 : r'(?::(\d*))?' // The column number. Not present in older browsers and
56 : // empty in Safari if it's unknown.
57 : r'$');
58 :
59 : // foo/bar.dart 10:11 Foo._bar
60 : // foo/bar.dart 10:11 (anonymous function).dart.fn
61 : // https://dart.dev/foo/bar.dart Foo._bar
62 : // data:... 10:11 Foo._bar
63 0 : final _friendlyFrame = RegExp(r'^(\S+)(?: (\d+)(?::(\d+))?)?\s+([^\d].*)$');
64 :
65 : /// A regular expression that matches asynchronous member names generated by the
66 : /// VM.
67 33 : final _asyncBody = RegExp(r'<(<anonymous closure>|[^>]+)_async_body>');
68 :
69 0 : final _initialDot = RegExp(r'^\.');
70 :
71 : /// A single stack frame. Each frame points to a precise location in Dart code.
72 : class Frame {
73 : /// The URI of the file in which the code is located.
74 : ///
75 : /// This URI will usually have the scheme `dart`, `file`, `http`, or `https`.
76 : final Uri uri;
77 :
78 : /// The line number on which the code location is located.
79 : ///
80 : /// This can be null, indicating that the line number is unknown or
81 : /// unimportant.
82 : final int? line;
83 :
84 : /// The column number of the code location.
85 : ///
86 : /// This can be null, indicating that the column number is unknown or
87 : /// unimportant.
88 : final int? column;
89 :
90 : /// The name of the member in which the code location occurs.
91 : ///
92 : /// Anonymous closures are represented as `<fn>` in this member string.
93 : final String? member;
94 :
95 : /// Whether this stack frame comes from the Dart core libraries.
96 44 : bool get isCore => uri.scheme == 'dart';
97 :
98 : /// Returns a human-friendly description of the library that this stack frame
99 : /// comes from.
100 : ///
101 : /// This will usually be the string form of [uri], but a relative URI will be
102 : /// used if possible. Data URIs will be truncated.
103 11 : String get library {
104 33 : if (uri.scheme == 'data') return 'data:...';
105 22 : return path.prettyUri(uri);
106 : }
107 :
108 : /// Returns the name of the package this stack frame comes from, or `null` if
109 : /// this stack frame doesn't come from a `package:` URL.
110 11 : String? get package {
111 33 : if (uri.scheme != 'package') return null;
112 44 : return uri.path.split('/').first;
113 : }
114 :
115 : /// A human-friendly description of the code location.
116 11 : String get location {
117 13 : if (line == null) return library;
118 11 : if (column == null) return '$library $line';
119 44 : return '$library $line:$column';
120 : }
121 :
122 : /// Returns a single frame of the current stack.
123 : ///
124 : /// By default, this will return the frame above the current method. If
125 : /// [level] is `0`, it will return the current method's frame; if [level] is
126 : /// higher than `1`, it will return higher frames.
127 0 : factory Frame.caller([int level = 1]) {
128 0 : if (level < 0) {
129 0 : throw ArgumentError('Argument [level] must be greater than or equal '
130 : 'to 0.');
131 : }
132 :
133 0 : return Trace.current(level + 1).frames.first;
134 : }
135 :
136 : /// Parses a string representation of a Dart VM stack frame.
137 33 : factory Frame.parseVM(String frame) => _catchFormatException(frame, () {
138 : // The VM sometimes folds multiple stack frames together and replaces
139 : // them with "...".
140 11 : if (frame == '...') {
141 0 : return Frame(Uri(), null, null, '...');
142 : }
143 :
144 22 : var match = _vmFrame.firstMatch(frame);
145 0 : if (match == null) return UnparsedFrame(frame);
146 :
147 : // Get the pieces out of the regexp match. Function, URI and line should
148 : // always be found. The column is optional.
149 11 : var member = match[1]!
150 22 : .replaceAll(_asyncBody, '<async>')
151 11 : .replaceAll('<anonymous closure>', '<fn>');
152 22 : var uri = match[2]!.startsWith('<data:')
153 0 : ? Uri.dataFromString('')
154 22 : : Uri.parse(match[2]!);
155 :
156 22 : var lineAndColumn = match[3]!.split(':');
157 : var line =
158 44 : lineAndColumn.length > 1 ? int.parse(lineAndColumn[1]) : null;
159 : var column =
160 44 : lineAndColumn.length > 2 ? int.parse(lineAndColumn[2]) : null;
161 11 : return Frame(uri, line, column, member);
162 : });
163 :
164 : /// Parses a string representation of a Chrome/V8 stack frame.
165 0 : factory Frame.parseV8(String frame) => _catchFormatException(frame, () {
166 0 : var match = _v8Frame.firstMatch(frame);
167 0 : if (match == null) return UnparsedFrame(frame);
168 :
169 : // v8 location strings can be arbitrarily-nested, since it adds a layer
170 : // of nesting for each eval performed on that line.
171 0 : Frame parseLocation(String location, String member) {
172 0 : var evalMatch = _v8EvalLocation.firstMatch(location);
173 : while (evalMatch != null) {
174 0 : location = evalMatch[1]!;
175 0 : evalMatch = _v8EvalLocation.firstMatch(location);
176 : }
177 :
178 0 : if (location == 'native') {
179 0 : return Frame(Uri.parse('native'), null, null, member);
180 : }
181 :
182 0 : var urlMatch = _v8UrlLocation.firstMatch(location);
183 0 : if (urlMatch == null) return UnparsedFrame(frame);
184 :
185 0 : final uri = _uriOrPathToUri(urlMatch[1]!);
186 0 : final line = int.parse(urlMatch[2]!);
187 0 : final columnMatch = urlMatch[3];
188 0 : final column = columnMatch != null ? int.parse(columnMatch) : null;
189 0 : return Frame(uri, line, column, member);
190 : }
191 :
192 : // V8 stack frames can be in two forms.
193 0 : if (match[2] != null) {
194 : // The first form looks like " at FUNCTION (LOCATION)". V8 proper
195 : // lists anonymous functions within eval as "<anonymous>", while IE10
196 : // lists them as "Anonymous function".
197 : return parseLocation(
198 0 : match[2]!,
199 0 : match[1]!
200 0 : .replaceAll('<anonymous>', '<fn>')
201 0 : .replaceAll('Anonymous function', '<fn>')
202 0 : .replaceAll('(anonymous function)', '<fn>'));
203 : } else {
204 : // The second form looks like " at LOCATION", and is used for
205 : // anonymous functions.
206 0 : return parseLocation(match[3]!, '<fn>');
207 : }
208 : });
209 :
210 : /// Parses a string representation of a JavaScriptCore stack trace.
211 0 : factory Frame.parseJSCore(String frame) => Frame.parseV8(frame);
212 :
213 : /// Parses a string representation of an IE stack frame.
214 : ///
215 : /// IE10+ frames look just like V8 frames. Prior to IE10, stack traces can't
216 : /// be retrieved.
217 0 : factory Frame.parseIE(String frame) => Frame.parseV8(frame);
218 :
219 : /// Parses a Firefox 'eval' or 'function' stack frame.
220 : ///
221 : /// for example:
222 : /// anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
223 : /// anonymous/<@https://example.com/stuff.js line 693 > eval:3:40
224 0 : factory Frame._parseFirefoxEval(String frame) =>
225 0 : _catchFormatException(frame, () {
226 0 : final match = _firefoxEvalLocation.firstMatch(frame);
227 0 : if (match == null) return UnparsedFrame(frame);
228 0 : var member = match[1]!.replaceAll('/<', '');
229 0 : final uri = _uriOrPathToUri(match[2]!);
230 0 : final line = int.parse(match[3]!);
231 0 : if (member.isEmpty || member == 'anonymous') {
232 : member = '<fn>';
233 : }
234 0 : return Frame(uri, line, null, member);
235 : });
236 :
237 : /// Parses a string representation of a Firefox stack frame.
238 0 : factory Frame.parseFirefox(String frame) => _catchFormatException(frame, () {
239 0 : var match = _firefoxSafariFrame.firstMatch(frame);
240 0 : if (match == null) return UnparsedFrame(frame);
241 :
242 0 : if (match[3]!.contains(' line ')) {
243 0 : return Frame._parseFirefoxEval(frame);
244 : }
245 :
246 : // Normally this is a URI, but in a jsshell trace it can be a path.
247 0 : var uri = _uriOrPathToUri(match[3]!);
248 :
249 0 : var member = match[1];
250 : if (member != null) {
251 0 : member +=
252 0 : List.filled('/'.allMatches(match[2]!).length, '.<fn>').join();
253 0 : if (member == '') member = '<fn>';
254 :
255 : // Some Firefox members have initial dots. We remove them for
256 : // consistency with other platforms.
257 0 : member = member.replaceFirst(_initialDot, '');
258 : } else {
259 : member = '<fn>';
260 : }
261 :
262 0 : var line = match[4] == '' ? null : int.parse(match[4]!);
263 : var column =
264 0 : match[5] == null || match[5] == '' ? null : int.parse(match[5]!);
265 0 : return Frame(uri, line, column, member);
266 : });
267 :
268 : /// Parses a string representation of a Safari 6.0 stack frame.
269 : @Deprecated('Use Frame.parseSafari instead.')
270 0 : factory Frame.parseSafari6_0(String frame) => Frame.parseFirefox(frame);
271 :
272 : /// Parses a string representation of a Safari 6.1+ stack frame.
273 : @Deprecated('Use Frame.parseSafari instead.')
274 0 : factory Frame.parseSafari6_1(String frame) => Frame.parseFirefox(frame);
275 :
276 : /// Parses a string representation of a Safari stack frame.
277 0 : factory Frame.parseSafari(String frame) => Frame.parseFirefox(frame);
278 :
279 : /// Parses this package's string representation of a stack frame.
280 0 : factory Frame.parseFriendly(String frame) => _catchFormatException(frame, () {
281 0 : var match = _friendlyFrame.firstMatch(frame);
282 : if (match == null) {
283 0 : throw FormatException(
284 0 : "Couldn't parse package:stack_trace stack trace line '$frame'.");
285 : }
286 : // Fake truncated data urls generated by the friendly stack trace format
287 : // cause Uri.parse to throw an exception so we have to special case
288 : // them.
289 0 : var uri = match[1] == 'data:...'
290 0 : ? Uri.dataFromString('')
291 0 : : Uri.parse(match[1]!);
292 : // If there's no scheme, this is a relative URI. We should interpret it as
293 : // relative to the current working directory.
294 0 : if (uri.scheme == '') {
295 0 : uri = path.toUri(path.absolute(path.fromUri(uri)));
296 : }
297 :
298 0 : var line = match[2] == null ? null : int.parse(match[2]!);
299 0 : var column = match[3] == null ? null : int.parse(match[3]!);
300 0 : return Frame(uri, line, column, match[4]);
301 : });
302 :
303 : /// A regular expression matching an absolute URI.
304 0 : static final _uriRegExp = RegExp(r'^[a-zA-Z][-+.a-zA-Z\d]*://');
305 :
306 : /// A regular expression matching a Windows path.
307 0 : static final _windowsRegExp = RegExp(r'^([a-zA-Z]:[\\/]|\\\\)');
308 :
309 : /// Converts [uriOrPath], which can be a URI, a Windows path, or a Posix path,
310 : /// to a URI (absolute if possible).
311 0 : static Uri _uriOrPathToUri(String uriOrPath) {
312 0 : if (uriOrPath.contains(_uriRegExp)) {
313 0 : return Uri.parse(uriOrPath);
314 0 : } else if (uriOrPath.contains(_windowsRegExp)) {
315 0 : return Uri.file(uriOrPath, windows: true);
316 0 : } else if (uriOrPath.startsWith('/')) {
317 0 : return Uri.file(uriOrPath, windows: false);
318 : }
319 :
320 : // As far as I've seen, Firefox and V8 both always report absolute paths in
321 : // their stack frames. However, if we do get a relative path, we should
322 : // handle it gracefully.
323 0 : if (uriOrPath.contains('\\')) return path.windows.toUri(uriOrPath);
324 0 : return Uri.parse(uriOrPath);
325 : }
326 :
327 : /// Runs [body] and returns its result.
328 : ///
329 : /// If [body] throws a [FormatException], returns an [UnparsedFrame] with
330 : /// [text] instead.
331 11 : static Frame _catchFormatException(String text, Frame Function() body) {
332 : try {
333 : return body();
334 0 : } on FormatException catch (_) {
335 0 : return UnparsedFrame(text);
336 : }
337 : }
338 :
339 11 : Frame(this.uri, this.line, this.column, this.member);
340 :
341 0 : @override
342 0 : String toString() => '$location in $member';
343 : }
|