main
js 333 lines 12 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 {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
11
12 import ReactSharedInternals from 'shared/ReactSharedInternals';
13
14 import DefaultPrepareStackTrace from 'shared/DefaultPrepareStackTrace';
15
16 import {formatOwnerStack} from './ReactOwnerStackFrames';
17
18 let prefix;
19 let suffix;
20 export function describeBuiltInComponentFrame(name: string): string {
21 if (prefix === undefined) {
22 // Extract the VM specific prefix used by each line.
23 try {
24 throw Error();
25 } catch (x) {
26 const match = x.stack.trim().match(/\n( *(at )?)/);
27 prefix = (match && match[1]) || '';
28 suffix =
29 x.stack.indexOf('\n at') > -1
30 ? // V8
31 ' (<anonymous>)'
32 : // JSC/Spidermonkey
33 x.stack.indexOf('@') > -1
34 ? '@unknown:0:0'
35 : // Other
36 '';
37 }
38 }
39 // We use the prefix to ensure our stacks line up with native stack frames.
40 return '\n' + prefix + name + suffix;
41 }
42
43 export function describeDebugInfoFrame(
44 name: string,
45 env: ?string,
46 location: ?Error,
47 ): string {
48 if (location != null) {
49 // If we have a location, it's the child's owner stack. Treat the bottom most frame as
50 // the location of this function.
51 const childStack = formatOwnerStack(location);
52 const idx = childStack.lastIndexOf('\n');
53 const lastLine = idx === -1 ? childStack : childStack.slice(idx + 1);
54 if (lastLine.indexOf(name) !== -1) {
55 // For async stacks it's possible we don't have the owner on it. As a precaution only
56 // use this frame if it has the name of the function in it.
57 return '\n' + lastLine;
58 }
59 }
60
61 return describeBuiltInComponentFrame(name + (env ? ' [' + env + ']' : ''));
62 }
63
64 let reentry = false;
65 let componentFrameCache;
66 if (__DEV__) {
67 const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
68 componentFrameCache = new PossiblyWeakMap<Function, string>();
69 }
70
71 /**
72 * Leverages native browser/VM stack frames to get proper details (e.g.
73 * filename, line + col number) for a single component in a component stack. We
74 * do this by:
75 * (1) throwing and catching an error in the function - this will be our
76 * control error.
77 * (2) calling the component which will eventually throw an error that we'll
78 * catch - this will be our sample error.
79 * (3) diffing the control and sample error stacks to find the stack frame
80 * which represents our component.
81 */
82 export function describeNativeComponentFrame(
83 fn: Function,
84 construct: boolean,
85 ): string {
86 // If something asked for a stack inside a fake render, it should get ignored.
87 if (!fn || reentry) {
88 return '';
89 }
90
91 if (__DEV__) {
92 const frame = componentFrameCache.get(fn);
93 if (frame !== undefined) {
94 return frame;
95 }
96 }
97
98 reentry = true;
99 const previousPrepareStackTrace = Error.prepareStackTrace;
100 Error.prepareStackTrace = DefaultPrepareStackTrace;
101 let previousDispatcher = null;
102
103 if (__DEV__) {
104 previousDispatcher = ReactSharedInternals.H;
105 // Set the dispatcher in DEV because this might be call in the render function
106 // for warnings.
107 ReactSharedInternals.H = null;
108 disableLogs();
109 }
110 try {
111 /**
112 * Finding a common stack frame between sample and control errors can be
113 * tricky given the different types and levels of stack trace truncation from
114 * different JS VMs. So instead we'll attempt to control what that common
115 * frame should be through this object method:
116 * Having both the sample and control errors be in the function under the
117 * `DescribeNativeComponentFrameRoot` property, + setting the `name` and
118 * `displayName` properties of the function ensures that a stack
119 * frame exists that has the method name `DescribeNativeComponentFrameRoot` in
120 * it for both control and sample stacks.
121 */
122 const RunInRootFrame = {
123 DetermineComponentFrameRoot(): [?string, ?string] {
124 let control;
125 try {
126 // This should throw.
127 if (construct) {
128 // Something should be setting the props in the constructor.
129 const Fake = function () {
130 throw Error();
131 };
132 // $FlowFixMe[prop-missing]
133 Object.defineProperty(Fake.prototype, 'props', {
134 set: function () {
135 // We use a throwing setter instead of frozen or non-writable props
136 // because that won't throw in a non-strict mode function.
137 throw Error();
138 },
139 });
140 if (typeof Reflect === 'object' && Reflect.construct) {
141 // We construct a different control for this case to include any extra
142 // frames added by the construct call.
143 try {
144 Reflect.construct(Fake, []);
145 } catch (x) {
146 control = x;
147 }
148 Reflect.construct(fn, [], Fake);
149 } else {
150 try {
151 Fake.call();
152 } catch (x) {
153 control = x;
154 }
155
156 let prototypeModified = false;
157 let prevProps;
158 try {
159 prevProps = Object.getOwnPropertyDescriptor(
160 fn.prototype,
161 'props',
162 );
163 Object.defineProperty(fn.prototype, 'props', {
164 configurable: true,
165 set() {
166 throw Error();
167 },
168 });
169 prototypeModified = true;
170
171 // eslint-disable-next-line no-new
172 new fn();
173 } finally {
174 if (prototypeModified) {
175 if (prevProps !== undefined) {
176 Object.defineProperty(fn.prototype, 'props', prevProps);
177 } else {
178 delete fn.prototype.props;
179 }
180 }
181 }
182 }
183 } else {
184 try {
185 throw Error();
186 } catch (x) {
187 control = x;
188 }
189 // TODO(luna): This will currently only throw if the function component
190 // tries to access React/ReactDOM/props. We should probably make this throw
191 // in simple components too
192 const maybePromise = fn();
193
194 // If the function component returns a promise, it's likely an async
195 // component, which we don't yet support. Attach a noop catch handler to
196 // silence the error.
197 // TODO: Implement component stacks for async client components?
198 if (maybePromise && typeof maybePromise.catch === 'function') {
199 maybePromise.catch(() => {});
200 }
201 }
202 } catch (sample) {
203 // This is inlined manually because closure doesn't do it for us.
204 if (sample && control && typeof sample.stack === 'string') {
205 return [sample.stack, control.stack];
206 }
207 }
208 return [null, null];
209 },
210 };
211 // $FlowFixMe[prop-missing]
212 RunInRootFrame.DetermineComponentFrameRoot.displayName =
213 'DetermineComponentFrameRoot';
214 const namePropDescriptor = Object.getOwnPropertyDescriptor(
215 RunInRootFrame.DetermineComponentFrameRoot,
216 'name',
217 );
218 // Before ES6, the `name` property was not configurable.
219 if (namePropDescriptor && namePropDescriptor.configurable) {
220 // V8 utilizes a function's `name` property when generating a stack trace.
221 Object.defineProperty(
222 RunInRootFrame.DetermineComponentFrameRoot,
223 // Configurable properties can be updated even if its writable descriptor
224 // is set to `false`.
225 // $FlowFixMe[cannot-write]
226 'name',
227 {value: 'DetermineComponentFrameRoot'},
228 );
229 }
230
231 const [sampleStack, controlStack] =
232 RunInRootFrame.DetermineComponentFrameRoot();
233 if (sampleStack && controlStack) {
234 // This extracts the first frame from the sample that isn't also in the control.
235 // Skipping one frame that we assume is the frame that calls the two.
236 const sampleLines = sampleStack.split('\n');
237 const controlLines = controlStack.split('\n');
238 let s = 0;
239 let c = 0;
240 while (
241 s < sampleLines.length &&
242 !sampleLines[s].includes('DetermineComponentFrameRoot')
243 ) {
244 s++;
245 }
246 while (
247 c < controlLines.length &&
248 !controlLines[c].includes('DetermineComponentFrameRoot')
249 ) {
250 c++;
251 }
252 // We couldn't find our intentionally injected common root frame, attempt
253 // to find another common root frame by search from the bottom of the
254 // control stack...
255 if (s === sampleLines.length || c === controlLines.length) {
256 s = sampleLines.length - 1;
257 c = controlLines.length - 1;
258 while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) {
259 // We expect at least one stack frame to be shared.
260 // Typically this will be the root most one. However, stack frames may be
261 // cut off due to maximum stack limits. In this case, one maybe cut off
262 // earlier than the other. We assume that the sample is longer or the same
263 // and there for cut off earlier. So we should find the root most frame in
264 // the sample somewhere in the control.
265 c--;
266 }
267 }
268 for (; s >= 1 && c >= 0; s--, c--) {
269 // Next we find the first one that isn't the same which should be the
270 // frame that called our sample function and the control.
271 if (sampleLines[s] !== controlLines[c]) {
272 // In V8, the first line is describing the message but other VMs don't.
273 // If we're about to return the first line, and the control is also on the same
274 // line, that's a pretty good indicator that our sample threw at same line as
275 // the control. I.e. before we entered the sample frame. So we ignore this result.
276 // This can happen if you passed a class to function component, or non-function.
277 if (s !== 1 || c !== 1) {
278 do {
279 s--;
280 c--;
281 // We may still have similar intermediate frames from the construct call.
282 // The next one that isn't the same should be our match though.
283 if (c < 0 || sampleLines[s] !== controlLines[c]) {
284 // V8 adds a "new" prefix for native classes. Let's remove it to make it prettier.
285 let frame = '\n' + sampleLines[s].replace(' at new ', ' at ');
286
287 // If our component frame is labeled "<anonymous>"
288 // but we have a user-provided "displayName"
289 // splice it in to make the stack more readable.
290 if (fn.displayName && frame.includes('<anonymous>')) {
291 frame = frame.replace('<anonymous>', fn.displayName);
292 }
293
294 if (__DEV__) {
295 if (typeof fn === 'function') {
296 componentFrameCache.set(fn, frame);
297 }
298 }
299 // Return the line we found.
300 return frame;
301 }
302 } while (s >= 1 && c >= 0);
303 }
304 break;
305 }
306 }
307 }
308 } finally {
309 reentry = false;
310 if (__DEV__) {
311 ReactSharedInternals.H = previousDispatcher;
312 reenableLogs();
313 }
314 Error.prepareStackTrace = previousPrepareStackTrace;
315 }
316 // Fallback to just using the name if we couldn't make it throw.
317 const name = fn ? fn.displayName || fn.name : '';
318 const syntheticFrame = name ? describeBuiltInComponentFrame(name) : '';
319 if (__DEV__) {
320 if (typeof fn === 'function') {
321 componentFrameCache.set(fn, syntheticFrame);
322 }
323 }
324 return syntheticFrame;
325 }
326
327 export function describeClassComponentFrame(ctor: Function): string {
328 return describeNativeComponentFrame(ctor, true);
329 }
330
331 export function describeFunctionComponentFrame(fn: Function): string {
332 return describeNativeComponentFrame(fn, false);
333 }