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 | |
5 | import 'package:meta/meta.dart' ; |
6 | |
7 | import 'constants.dart'; |
8 | import '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 |
22 | class 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 | |