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 'dart:math' as math;
6 :
7 : import 'chain.dart';
8 : import 'frame.dart';
9 : import 'lazy_trace.dart';
10 : import 'unparsed_frame.dart';
11 : import 'utils.dart';
12 : import 'vm_trace.dart';
13 :
14 : final _terseRegExp = new RegExp(r"(-patch)?([/\\].*)?$");
15 :
16 : /// A RegExp to match V8's stack traces.
17 : ///
18 : /// V8's traces start with a line that's either just "Error" or else is a
19 : /// description of the exception that occurred. That description can be multiple
20 : /// lines, so we just look for any line other than the first that begins with
21 : /// three or four spaces and "at".
22 : final _v8Trace = new RegExp(r"\n ?at ");
23 :
24 : /// A RegExp to match indidual lines of V8's stack traces.
25 : ///
26 : /// This is intended to filter out the leading exception details of the trace
27 : /// though it is possible for the message to match this as well.
28 : final _v8TraceLine = new RegExp(r" ?at ");
29 :
30 : /// A RegExp to match Firefox and Safari's stack traces.
31 : ///
32 : /// Firefox and Safari have very similar stack trace formats, so we use the same
33 : /// logic for parsing them.
34 : ///
35 : /// Firefox's trace frames start with the name of the function in which the
36 : /// error occurred, possibly including its parameters inside `()`. For example,
37 : /// `.VW.call$0("arg")@http://pub.dartlang.org/stuff.dart.js:560`.
38 : ///
39 : /// Safari traces occasionally don't include the initial method name followed by
40 : /// "@", and they always have both the line and column number (or just a
41 : /// trailing colon if no column number is available). They can also contain
42 : /// empty lines or lines consisting only of `[native code]`.
43 : final _firefoxSafariTrace = new RegExp(
44 : r"^"
45 : r"(" // Member description. Not present in some Safari frames.
46 : r"([.0-9A-Za-z_$/<]|\(.*\))*" // Member name and arguments.
47 : r"@"
48 : r")?"
49 : r"[^\s]*" // Frame URL.
50 : r":\d*" // Line or column number. Some older frames only have a line number.
51 : r"$",
52 : multiLine: true);
53 :
54 : /// A RegExp to match this package's stack traces.
55 : final _friendlyTrace =
56 : new RegExp(r"^[^\s<][^\s]*( \d+(:\d+)?)?[ \t]+[^\s]+$", multiLine: true);
57 :
58 : /// A stack trace, comprised of a list of stack frames.
59 : class Trace implements StackTrace {
60 : /// The stack frames that comprise this stack trace.
61 : final List<Frame> frames;
62 :
63 : /// The original stack trace from which this trace was parsed.
64 : final StackTrace original;
65 :
66 : /// Returns a human-readable representation of [stackTrace]. If [terse] is
67 : /// set, this folds together multiple stack frames from the Dart core
68 : /// libraries, so that only the core library method directly called from user
69 : /// code is visible (see [Trace.terse]).
70 : static String format(StackTrace stackTrace, {bool terse: true}) {
71 0 : var trace = new Trace.from(stackTrace);
72 0 : if (terse) trace = trace.terse;
73 0 : return trace.toString();
74 : }
75 :
76 : /// Returns the current stack trace.
77 : ///
78 : /// By default, the first frame of this trace will be the line where
79 : /// [Trace.current] is called. If [level] is passed, the trace will start that
80 : /// many frames up instead.
81 : factory Trace.current([int level = 0]) {
82 0 : if (level < 0) {
83 0 : throw new ArgumentError("Argument [level] must be greater than or equal "
84 : "to 0.");
85 : }
86 :
87 0 : var trace = new Trace.from(StackTrace.current);
88 0 : return new LazyTrace(() {
89 : // JS includes a frame for the call to StackTrace.current, but the VM
90 : // doesn't, so we skip an extra frame in a JS context.
91 0 : return new Trace(trace.frames.skip(level + (inJS ? 2 : 1)),
92 0 : original: trace.original.toString());
93 : });
94 : }
95 :
96 : /// Returns a new stack trace containing the same data as [trace].
97 : ///
98 : /// If [trace] is a native [StackTrace], its data will be parsed out; if it's
99 : /// a [Trace], it will be returned as-is.
100 : factory Trace.from(StackTrace trace) {
101 : // Normally explicitly validating null arguments is bad Dart style, but here
102 : // the natural failure will only occur when the LazyTrace is materialized,
103 : // and we want to provide an error that's more local to the actual problem.
104 : if (trace == null) {
105 0 : throw new ArgumentError("Cannot create a Trace from null.");
106 : }
107 :
108 5 : if (trace is Trace) return trace;
109 0 : if (trace is Chain) return trace.toTrace();
110 0 : return new LazyTrace(() => new Trace.parse(trace.toString()));
111 : }
112 :
113 : /// Parses a string representation of a stack trace.
114 : ///
115 : /// [trace] should be formatted in the same way as a Dart VM or browser stack
116 : /// trace. If it's formatted as a stack chain, this will return the equivalent
117 : /// of [Chain.toTrace].
118 : factory Trace.parse(String trace) {
119 : try {
120 0 : if (trace.isEmpty) return new Trace(<Frame>[]);
121 0 : if (trace.contains(_v8Trace)) return new Trace.parseV8(trace);
122 0 : if (trace.contains("\tat ")) return new Trace.parseJSCore(trace);
123 0 : if (trace.contains(_firefoxSafariTrace)) {
124 0 : return new Trace.parseFirefox(trace);
125 : }
126 0 : if (trace.contains(chainGap)) return new Chain.parse(trace).toTrace();
127 0 : if (trace.contains(_friendlyTrace)) {
128 0 : return new Trace.parseFriendly(trace);
129 : }
130 :
131 : // Default to parsing the stack trace as a VM trace. This is also hit on
132 : // IE and Safari, where the stack trace is just an empty string (issue
133 : // 11257).
134 0 : return new Trace.parseVM(trace);
135 0 : } on FormatException catch (error) {
136 0 : throw new FormatException('${error.message}\nStack trace:\n$trace');
137 : }
138 : }
139 :
140 : /// Parses a string representation of a Dart VM stack trace.
141 0 : Trace.parseVM(String trace) : this(_parseVM(trace), original: trace);
142 :
143 : static List<Frame> _parseVM(String trace) {
144 : // Ignore [vmChainGap]. This matches the behavior of
145 : // `Chain.parse().toTrace()`.
146 0 : var lines = trace.trim().replaceAll(vmChainGap, '').split("\n");
147 : var frames = lines
148 0 : .take(lines.length - 1)
149 0 : .map((line) => new Frame.parseVM(line))
150 0 : .toList();
151 :
152 : // TODO(nweiz): Remove this when issue 23614 is fixed.
153 0 : if (!lines.last.endsWith(".da")) {
154 0 : frames.add(new Frame.parseVM(lines.last));
155 : }
156 :
157 : return frames;
158 : }
159 :
160 : /// Parses a string representation of a Chrome/V8 stack trace.
161 : Trace.parseV8(String trace)
162 0 : : this(
163 : trace
164 0 : .split("\n")
165 0 : .skip(1)
166 : // It's possible that an Exception's description contains a line that
167 : // looks like a V8 trace line, which will screw this up.
168 : // Unfortunately, that's impossible to detect.
169 0 : .skipWhile((line) => !line.startsWith(_v8TraceLine))
170 0 : .map((line) => new Frame.parseV8(line)),
171 : original: trace);
172 :
173 : /// Parses a string representation of a JavaScriptCore stack trace.
174 : Trace.parseJSCore(String trace)
175 0 : : this(
176 : trace
177 0 : .split("\n")
178 0 : .where((line) => line != "\tat ")
179 0 : .map((line) => new Frame.parseV8(line)),
180 : original: trace);
181 :
182 : /// Parses a string representation of an Internet Explorer stack trace.
183 : ///
184 : /// IE10+ traces look just like V8 traces. Prior to IE10, stack traces can't
185 : /// be retrieved.
186 0 : Trace.parseIE(String trace) : this.parseV8(trace);
187 :
188 : /// Parses a string representation of a Firefox stack trace.
189 : Trace.parseFirefox(String trace)
190 0 : : this(
191 : trace
192 0 : .trim()
193 0 : .split("\n")
194 0 : .where((line) => line.isNotEmpty && line != '[native code]')
195 0 : .map((line) => new Frame.parseFirefox(line)),
196 : original: trace);
197 :
198 : /// Parses a string representation of a Safari stack trace.
199 0 : Trace.parseSafari(String trace) : this.parseFirefox(trace);
200 :
201 : /// Parses a string representation of a Safari 6.1+ stack trace.
202 : @Deprecated("Use Trace.parseSafari instead.")
203 0 : Trace.parseSafari6_1(String trace) : this.parseSafari(trace);
204 :
205 : /// Parses a string representation of a Safari 6.0 stack trace.
206 : @Deprecated("Use Trace.parseSafari instead.")
207 : Trace.parseSafari6_0(String trace)
208 0 : : this(
209 : trace
210 0 : .trim()
211 0 : .split("\n")
212 0 : .where((line) => line != '[native code]')
213 0 : .map((line) => new Frame.parseFirefox(line)),
214 : original: trace);
215 :
216 : /// Parses this package's string representation of a stack trace.
217 : ///
218 : /// This also parses string representations of [Chain]s. They parse to the
219 : /// same trace that [Chain.toTrace] would return.
220 : Trace.parseFriendly(String trace)
221 0 : : this(
222 0 : trace.isEmpty
223 0 : ? []
224 : : trace
225 0 : .trim()
226 0 : .split("\n")
227 : // Filter out asynchronous gaps from [Chain]s.
228 0 : .where((line) => !line.startsWith('====='))
229 0 : .map((line) => new Frame.parseFriendly(line)),
230 : original: trace);
231 :
232 : /// Returns a new [Trace] comprised of [frames].
233 : Trace(Iterable<Frame> frames, {String original})
234 0 : : frames = new List<Frame>.unmodifiable(frames),
235 0 : original = new StackTrace.fromString(original);
236 :
237 : /// Returns a VM-style [StackTrace] object.
238 : ///
239 : /// The return value's [toString] method will always return a string
240 : /// representation in the Dart VM's stack trace format, regardless of what
241 : /// platform is being used.
242 0 : StackTrace get vmTrace => new VMTrace(frames);
243 :
244 : /// Returns a terser version of [this].
245 : ///
246 : /// This is accomplished by folding together multiple stack frames from the
247 : /// core library or from this package, as in [foldFrames]. Remaining core
248 : /// library frames have their libraries, "-patch" suffixes, and line numbers
249 : /// removed. If the outermost frame of the stack trace is a core library
250 : /// frame, it's removed entirely.
251 : ///
252 : /// This won't do anything with a raw JavaScript trace, since there's no way
253 : /// to determine which frames come from which Dart libraries. However, the
254 : /// [`source_map_stack_trace`][source_map_stack_trace] package can be used to
255 : /// convert JavaScript traces into Dart-style traces.
256 : ///
257 : /// [source_map_stack_trace]: https://pub.dartlang.org/packages/source_map_stack_trace
258 : ///
259 : /// For custom folding, see [foldFrames].
260 0 : Trace get terse => foldFrames((_) => false, terse: true);
261 :
262 : /// Returns a new [Trace] based on [this] where multiple stack frames matching
263 : /// [predicate] are folded together.
264 : ///
265 : /// This means that whenever there are multiple frames in a row that match
266 : /// [predicate], only the last one is kept. This is useful for limiting the
267 : /// amount of library code that appears in a stack trace by only showing user
268 : /// code and code that's called by user code.
269 : ///
270 : /// If [terse] is true, this will also fold together frames from the core
271 : /// library or from this package, simplify core library frames, and
272 : /// potentially remove the outermost frame as in [Trace.terse].
273 : Trace foldFrames(bool predicate(Frame frame), {bool terse: false}) {
274 : if (terse) {
275 : var oldPredicate = predicate;
276 : predicate = (frame) {
277 0 : if (oldPredicate(frame)) return true;
278 :
279 0 : if (frame.isCore) return true;
280 0 : if (frame.package == 'stack_trace') return true;
281 :
282 : // Ignore async stack frames without any line or column information.
283 : // These come from the VM's async/await implementation and represent
284 : // internal frames. They only ever show up in stack chains and are
285 : // always surrounded by other traces that are actually useful, so we can
286 : // just get rid of them.
287 : // TODO(nweiz): Get rid of this logic some time after issue 22009 is
288 : // fixed.
289 0 : if (!frame.member.contains('<async>')) return false;
290 0 : return frame.line == null;
291 : };
292 : }
293 :
294 0 : var newFrames = <Frame>[];
295 0 : for (var frame in frames.reversed) {
296 0 : if (frame is UnparsedFrame || !predicate(frame)) {
297 0 : newFrames.add(frame);
298 0 : } else if (newFrames.isEmpty || !predicate(newFrames.last)) {
299 : newFrames
300 0 : .add(new Frame(frame.uri, frame.line, frame.column, frame.member));
301 : }
302 : }
303 :
304 : if (terse) {
305 0 : newFrames = newFrames.map((frame) {
306 0 : if (frame is UnparsedFrame || !predicate(frame)) return frame;
307 0 : var library = frame.library.replaceAll(_terseRegExp, '');
308 0 : return new Frame(Uri.parse(library), null, null, frame.member);
309 0 : }).toList();
310 :
311 0 : if (newFrames.length > 1 && predicate(newFrames.first)) {
312 0 : newFrames.removeAt(0);
313 : }
314 : }
315 :
316 0 : return new Trace(newFrames.reversed, original: this.original.toString());
317 : }
318 :
319 : /// Returns a human-readable string representation of [this].
320 : String toString() {
321 : // Figure out the longest path so we know how much to pad.
322 : var longest =
323 0 : frames.map((frame) => frame.location.length).fold(0, math.max);
324 :
325 : // Print out the stack trace nicely formatted.
326 0 : return frames.map((frame) {
327 0 : if (frame is UnparsedFrame) return "$frame\n";
328 0 : return '${frame.location.padRight(longest)} ${frame.member}\n';
329 0 : }).join();
330 : }
331 : }
|