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