Skip to content

Commit 3efe575

Browse files
author
Brian Vaughn
committed
Forked ComponentStackFrame and FiberComponentStack into DevTools
1 parent e781580 commit 3efe575

File tree

9 files changed

+584
-160
lines changed

9 files changed

+584
-160
lines changed
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its 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+
// This is a DevTools fork of ReactComponentStackFrame.
11+
// This fork enables DevTools to use the same "native" component stack format,
12+
// while still maintaining support for multiple renderer versions
13+
// (which use different values for ReactTypeOfWork).
14+
15+
import type {Source} from 'shared/ReactElementType';
16+
import type {LazyComponent} from 'react/src/ReactLazy';
17+
import type {CurrentDispatcherRef} from './types';
18+
19+
import {
20+
BLOCK_NUMBER,
21+
BLOCK_SYMBOL_STRING,
22+
FORWARD_REF_NUMBER,
23+
FORWARD_REF_SYMBOL_STRING,
24+
LAZY_NUMBER,
25+
LAZY_SYMBOL_STRING,
26+
MEMO_NUMBER,
27+
MEMO_SYMBOL_STRING,
28+
SUSPENSE_NUMBER,
29+
SUSPENSE_SYMBOL_STRING,
30+
SUSPENSE_LIST_NUMBER,
31+
SUSPENSE_LIST_SYMBOL_STRING,
32+
} from './ReactSymbols';
33+
34+
// These methods are safe to import from shared;
35+
// there is no React-specific logic here.
36+
import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
37+
38+
// TODO Add branching back in for <= 16.7 that doesn't iject currentDispatcherRef
39+
40+
let prefix;
41+
export function describeBuiltInComponentFrame(
42+
name: string,
43+
source: void | null | Source,
44+
ownerFn: void | null | Function,
45+
): string {
46+
if (prefix === undefined) {
47+
// Extract the VM specific prefix used by each line.
48+
try {
49+
throw Error();
50+
} catch (x) {
51+
const match = x.stack.trim().match(/\n( *(at )?)/);
52+
prefix = (match && match[1]) || '';
53+
}
54+
}
55+
// We use the prefix to ensure our stacks line up with native stack frames.
56+
return '\n' + prefix + name;
57+
}
58+
59+
let reentry = false;
60+
let componentFrameCache;
61+
if (__DEV__) {
62+
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
63+
componentFrameCache = new PossiblyWeakMap();
64+
}
65+
66+
export function describeNativeComponentFrame(
67+
fn: Function,
68+
construct: boolean,
69+
currentDispatcherRef: CurrentDispatcherRef,
70+
): string {
71+
// If something asked for a stack inside a fake render, it should get ignored.
72+
if (!fn || reentry) {
73+
return '';
74+
}
75+
76+
if (__DEV__) {
77+
const frame = componentFrameCache.get(fn);
78+
if (frame !== undefined) {
79+
return frame;
80+
}
81+
}
82+
83+
let control;
84+
85+
reentry = true;
86+
let previousDispatcher;
87+
if (__DEV__) {
88+
previousDispatcher = currentDispatcherRef.current;
89+
// Set the dispatcher in DEV because this might be call in the render function
90+
// for warnings.
91+
currentDispatcherRef.current = null;
92+
disableLogs();
93+
}
94+
try {
95+
// This should throw.
96+
if (construct) {
97+
// Something should be setting the props in the constructor.
98+
const Fake = function() {
99+
throw Error();
100+
};
101+
// $FlowFixMe
102+
Object.defineProperty(Fake.prototype, 'props', {
103+
set: function() {
104+
// We use a throwing setter instead of frozen or non-writable props
105+
// because that won't throw in a non-strict mode function.
106+
throw Error();
107+
},
108+
});
109+
if (typeof Reflect === 'object' && Reflect.construct) {
110+
// We construct a different control for this case to include any extra
111+
// frames added by the construct call.
112+
try {
113+
Reflect.construct(Fake, []);
114+
} catch (x) {
115+
control = x;
116+
}
117+
Reflect.construct(fn, [], Fake);
118+
} else {
119+
try {
120+
Fake.call();
121+
} catch (x) {
122+
control = x;
123+
}
124+
fn.call(Fake.prototype);
125+
}
126+
} else {
127+
try {
128+
throw Error();
129+
} catch (x) {
130+
control = x;
131+
}
132+
fn();
133+
}
134+
} catch (sample) {
135+
// This is inlined manually because closure doesn't do it for us.
136+
if (sample && control && typeof sample.stack === 'string') {
137+
// This extracts the first frame from the sample that isn't also in the control.
138+
// Skipping one frame that we assume is the frame that calls the two.
139+
const sampleLines = sample.stack.split('\n');
140+
const controlLines = control.stack.split('\n');
141+
let s = sampleLines.length - 1;
142+
let c = controlLines.length - 1;
143+
while (s >= 1 && c >= 0 && sampleLines[s] !== controlLines[c]) {
144+
// We expect at least one stack frame to be shared.
145+
// Typically this will be the root most one. However, stack frames may be
146+
// cut off due to maximum stack limits. In this case, one maybe cut off
147+
// earlier than the other. We assume that the sample is longer or the same
148+
// and there for cut off earlier. So we should find the root most frame in
149+
// the sample somewhere in the control.
150+
c--;
151+
}
152+
for (; s >= 1 && c >= 0; s--, c--) {
153+
// Next we find the first one that isn't the same which should be the
154+
// frame that called our sample function and the control.
155+
if (sampleLines[s] !== controlLines[c]) {
156+
// In V8, the first line is describing the message but other VMs don't.
157+
// If we're about to return the first line, and the control is also on the same
158+
// line, that's a pretty good indicator that our sample threw at same line as
159+
// the control. I.e. before we entered the sample frame. So we ignore this result.
160+
// This can happen if you passed a class to function component, or non-function.
161+
if (s !== 1 || c !== 1) {
162+
do {
163+
s--;
164+
c--;
165+
// We may still have similar intermediate frames from the construct call.
166+
// The next one that isn't the same should be our match though.
167+
if (c < 0 || sampleLines[s] !== controlLines[c]) {
168+
// V8 adds a "new" prefix for native classes. Let's remove it to make it prettier.
169+
const frame = '\n' + sampleLines[s].replace(' at new ', ' at ');
170+
if (__DEV__) {
171+
if (typeof fn === 'function') {
172+
componentFrameCache.set(fn, frame);
173+
}
174+
}
175+
// Return the line we found.
176+
return frame;
177+
}
178+
} while (s >= 1 && c >= 0);
179+
}
180+
break;
181+
}
182+
}
183+
}
184+
} finally {
185+
reentry = false;
186+
if (__DEV__) {
187+
currentDispatcherRef.current = previousDispatcher;
188+
reenableLogs();
189+
}
190+
}
191+
// Fallback to just using the name if we couldn't make it throw.
192+
const name = fn ? fn.displayName || fn.name : '';
193+
const syntheticFrame = name ? describeBuiltInComponentFrame(name) : '';
194+
if (__DEV__) {
195+
if (typeof fn === 'function') {
196+
componentFrameCache.set(fn, syntheticFrame);
197+
}
198+
}
199+
return syntheticFrame;
200+
}
201+
202+
export function describeClassComponentFrame(
203+
ctor: Function,
204+
source: void | null | Source,
205+
ownerFn: void | null | Function,
206+
currentDispatcherRef: CurrentDispatcherRef,
207+
): string {
208+
return describeNativeComponentFrame(ctor, true, currentDispatcherRef);
209+
}
210+
211+
export function describeFunctionComponentFrame(
212+
fn: Function,
213+
source: void | null | Source,
214+
ownerFn: void | null | Function,
215+
currentDispatcherRef: CurrentDispatcherRef,
216+
): string {
217+
return describeNativeComponentFrame(fn, false, currentDispatcherRef);
218+
}
219+
220+
function shouldConstruct(Component: Function) {
221+
const prototype = Component.prototype;
222+
return !!(prototype && prototype.isReactComponent);
223+
}
224+
225+
export function describeUnknownElementTypeFrameInDEV(
226+
type: any,
227+
source: void | null | Source,
228+
ownerFn: void | null | Function,
229+
currentDispatcherRef: CurrentDispatcherRef,
230+
): string {
231+
if (!__DEV__) {
232+
return '';
233+
}
234+
if (type == null) {
235+
return '';
236+
}
237+
if (typeof type === 'function') {
238+
return describeNativeComponentFrame(
239+
type,
240+
shouldConstruct(type),
241+
currentDispatcherRef,
242+
);
243+
}
244+
if (typeof type === 'string') {
245+
return describeBuiltInComponentFrame(type, source, ownerFn);
246+
}
247+
switch (type) {
248+
case SUSPENSE_NUMBER:
249+
case SUSPENSE_SYMBOL_STRING:
250+
return describeBuiltInComponentFrame('Suspense', source, ownerFn);
251+
case SUSPENSE_LIST_NUMBER:
252+
case SUSPENSE_LIST_SYMBOL_STRING:
253+
return describeBuiltInComponentFrame('SuspenseList', source, ownerFn);
254+
}
255+
if (typeof type === 'object') {
256+
switch (type.$$typeof) {
257+
case FORWARD_REF_NUMBER:
258+
case FORWARD_REF_SYMBOL_STRING:
259+
return describeFunctionComponentFrame(
260+
type.render,
261+
source,
262+
ownerFn,
263+
currentDispatcherRef,
264+
);
265+
case MEMO_NUMBER:
266+
case MEMO_SYMBOL_STRING:
267+
// Memo may contain any component type so we recursively resolve it.
268+
return describeUnknownElementTypeFrameInDEV(
269+
type.type,
270+
source,
271+
ownerFn,
272+
currentDispatcherRef,
273+
);
274+
case BLOCK_NUMBER:
275+
case BLOCK_SYMBOL_STRING:
276+
return describeFunctionComponentFrame(
277+
type._render,
278+
source,
279+
ownerFn,
280+
currentDispatcherRef,
281+
);
282+
case LAZY_NUMBER:
283+
case LAZY_SYMBOL_STRING: {
284+
const lazyComponent: LazyComponent<any, any> = (type: any);
285+
const payload = lazyComponent._payload;
286+
const init = lazyComponent._init;
287+
try {
288+
// Lazy may contain any component type so we recursively resolve it.
289+
return describeUnknownElementTypeFrameInDEV(
290+
init(payload),
291+
source,
292+
ownerFn,
293+
currentDispatcherRef,
294+
);
295+
} catch (x) {}
296+
}
297+
}
298+
}
299+
return '';
300+
}

0 commit comments

Comments
 (0)