main
js 253 lines 8.25 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 import type {ReactStackTrace} from 'shared/ReactTypes';
11
12 let framesToSkip: number = 0;
13 let collectedStackTrace: null | ReactStackTrace = null;
14
15 const identifierRegExp = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
16
17 function getMethodCallName(callSite: CallSite): string {
18 const typeName = callSite.getTypeName();
19 const methodName = callSite.getMethodName();
20 const functionName = callSite.getFunctionName();
21 let result = '';
22 if (functionName) {
23 if (
24 typeName &&
25 identifierRegExp.test(functionName) &&
26 functionName !== typeName
27 ) {
28 result += typeName + '.';
29 }
30 result += functionName;
31 if (
32 methodName &&
33 functionName !== methodName &&
34 !functionName.endsWith('.' + methodName) &&
35 !functionName.endsWith(' ' + methodName)
36 ) {
37 result += ' [as ' + methodName + ']';
38 }
39 } else {
40 if (typeName) {
41 result += typeName + '.';
42 }
43 if (methodName) {
44 result += methodName;
45 } else {
46 result += '<anonymous>';
47 }
48 }
49 return result;
50 }
51
52 function collectStackTracePrivate(
53 error: Error,
54 structuredStackTrace: CallSite[],
55 ): string {
56 const result: ReactStackTrace = [];
57 // Collect structured stack traces from the callsites.
58 // We mirror how V8 serializes stack frames and how we later parse them.
59 for (let i = framesToSkip; i < structuredStackTrace.length; i++) {
60 const callSite = structuredStackTrace[i];
61 let name = callSite.getFunctionName() || '<anonymous>';
62 if (name.includes('react_stack_bottom_frame')) {
63 // Skip everything after the bottom frame since it'll be internals.
64 break;
65 } else if (callSite.isNative()) {
66 // $FlowFixMe[prop-missing]
67 const isAsync = callSite.isAsync();
68 result.push([name, '', 0, 0, 0, 0, isAsync]);
69 } else {
70 // We encode complex function calls as if they're part of the function
71 // name since we cannot simulate the complex ones and they look the same
72 // as function names in UIs on the client as well as stacks.
73 if (callSite.isConstructor()) {
74 name = 'new ' + name;
75 } else if (!callSite.isToplevel()) {
76 name = getMethodCallName(callSite);
77 }
78 if (name === '<anonymous>') {
79 name = '';
80 }
81 let filename = callSite.getScriptNameOrSourceURL() || '<anonymous>';
82 if (filename === '<anonymous>') {
83 filename = '';
84 if (callSite.isEval()) {
85 const origin = callSite.getEvalOrigin();
86 if (origin) {
87 filename = origin.toString() + ', <anonymous>';
88 }
89 }
90 }
91 const line = callSite.getLineNumber() || 0;
92 const col = callSite.getColumnNumber() || 0;
93 const enclosingLine: number =
94 // $FlowFixMe[prop-missing]
95 typeof callSite.getEnclosingLineNumber === 'function'
96 ? (callSite as any).getEnclosingLineNumber() || 0
97 : 0;
98 const enclosingCol: number =
99 // $FlowFixMe[prop-missing]
100 typeof callSite.getEnclosingColumnNumber === 'function'
101 ? (callSite as any).getEnclosingColumnNumber() || 0
102 : 0;
103 // $FlowFixMe[prop-missing]
104 const isAsync = callSite.isAsync();
105 result.push([
106 name,
107 filename,
108 line,
109 col,
110 enclosingLine,
111 enclosingCol,
112 isAsync,
113 ]);
114 }
115 }
116 collectedStackTrace = result;
117 return '';
118 }
119
120 function collectStackTrace(
121 error: Error,
122 structuredStackTrace: CallSite[],
123 ): string {
124 collectStackTracePrivate(error, structuredStackTrace);
125 // At the same time we generate a string stack trace just in case someone
126 // else reads it. Ideally, we'd call the previous prepareStackTrace to
127 // ensure it's in the expected format but it's common for that to be
128 // source mapped and since we do a lot of eager parsing of errors, it
129 // would be slow in those environments. We could maybe just rely on those
130 // environments having to disable source mapping globally to speed things up.
131 // For now, we just generate a default V8 formatted stack trace without
132 // source mapping as a fallback.
133 const name = error.name || 'Error';
134 const message = error.message || '';
135 let stack = name + ': ' + message;
136 for (let i = 0; i < structuredStackTrace.length; i++) {
137 stack += '\n at ' + structuredStackTrace[i].toString();
138 }
139 return stack;
140 }
141
142 // This matches either of these V8 formats.
143 // at name (filename:0:0)
144 // at filename:0:0
145 // at async filename:0:0
146 const frameRegExp =
147 /^ {3} at (?:(.+) \((?:(.+):(\d+):(\d+)|\<anonymous\>)\)|(?:async )?(.+):(\d+):(\d+)|\<anonymous\>)$/;
148
149 // DEV-only cache of parsed and filtered stack frames.
150 const stackTraceCache: WeakMap<Error, ReactStackTrace> = __DEV__
151 ? new WeakMap()
152 : (null as any);
153
154 // This version is only used when React fully owns the Error object and there's no risk of it having
155 // been already initialized and no risky that anyone else will initialize it later.
156 export function parseStackTracePrivate(
157 error: Error,
158 skipFrames: number,
159 ): null | ReactStackTrace {
160 collectedStackTrace = null;
161 framesToSkip = skipFrames;
162 const previousPrepare = Error.prepareStackTrace;
163 Error.prepareStackTrace = collectStackTracePrivate;
164 try {
165 if (error.stack !== '') {
166 return null;
167 }
168 } finally {
169 Error.prepareStackTrace = previousPrepare;
170 }
171 return collectedStackTrace;
172 }
173
174 export function parseStackTrace(
175 error: Error,
176 skipFrames: number,
177 ): ReactStackTrace {
178 // We can only get structured data out of error objects once. So we cache the information
179 // so we can get it again each time. It also helps performance when the same error is
180 // referenced more than once.
181 const existing = stackTraceCache.get(error);
182 if (existing !== undefined) {
183 return existing;
184 }
185 // We override Error.prepareStackTrace with our own version that collects
186 // the structured data. We need more information than the raw stack gives us
187 // and we need to ensure that we don't get the source mapped version.
188 collectedStackTrace = null;
189 framesToSkip = skipFrames;
190 const previousPrepare = Error.prepareStackTrace;
191 Error.prepareStackTrace = collectStackTrace;
192 let stack;
193 try {
194 // eslint-disable-next-line react-internal/safe-string-coercion
195 stack = String(error.stack);
196 } finally {
197 Error.prepareStackTrace = previousPrepare;
198 }
199
200 if (collectedStackTrace !== null) {
201 const result = collectedStackTrace;
202 collectedStackTrace = null;
203 stackTraceCache.set(error, result);
204 return result;
205 }
206
207 // If the stack has already been read, or this is not actually a V8 compatible
208 // engine then we might not get a normalized stack and it might still have been
209 // source mapped. Regardless we try our best to parse it. This works best if the
210 // environment just uses default V8 formatting and no source mapping.
211
212 if (stack.startsWith('Error: react-stack-top-frame\n')) {
213 // V8's default formatting prefixes with the error message which we
214 // don't want/need.
215 stack = stack.slice(29);
216 }
217 let idx = stack.indexOf('react_stack_bottom_frame');
218 if (idx !== -1) {
219 idx = stack.lastIndexOf('\n', idx);
220 }
221 if (idx !== -1) {
222 // Cut off everything after the bottom frame since it'll be internals.
223 stack = stack.slice(0, idx);
224 }
225 const frames = stack.split('\n');
226 const parsedFrames: ReactStackTrace = [];
227 // We skip top frames here since they may or may not be parseable but we
228 // want to skip the same number of frames regardless. I.e. we can't do it
229 // in the caller.
230 for (let i = skipFrames; i < frames.length; i++) {
231 const parsed = frameRegExp.exec(frames[i]);
232 if (!parsed) {
233 continue;
234 }
235 let name = parsed[1] || '';
236 let isAsync = parsed[8] === 'async ';
237 if (name === '<anonymous>') {
238 name = '';
239 } else if (name.startsWith('async ')) {
240 name = name.slice(5);
241 isAsync = true;
242 }
243 let filename = parsed[2] || parsed[5] || '';
244 if (filename === '<anonymous>') {
245 filename = '';
246 }
247 const line = +(parsed[3] || parsed[6]);
248 const col = +(parsed[4] || parsed[7]);
249 parsedFrames.push([name, filename, line, col, 0, 0, isAsync]);
250 }
251 stackTraceCache.set(error, parsedFrames);
252 return parsedFrames;
253 }