1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:meta/meta.dart';
6
7import 'constants.dart';
8import 'object.dart';
9
10/// A object representation of a frame from a stack trace.
11///
12/// {@tool snippet}
13///
14/// This example creates a traversable list of parsed [StackFrame] objects from
15/// the current [StackTrace].
16///
17/// ```dart
18/// final List<StackFrame> currentFrames = StackFrame.fromStackTrace(StackTrace.current);
19/// ```
20/// {@end-tool}
21@immutable
22class StackFrame {
23 /// Creates a new StackFrame instance.
24 ///
25 /// The [className] may be the empty string if there is no class (e.g. for a
26 /// top level library method).
27 const StackFrame({
28 required this.number,
29 required this.column,
30 required this.line,
31 required this.packageScheme,
32 required this.package,
33 required this.packagePath,
34 this.className = '',
35 required this.method,
36 this.isConstructor = false,
37 required this.source,
38 });
39
40 /// A stack frame representing an asynchronous suspension.
41 static const StackFrame asynchronousSuspension = StackFrame(
42 number: -1,
43 column: -1,
44 line: -1,
45 method: 'asynchronous suspension',
46 packageScheme: '',
47 package: '',
48 packagePath: '',
49 source: '<asynchronous suspension>',
50 );
51
52 /// A stack frame representing a Dart elided stack overflow frame.
53 static const StackFrame stackOverFlowElision = StackFrame(
54 number: -1,
55 column: -1,
56 line: -1,
57 method: '...',
58 packageScheme: '',
59 package: '',
60 packagePath: '',
61 source: '...',
62 );
63
64 /// Parses a list of [StackFrame]s from a [StackTrace] object.
65 ///
66 /// This is normally useful with [StackTrace.current].
67 static List<StackFrame> fromStackTrace(StackTrace stack) {
68 return fromStackString(stack.toString());
69 }
70
71 /// Parses a list of [StackFrame]s from the [StackTrace.toString] method.
72 static List<StackFrame> fromStackString(String stack) {
73 return stack
74 .trim()
75 .split('\n')
76 .where((String line) => line.isNotEmpty)
77 .map(fromStackTraceLine)
78 // On the Web in non-debug builds the stack trace includes the exception
79 // message that precedes the stack trace itself. fromStackTraceLine will
80 // return null in that case. We will skip it here.
81 // TODO(polina-c): if one of lines was parsed to null, the entire stack trace
82 // is in unexpected format and should be returned as is, without partial parsing.
83 // https://github.com/flutter/flutter/issues/131877
84 .whereType<StackFrame>()
85 .toList();
86 }
87
88 /// Parses a single [StackFrame] from a line of a [StackTrace].
89 ///
90 /// Returns null if format is not as expected.
91 static StackFrame? _tryParseWebFrame(String line) {
92 if (kDebugMode) {
93 return _tryParseWebDebugFrame(line);
94 } else {
95 return _tryParseWebNonDebugFrame(line);
96 }
97 }
98
99 /// Parses a single [StackFrame] from a line of a [StackTrace].
100 ///
101 /// Returns null if format is not as expected.
102 static StackFrame? _tryParseWebDebugFrame(String line) {
103 // This RegExp is only partially correct for flutter run/test differences.
104 // https://github.com/flutter/flutter/issues/52685
105 final bool hasPackage = line.startsWith('package');
106 final RegExp parser = hasPackage
107 ? RegExp(r'^(package.+) (\d+):(\d+)\s+(.+)$')
108 : RegExp(r'^(.+) (\d+):(\d+)\s+(.+)$');
109
110 final Match? match = parser.firstMatch(line);
111
112 if (match == null) {
113 return null;
114 }
115
116 String package = '<unknown>';
117 String packageScheme = '<unknown>';
118 String packagePath = '<unknown>';
119
120 if (hasPackage) {
121 packageScheme = 'package';
122 final Uri packageUri = Uri.parse(match.group(1)!);
123 package = packageUri.pathSegments[0];
124 packagePath = packageUri.path.replaceFirst('${packageUri.pathSegments[0]}/', '');
125 }
126
127 return StackFrame(
128 number: -1,
129 packageScheme: packageScheme,
130 package: package,
131 packagePath: packagePath,
132 line: int.parse(match.group(2)!),
133 column: int.parse(match.group(3)!),
134 className: '<unknown>',
135 method: match.group(4)!,
136 source: line,
137 );
138 }
139
140 // Non-debug builds do not point to dart code but compiled JavaScript, so
141 // line numbers are meaningless. We only attempt to parse the class and
142 // method name, which is more or less readable in profile builds, and
143 // minified in release builds.
144 static final RegExp _webNonDebugFramePattern = RegExp(r'^\s*at ([^\s]+).*$');
145
146 // Parses `line` as a stack frame in profile and release Web builds. If not
147 // recognized as a stack frame, returns null.
148 static StackFrame? _tryParseWebNonDebugFrame(String line) {
149 final Match? match = _webNonDebugFramePattern.firstMatch(line);
150 if (match == null) {
151 // On the Web in non-debug builds the stack trace includes the exception
152 // message that precedes the stack trace itself. Example:
153 //
154 // TypeError: Cannot read property 'hello$0' of null
155 // at _GalleryAppState.build$1 (http://localhost:8080/main.dart.js:149790:13)
156 // at StatefulElement.build$0 (http://localhost:8080/main.dart.js:129138:37)
157 // at StatefulElement.performRebuild$0 (http://localhost:8080/main.dart.js:129032:23)
158 //
159 // Instead of crashing when a line is not recognized as a stack frame, we
160 // return null. The caller, such as fromStackString, can then just skip
161 // this frame.
162 return null;
163 }
164
165 final List<String> classAndMethod = match.group(1)!.split('.');
166 final String className = classAndMethod.length > 1 ? classAndMethod.first : '<unknown>';
167 final String method = classAndMethod.length > 1
168 ? classAndMethod.skip(1).join('.')
169 : classAndMethod.single;
170
171 return StackFrame(
172 number: -1,
173 packageScheme: '<unknown>',
174 package: '<unknown>',
175 packagePath: '<unknown>',
176 line: -1,
177 column: -1,
178 className: className,
179 method: method,
180 source: line,
181 );
182 }
183
184 /// Parses a single [StackFrame] from a single line of a [StackTrace].
185 ///
186 /// Returns null if format is not as expected.
187 static StackFrame? fromStackTraceLine(String line) {
188 if (line == '<asynchronous suspension>') {
189 return asynchronousSuspension;
190 } else if (line == '...') {
191 return stackOverFlowElision;
192 }
193
194 assert(
195 line != '===== asynchronous gap ===========================',
196 'Got a stack frame from package:stack_trace, where a vm or web frame was expected. '
197 'This can happen if FlutterError.demangleStackTrace was not set in an environment '
198 'that propagates non-standard stack traces to the framework, such as during tests.',
199 );
200
201 // Web frames.
202 if (!line.startsWith('#')) {
203 return _tryParseWebFrame(line);
204 }
205
206 final RegExp parser = RegExp(r'^#(\d+) +(.+) \((.+?):?(\d+){0,1}:?(\d+){0,1}\)$');
207 Match? match = parser.firstMatch(line);
208 assert(match != null, 'Expected $line to match $parser.');
209 match = match!;
210
211 bool isConstructor = false;
212 String className = '';
213 String method = match.group(2)!.replaceAll('.<anonymous closure>', '');
214 if (method.startsWith('new')) {
215 final List<String> methodParts = method.split(' ');
216 // Sometimes a web frame will only read "new" and have no class name.
217 className = methodParts.length > 1 ? method.split(' ')[1] : '<unknown>';
218 method = '';
219 if (className.contains('.')) {
220 final List<String> parts = className.split('.');
221 className = parts[0];
222 method = parts[1];
223 }
224 isConstructor = true;
225 } else if (method.contains('.')) {
226 final List<String> parts = method.split('.');
227 className = parts[0];
228 method = parts[1];
229 }
230
231 final Uri packageUri = Uri.parse(match.group(3)!);
232 String package = '<unknown>';
233 String packagePath = packageUri.path;
234 if (packageUri.scheme == 'dart' || packageUri.scheme == 'package') {
235 package = packageUri.pathSegments[0];
236 packagePath = packageUri.path.replaceFirst('${packageUri.pathSegments[0]}/', '');
237 }
238
239 return StackFrame(
240 number: int.parse(match.group(1)!),
241 className: className,
242 method: method,
243 packageScheme: packageUri.scheme,
244 package: package,
245 packagePath: packagePath,
246 line: match.group(4) == null ? -1 : int.parse(match.group(4)!),
247 column: match.group(5) == null ? -1 : int.parse(match.group(5)!),
248 isConstructor: isConstructor,
249 source: line,
250 );
251 }
252
253 /// The original source of this stack frame.
254 final String source;
255
256 /// The zero-indexed frame number.
257 ///
258 /// This value may be -1 to indicate an unknown frame number.
259 final int number;
260
261 /// The scheme of the package for this frame, e.g. "dart" for
262 /// dart:core/errors_patch.dart or "package" for
263 /// package:flutter/src/widgets/text.dart.
264 ///
265 /// The path property refers to the source file.
266 final String packageScheme;
267
268 /// The package for this frame, e.g. "core" for
269 /// dart:core/errors_patch.dart or "flutter" for
270 /// package:flutter/src/widgets/text.dart.
271 final String package;
272
273 /// The path of the file for this frame, e.g. "errors_patch.dart" for
274 /// dart:core/errors_patch.dart or "src/widgets/text.dart" for
275 /// package:flutter/src/widgets/text.dart.
276 final String packagePath;
277
278 /// The source line number.
279 final int line;
280
281 /// The source column number.
282 final int column;
283
284 /// The class name, if any, for this frame.
285 ///
286 /// This may be null for top level methods in a library or anonymous closure
287 /// methods.
288 final String className;
289
290 /// The method name for this frame.
291 ///
292 /// This will be an empty string if the stack frame is from the default
293 /// constructor.
294 final String method;
295
296 /// Whether or not this was thrown from a constructor.
297 final bool isConstructor;
298
299 @override
300 int get hashCode => Object.hash(number, package, line, column, className, method, source);
301
302 @override
303 bool operator ==(Object other) {
304 if (other.runtimeType != runtimeType) {
305 return false;
306 }
307 return other is StackFrame
308 && other.number == number
309 && other.package == package
310 && other.line == line
311 && other.column == column
312 && other.className == className
313 && other.method == method
314 && other.source == source;
315 }
316
317 @override
318 String toString() => '${objectRuntimeType(this, 'StackFrame')}(#$number, $packageScheme:$package/$packagePath:$line:$column, className: $className, method: $method)';
319}
320