Skip to content

Commit

Permalink
Rely on sourcemaps to compute hook name of built-in hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon committed Mar 13, 2024
1 parent ec606a6 commit 88ccaec
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 80 deletions.
85 changes: 53 additions & 32 deletions packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type HookLogEntry = {
stackError: Error,
value: mixed,
debugInfo: ReactDebugInfo | null,
dispatcherMethodName: string,
};

let hookLog: Array<HookLogEntry> = [];
Expand Down Expand Up @@ -130,6 +131,7 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
// This type check is for Flow only.
Dispatcher.useHostTransitionStatus();
}
Dispatcher.useId();
} finally {
readHookLog = hookLog;
hookLog = [];
Expand Down Expand Up @@ -200,6 +202,7 @@ function use<T>(usable: Usable<T>): T {
value: fulfilledValue,
debugInfo:
thenable._debugInfo === undefined ? null : thenable._debugInfo,
dispatcherMethodName: 'use',
});
return fulfilledValue;
}
Expand All @@ -217,6 +220,7 @@ function use<T>(usable: Usable<T>): T {
value: thenable,
debugInfo:
thenable._debugInfo === undefined ? null : thenable._debugInfo,
dispatcherMethodName: 'use',
});
throw SuspenseException;
} else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
Expand All @@ -229,6 +233,7 @@ function use<T>(usable: Usable<T>): T {
stackError: new Error(),
value,
debugInfo: null,
dispatcherMethodName: 'use',
});

return value;
Expand All @@ -247,6 +252,7 @@ function useContext<T>(context: ReactContext<T>): T {
stackError: new Error(),
value: value,
debugInfo: null,
dispatcherMethodName: 'useContext',
});
return value;
}
Expand All @@ -268,6 +274,7 @@ function useState<S>(
stackError: new Error(),
value: state,
debugInfo: null,
dispatcherMethodName: 'useState',
});
return [state, (action: BasicStateAction<S>) => {}];
}
Expand All @@ -290,6 +297,7 @@ function useReducer<S, I, A>(
stackError: new Error(),
value: state,
debugInfo: null,
dispatcherMethodName: 'useReducer',
});
return [state, (action: A) => {}];
}
Expand All @@ -303,6 +311,7 @@ function useRef<T>(initialValue: T): {current: T} {
stackError: new Error(),
value: ref.current,
debugInfo: null,
dispatcherMethodName: 'useRef',
});
return ref;
}
Expand All @@ -315,6 +324,7 @@ function useCacheRefresh(): () => void {
stackError: new Error(),
value: hook !== null ? hook.memoizedState : function refresh() {},
debugInfo: null,
dispatcherMethodName: 'useCacheRefresh',
});
return () => {};
}
Expand All @@ -330,6 +340,7 @@ function useLayoutEffect(
stackError: new Error(),
value: create,
debugInfo: null,
dispatcherMethodName: 'useLayoutEffect',
});
}

Expand All @@ -344,6 +355,7 @@ function useInsertionEffect(
stackError: new Error(),
value: create,
debugInfo: null,
dispatcherMethodName: 'useInsertionEffect',
});
}

Expand All @@ -358,6 +370,7 @@ function useEffect(
stackError: new Error(),
value: create,
debugInfo: null,
dispatcherMethodName: 'useEffect',
});
}

Expand All @@ -381,6 +394,7 @@ function useImperativeHandle<T>(
stackError: new Error(),
value: instance,
debugInfo: null,
dispatcherMethodName: 'useImperativeHandle',
});
}

Expand All @@ -391,6 +405,7 @@ function useDebugValue(value: any, formatterFn: ?(value: any) => any) {
stackError: new Error(),
value: typeof formatterFn === 'function' ? formatterFn(value) : value,
debugInfo: null,
dispatcherMethodName: 'useDebugValue',
});
}

Expand All @@ -402,6 +417,7 @@ function useCallback<T>(callback: T, inputs: Array<mixed> | void | null): T {
stackError: new Error(),
value: hook !== null ? hook.memoizedState[0] : callback,
debugInfo: null,
dispatcherMethodName: 'useCallback',
});
return callback;
}
Expand All @@ -418,6 +434,7 @@ function useMemo<T>(
stackError: new Error(),
value,
debugInfo: null,
dispatcherMethodName: 'useMemo',
});
return value;
}
Expand All @@ -439,6 +456,7 @@ function useSyncExternalStore<T>(
stackError: new Error(),
value,
debugInfo: null,
dispatcherMethodName: 'useSyncExternalStore',
});
return value;
}
Expand All @@ -458,6 +476,7 @@ function useTransition(): [
stackError: new Error(),
value: undefined,
debugInfo: null,
dispatcherMethodName: 'useTransition',
});
return [false, callback => {}];
}
Expand All @@ -470,6 +489,7 @@ function useDeferredValue<T>(value: T, initialValue?: T): T {
stackError: new Error(),
value: hook !== null ? hook.memoizedState : value,
debugInfo: null,
dispatcherMethodName: 'useDeferredValue',
});
return value;
}
Expand All @@ -483,6 +503,7 @@ function useId(): string {
stackError: new Error(),
value: id,
debugInfo: null,
dispatcherMethodName: 'useId',
});
return id;
}
Expand Down Expand Up @@ -533,6 +554,7 @@ function useOptimistic<S, A>(
stackError: new Error(),
value: state,
debugInfo: null,
dispatcherMethodName: 'useOptimistic',
});
return [state, (action: A) => {}];
}
Expand Down Expand Up @@ -591,6 +613,7 @@ function useFormState<S, P>(
stackError: stackError,
value: value,
debugInfo: debugInfo,
dispatcherMethodName: 'useFormState',
});

if (error !== null) {
Expand Down Expand Up @@ -618,6 +641,7 @@ function useHostTransitionStatus(): TransitionStatus {
stackError: new Error(),
value: status,
debugInfo: null,
dispatcherMethodName: 'HostTransitionStatus',
});

return status;
Expand Down Expand Up @@ -696,8 +720,7 @@ export type HooksTree = Array<HooksNode>;
// of a hook call. A simple way to demonstrate this is wrapping `new Error()`
// in a wrapper constructor like a polyfill. That'll add an extra frame.
// Similar things can happen with the call to the dispatcher. The top frame
// may not be the primitive. Likewise the primitive can have fewer stack frames
// such as when a call to useState got inlined to use dispatcher.useState.
// may not be the primitive.
//
// We also can't assume that the last frame of the root call is the same
// frame as the last frame of the hook call because long stack traces can be
Expand Down Expand Up @@ -747,26 +770,16 @@ function findCommonAncestorIndex(rootStack: any, hookStack: any) {
return -1;
}

function isReactWrapper(functionName: any, primitiveName: string) {
function isReactWrapper(functionName: any, wrapperName: string) {
if (!functionName) {
return false;
}
switch (primitiveName) {
case 'Context':
case 'Context (use)':
case 'Promise':
case 'Unresolved':
if (functionName.endsWith('use')) {
return true;
}
}
const expectedPrimitiveName = 'use' + primitiveName;
if (functionName.length < expectedPrimitiveName.length) {
if (functionName.length < wrapperName.length) {
return false;
}
return (
functionName.lastIndexOf(expectedPrimitiveName) ===
functionName.length - expectedPrimitiveName.length
functionName.lastIndexOf(wrapperName) ===
functionName.length - wrapperName.length
);
}

Expand All @@ -778,18 +791,14 @@ function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) {
}
for (let i = 0; i < primitiveStack.length && i < hookStack.length; i++) {
if (primitiveStack[i].source !== hookStack[i].source) {
// If the next two frames are functions called `useX` then we assume that they're part of the
// wrappers that the React packager or other packages adds around the dispatcher.
// If the next frame is a method from the dispatcher, we
// assume that the next frame after that is the actual public API call.
// This prohibits nesting dispatcher calls in hooks.
if (
i < hookStack.length - 1 &&
isReactWrapper(hookStack[i].functionName, hook.primitive)
isReactWrapper(hookStack[i].functionName, hook.dispatcherMethodName)
) {
i++;
}
if (
i < hookStack.length - 1 &&
isReactWrapper(hookStack[i].functionName, hook.primitive)
) {
i++;
}
return i;
Expand All @@ -812,16 +821,21 @@ function parseTrimmedStack(rootStack: any, hook: HookLogEntry) {
// Something went wrong. Give up.
return null;
}
return hookStack.slice(primitiveIndex, rootIndex - 1);
return [
hookStack[primitiveIndex - 1],
hookStack.slice(primitiveIndex, rootIndex - 1),
];
}

function parseCustomHookName(functionName: void | string): string {
function parseHookName(functionName: void | string): string {
if (!functionName) {
return '';
}
let startIndex = functionName.lastIndexOf('.');
if (startIndex === -1) {
startIndex = 0;
} else {
startIndex += 1;
}
if (functionName.slice(startIndex, startIndex + 3) === 'use') {
startIndex += 3;
Expand All @@ -840,8 +854,14 @@ function buildTree(
const stackOfChildren = [];
for (let i = 0; i < readHookLog.length; i++) {
const hook = readHookLog[i];
const stack = parseTrimmedStack(rootStack, hook);
if (stack !== null) {
const parseResult = parseTrimmedStack(rootStack, hook);
let displayName = hook.displayName;
if (parseResult !== null) {
const [primitiveFrame, stack] = parseResult;
if (hook.displayName === null) {
// TODO: Support older versions of React without sourcemaps by using the primitive name if primitiveFrame.functionName does not look like a hook.
displayName = parseHookName(primitiveFrame.functionName);
}
// Note: The indices 0 <= n < length-1 will contain the names.
// The indices 1 <= n < length will contain the source locations.
// That's why we get the name from n - 1 and don't check the source
Expand Down Expand Up @@ -871,7 +891,7 @@ function buildTree(
const levelChild: HooksNode = {
id: null,
isStateEditable: false,
name: parseCustomHookName(stack[j - 1].functionName),
name: parseHookName(stack[j - 1].functionName),
value: undefined,
subHooks: children,
debugInfo: null,
Expand All @@ -889,7 +909,7 @@ function buildTree(
}
prevStack = stack;
}
const {displayName, primitive, debugInfo} = hook;
const {primitive, debugInfo} = hook;

// For now, the "id" of stateful hooks is just the stateful hook index.
// Custom hooks have no ids, nor do non-stateful native hooks (e.g. Context, DebugValue).
Expand All @@ -905,11 +925,11 @@ function buildTree(

// For the time being, only State and Reducer hooks support runtime overrides.
const isStateEditable = primitive === 'Reducer' || primitive === 'State';
const name = displayName || primitive;

const levelChild: HooksNode = {
id,
isStateEditable,
name: name,
name: displayName || 'Unknown',
value: hook.value,
subHooks: [],
debugInfo: debugInfo,
Expand All @@ -922,6 +942,7 @@ function buildTree(
fileName: null,
columnNumber: null,
};
const stack = parseResult !== null ? parseResult[1] : null;
if (stack && stack.length >= 1) {
const stackFrame = stack[0];
hookSource.lineNumber = stackFrame.lineNumber;
Expand Down
Loading

0 comments on commit 88ccaec

Please sign in to comment.