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:async';
6 : import 'dart:math' as math;
7 :
8 : import 'frame.dart';
9 : import 'lazy_chain.dart';
10 : import 'stack_zone_specification.dart';
11 : import 'trace.dart';
12 : import 'utils.dart';
13 :
14 : /// A function that handles errors in the zone wrapped by [Chain.capture].
15 : @Deprecated('Will be removed in stack_trace 2.0.0.')
16 : typedef ChainHandler = void Function(dynamic error, Chain chain);
17 :
18 : /// An opaque key used to track the current [StackZoneSpecification].
19 33 : final _specKey = Object();
20 :
21 : /// A chain of stack traces.
22 : ///
23 : /// A stack chain is a collection of one or more stack traces that collectively
24 : /// represent the path from [main] through nested function calls to a particular
25 : /// code location, usually where an error was thrown. Multiple stack traces are
26 : /// necessary when using asynchronous functions, since the program's stack is
27 : /// reset before each asynchronous callback is run.
28 : ///
29 : /// Stack chains can be automatically tracked using [Chain.capture]. This sets
30 : /// up a new [Zone] in which the current stack chain is tracked and can be
31 : /// accessed using [new Chain.current]. Any errors that would be top-leveled in
32 : /// the zone can be handled, along with their associated chains, with the
33 : /// `onError` callback. For example:
34 : ///
35 : /// Chain.capture(() {
36 : /// // ...
37 : /// }, onError: (error, stackChain) {
38 : /// print("Caught error $error\n"
39 : /// "$stackChain");
40 : /// });
41 : class Chain implements StackTrace {
42 : /// The stack traces that make up this chain.
43 : ///
44 : /// Like the frames in a stack trace, the traces are ordered from most local
45 : /// to least local. The first one is the trace where the actual exception was
46 : /// raised, the second one is where that callback was scheduled, and so on.
47 : final List<Trace> traces;
48 :
49 : /// The [StackZoneSpecification] for the current zone.
50 11 : static StackZoneSpecification? get _currentSpec =>
51 33 : Zone.current[_specKey] as StackZoneSpecification?;
52 :
53 : /// If [when] is `true`, runs [callback] in a [Zone] in which the current
54 : /// stack chain is tracked and automatically associated with (most) errors.
55 : ///
56 : /// If [when] is `false`, this does not track stack chains. Instead, it's
57 : /// identical to [runZoned], except that it wraps any errors in [new
58 : /// Chain.forTrace]—which will only wrap the trace unless there's a different
59 : /// [Chain.capture] active. This makes it easy for the caller to only capture
60 : /// stack chains in debug mode or during development.
61 : ///
62 : /// If [onError] is passed, any error in the zone that would otherwise go
63 : /// unhandled is passed to it, along with the [Chain] associated with that
64 : /// error. Note that if [callback] produces multiple unhandled errors,
65 : /// [onError] may be called more than once. If [onError] isn't passed, the
66 : /// parent Zone's `unhandledErrorHandler` will be called with the error and
67 : /// its chain.
68 : ///
69 : /// The zone this creates will be an error zone if either [onError] is
70 : /// not `null` and [when] is false,
71 : /// or if both [when] and [errorZone] are `true`.
72 : /// If [errorZone] is `false`, [onError] must be `null`.
73 : ///
74 : /// If [callback] returns a value, it will be returned by [capture] as well.
75 11 : static T capture<T>(T Function() callback,
76 : {void Function(Object error, Chain)? onError,
77 : bool when = true,
78 : bool errorZone = true}) {
79 : if (!errorZone && onError != null) {
80 0 : throw ArgumentError.value(
81 : onError, 'onError', 'must be null if errorZone is false');
82 : }
83 :
84 : if (!when) {
85 11 : if (onError == null) return runZoned(callback);
86 0 : return runZonedGuarded(callback, (error, stackTrace) {
87 0 : onError(error, Chain.forTrace(stackTrace));
88 : }) as T;
89 : }
90 :
91 0 : var spec = StackZoneSpecification(onError, errorZone: errorZone);
92 0 : return runZoned(() {
93 : try {
94 : return callback();
95 : } on Object catch (error, stackTrace) {
96 : // TODO(nweiz): Don't special-case this when issue 19566 is fixed.
97 0 : Zone.current.handleUncaughtError(error, stackTrace);
98 :
99 : // If the expected return type of capture() is not nullable, this will
100 : // throw a cast exception. But the only other alternative is to throw
101 : // some other exception. Casting null to T at least lets existing uses
102 : // where T is a nullable type continue to work.
103 : return null as T;
104 : }
105 : },
106 0 : zoneSpecification: spec.toSpec(),
107 0 : zoneValues: {_specKey: spec, StackZoneSpecification.disableKey: false});
108 : }
109 :
110 : /// If [when] is `true` and this is called within a [Chain.capture] zone, runs
111 : /// [callback] in a [Zone] in which chain capturing is disabled.
112 : ///
113 : /// If [callback] returns a value, it will be returned by [disable] as well.
114 0 : static T disable<T>(T Function() callback, {bool when = true}) {
115 : var zoneValues =
116 0 : when ? {_specKey: null, StackZoneSpecification.disableKey: true} : null;
117 :
118 0 : return runZoned(callback, zoneValues: zoneValues);
119 : }
120 :
121 : /// Returns [futureOrStream] unmodified.
122 : ///
123 : /// Prior to Dart 1.7, this was necessary to ensure that stack traces for
124 : /// exceptions reported with [Completer.completeError] and
125 : /// [StreamController.addError] were tracked correctly.
126 0 : @Deprecated('Chain.track is not necessary in Dart 1.7+.')
127 : static dynamic track(futureOrStream) => futureOrStream;
128 :
129 : /// Returns the current stack chain.
130 : ///
131 : /// By default, the first frame of the first trace will be the line where
132 : /// [Chain.current] is called. If [level] is passed, the first trace will
133 : /// start that many frames up instead.
134 : ///
135 : /// If this is called outside of a [capture] zone, it just returns a
136 : /// single-trace chain.
137 0 : factory Chain.current([int level = 0]) {
138 0 : if (_currentSpec != null) return _currentSpec!.currentChain(level + 1);
139 :
140 0 : var chain = Chain.forTrace(StackTrace.current);
141 0 : return LazyChain(() {
142 : // JS includes a frame for the call to StackTrace.current, but the VM
143 : // doesn't, so we skip an extra frame in a JS context.
144 0 : var first = Trace(chain.traces.first.frames.skip(level + (inJS ? 2 : 1)),
145 0 : original: chain.traces.first.original.toString());
146 0 : return Chain([first, ...chain.traces.skip(1)]);
147 : });
148 : }
149 :
150 : /// Returns the stack chain associated with [trace].
151 : ///
152 : /// The first stack trace in the returned chain will always be [trace]
153 : /// (converted to a [Trace] if necessary). If there is no chain associated
154 : /// with [trace] or if this is called outside of a [capture] zone, this just
155 : /// returns a single-trace chain containing [trace].
156 : ///
157 : /// If [trace] is already a [Chain], it will be returned as-is.
158 11 : factory Chain.forTrace(StackTrace trace) {
159 11 : if (trace is Chain) return trace;
160 11 : if (_currentSpec != null) return _currentSpec!.chainFor(trace);
161 33 : if (trace is Trace) return Chain([trace]);
162 0 : return LazyChain(() => Chain.parse(trace.toString()));
163 : }
164 :
165 : /// Parses a string representation of a stack chain.
166 : ///
167 : /// If [chain] is the output of a call to [Chain.toString], it will be parsed
168 : /// as a full stack chain. Otherwise, it will be parsed as in [Trace.parse]
169 : /// and returned as a single-trace chain.
170 0 : factory Chain.parse(String chain) {
171 0 : if (chain.isEmpty) return Chain([]);
172 0 : if (chain.contains(vmChainGap)) {
173 0 : return Chain(chain
174 0 : .split(vmChainGap)
175 0 : .where((line) => line.isNotEmpty)
176 0 : .map((trace) => Trace.parseVM(trace)));
177 : }
178 0 : if (!chain.contains(chainGap)) return Chain([Trace.parse(chain)]);
179 :
180 0 : return Chain(
181 0 : chain.split(chainGap).map((trace) => Trace.parseFriendly(trace)));
182 : }
183 :
184 : /// Returns a new [Chain] comprised of [traces].
185 22 : Chain(Iterable<Trace> traces) : traces = List<Trace>.unmodifiable(traces);
186 :
187 : /// Returns a terser version of [this].
188 : ///
189 : /// This calls [Trace.terse] on every trace in [traces], and discards any
190 : /// trace that contain only internal frames.
191 : ///
192 : /// This won't do anything with a raw JavaScript trace, since there's no way
193 : /// to determine which frames come from which Dart libraries. However, the
194 : /// [`source_map_stack_trace`][source_map_stack_trace] package can be used to
195 : /// convert JavaScript traces into Dart-style traces.
196 : ///
197 : /// [source_map_stack_trace]: https://pub.dev/packages/source_map_stack_trace
198 0 : Chain get terse => foldFrames((_) => false, terse: true);
199 :
200 : /// Returns a new [Chain] based on [this] where multiple stack frames matching
201 : /// [predicate] are folded together.
202 : ///
203 : /// This means that whenever there are multiple frames in a row that match
204 : /// [predicate], only the last one is kept. In addition, traces that are
205 : /// composed entirely of frames matching [predicate] are omitted.
206 : ///
207 : /// This is useful for limiting the amount of library code that appears in a
208 : /// stack trace by only showing user code and code that's called by user code.
209 : ///
210 : /// If [terse] is true, this will also fold together frames from the core
211 : /// library or from this package, and simplify core library frames as in
212 : /// [Trace.terse].
213 11 : Chain foldFrames(bool Function(Frame) predicate, {bool terse = false}) {
214 : var foldedTraces =
215 44 : traces.map((trace) => trace.foldFrames(predicate, terse: terse));
216 22 : var nonEmptyTraces = foldedTraces.where((trace) {
217 : // Ignore traces that contain only folded frames.
218 33 : if (trace.frames.length > 1) return true;
219 22 : if (trace.frames.isEmpty) return false;
220 :
221 : // In terse mode, the trace may have removed an outer folded frame,
222 : // leaving a single non-folded frame. We can detect a folded frame because
223 : // it has no line information.
224 : if (!terse) return false;
225 33 : return trace.frames.single.line != null;
226 : });
227 :
228 : // If all the traces contain only internal processing, preserve the last
229 : // (top-most) one so that the chain isn't empty.
230 11 : if (nonEmptyTraces.isEmpty && foldedTraces.isNotEmpty) {
231 0 : return Chain([foldedTraces.last]);
232 : }
233 :
234 11 : return Chain(nonEmptyTraces);
235 : }
236 :
237 : /// Converts [this] to a [Trace].
238 : ///
239 : /// The trace version of a chain is just the concatenation of all the traces
240 : /// in the chain.
241 0 : Trace toTrace() => Trace(traces.expand((trace) => trace.frames));
242 :
243 11 : @override
244 : String toString() {
245 : // Figure out the longest path so we know how much to pad.
246 33 : var longest = traces.map((trace) {
247 11 : return trace.frames
248 44 : .map((frame) => frame.location.length)
249 11 : .fold(0, math.max);
250 11 : }).fold(0, math.max);
251 :
252 : // Don't call out to [Trace.toString] here because that doesn't ensure that
253 : // padding is consistent across all traces.
254 33 : return traces.map((trace) {
255 33 : return trace.frames.map((frame) {
256 44 : return '${frame.location.padRight(longest)} ${frame.member}\n';
257 11 : }).join();
258 11 : }).join(chainGap);
259 : }
260 : }
|