@samitouri / QOS-React / commits / 3e20dc8b9c

[DevTools] Fix crash when inspecting Components suspended on data awaited in anonymous functions (#34234)

Sebastian "Sebbie" Silbermann committed Aug 20, 2025 at 09:34 UTC 3e20dc8b9c4d335c77e246d114a1bcfd1b77aaae
6 files changed +161 -27
packages/react-debug-tools/src/ReactDebugHooks.js
+44 -17
@@ -6,7 +6,7 @@
6 *
7 * @flow
8 */
9 -
9 +import type {StackFrame as ParsedStackFrame} from 'error-stack-parser';
10 import type {
11 Awaited,
12 ReactContext,
@@ -844,7 +844,11 @@ export type HooksTree = Array<HooksNode>;
844
845 let mostLikelyAncestorIndex = 0;
846
847 -function findSharedIndex(hookStack: any, rootStack: any, rootIndex: number) {
847 +function findSharedIndex(
848 + hookStack: ParsedStackFrame[],
849 + rootStack: ParsedStackFrame[],
850 + rootIndex: number,
851 +) {
852 const source = rootStack[rootIndex].source;
853 hookSearch: for (let i = 0; i < hookStack.length; i++) {
854 if (hookStack[i].source === source) {
@@ -865,7 +869,10 @@ function findSharedIndex(hookStack: any, rootStack: any, rootIndex: number) {
869 return -1;
870 }
871
868 -function findCommonAncestorIndex(rootStack: any, hookStack: any) {
872 +function findCommonAncestorIndex(
873 + rootStack: ParsedStackFrame[],
874 + hookStack: ParsedStackFrame[],
875 +) {
876 let rootIndex = findSharedIndex(
877 hookStack,
878 rootStack,
@@ -886,7 +893,7 @@ function findCommonAncestorIndex(rootStack: any, hookStack: any) {
893 return -1;
894 }
895
889 -function isReactWrapper(functionName: any, wrapperName: string) {
896 +function isReactWrapper(functionName: void | string, wrapperName: string) {
897 const hookName = parseHookName(functionName);
898 if (wrapperName === 'HostTransitionStatus') {
899 return hookName === wrapperName || hookName === 'FormStatus';
@@ -895,7 +902,7 @@ function isReactWrapper(functionName: any, wrapperName: string) {
902 return hookName === wrapperName;
903 }
904
898 -function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) {
905 +function findPrimitiveIndex(hookStack: ParsedStackFrame[], hook: HookLogEntry) {
906 const stackCache = getPrimitiveStackCache();
907 const primitiveStack = stackCache.get(hook.primitive);
908 if (primitiveStack === undefined) {
@@ -926,7 +933,7 @@ function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) {
933 return -1;
934 }
935
929 -function parseTrimmedStack(rootStack: any, hook: HookLogEntry) {
936 +function parseTrimmedStack(rootStack: ParsedStackFrame[], hook: HookLogEntry) {
937 // Get the stack trace between the primitive hook function and
938 // the root function call. I.e. the stack frames of custom hooks.
939 const hookStack = ErrorStackParser.parse(hook.stackError);
@@ -987,7 +994,7 @@ function parseHookName(functionName: void | string): string {
994 }
995
996 function buildTree(
990 - rootStack: any,
997 + rootStack: ParsedStackFrame[],
998 readHookLog: Array<HookLogEntry>,
999 ): HooksTree {
1000 const rootChildren: Array<HooksNode> = [];
@@ -1044,10 +1051,20 @@ function buildTree(
1051 subHooks: children,
1052 debugInfo: null,
1053 hookSource: {
1047 - lineNumber: stackFrame.lineNumber,
1048 - columnNumber: stackFrame.columnNumber,
1049 - functionName: stackFrame.functionName,
1050 - fileName: stackFrame.fileName,
1054 + lineNumber:
1055 + stackFrame.lineNumber === undefined
1056 + ? null
1057 + : stackFrame.lineNumber,
1058 + columnNumber:
1059 + stackFrame.columnNumber === undefined
1060 + ? null
1061 + : stackFrame.columnNumber,
1062 + functionName:
1063 + stackFrame.functionName === undefined
1064 + ? null
1065 + : stackFrame.functionName,
1066 + fileName:
1067 + stackFrame.fileName === undefined ? null : stackFrame.fileName,
1068 },
1069 };
1070
@@ -1092,10 +1109,14 @@ function buildTree(
1109 };
1110 if (stack && stack.length >= 1) {
1111 const stackFrame = stack[0];
1095 - hookSource.lineNumber = stackFrame.lineNumber;
1096 - hookSource.functionName = stackFrame.functionName;
1097 - hookSource.fileName = stackFrame.fileName;
1098 - hookSource.columnNumber = stackFrame.columnNumber;
1112 + hookSource.lineNumber =
1113 + stackFrame.lineNumber === undefined ? null : stackFrame.lineNumber;
1114 + hookSource.functionName =
1115 + stackFrame.functionName === undefined ? null : stackFrame.functionName;
1116 + hookSource.fileName =
1117 + stackFrame.fileName === undefined ? null : stackFrame.fileName;
1118 + hookSource.columnNumber =
1119 + stackFrame.columnNumber === undefined ? null : stackFrame.columnNumber;
1120 }
1121
1122 levelChild.hookSource = hookSource;
@@ -1201,7 +1222,10 @@ export function inspectHooks<Props>(
1222 // $FlowFixMe[incompatible-use] found when upgrading Flow
1223 currentDispatcher.H = previousDispatcher;
1224 }
1204 - const rootStack = ErrorStackParser.parse(ancestorStackError);
1225 + const rootStack =
1226 + ancestorStackError === undefined
1227 + ? ([]: ParsedStackFrame[])
1228 + : ErrorStackParser.parse(ancestorStackError);
1229 return buildTree(rootStack, readHookLog);
1230 }
1231
@@ -1249,7 +1273,10 @@ function inspectHooksOfForwardRef<Props, Ref>(
1273 hookLog = [];
1274 currentDispatcher.H = previousDispatcher;
1275 }
1252 - const rootStack = ErrorStackParser.parse(ancestorStackError);
1276 + const rootStack =
1277 + ancestorStackError === undefined
1278 + ? ([]: ParsedStackFrame[])
1279 + : ErrorStackParser.parse(ancestorStackError);
1280 return buildTree(rootStack, readHookLog);
1281 }
1282
packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js
+47
@@ -720,6 +720,53 @@ describe('ReactHooksInspection', () => {
720 );
721 });
722
723 + it('should inspect use() calls in anonymous loops', () => {
724 + function Foo({entries}) {
725 + const values = Object.fromEntries(
726 + Object.entries(entries).map(([key, value]) => {
727 + return [key, React.use(value)];
728 + }),
729 + );
730 + return <div>{values}</div>;
731 + }
732 + const tree = ReactDebugTools.inspectHooks(Foo, {
733 + entries: {one: Promise.resolve('one'), two: Promise.resolve('two')},
734 + });
735 + const results = normalizeSourceLoc(tree);
736 + expect(results).toHaveLength(1);
737 + expect(results[0]).toMatchInlineSnapshot(`
738 + {
739 + "debugInfo": null,
740 + "hookSource": {
741 + "columnNumber": 0,
742 + "fileName": "**",
743 + "functionName": "Foo",
744 + "lineNumber": 0,
745 + },
746 + "id": null,
747 + "isStateEditable": false,
748 + "name": "",
749 + "subHooks": [
750 + {
751 + "debugInfo": null,
752 + "hookSource": {
753 + "columnNumber": 0,
754 + "fileName": "**",
755 + "functionName": null,
756 + "lineNumber": 0,
757 + },
758 + "id": null,
759 + "isStateEditable": false,
760 + "name": "Use",
761 + "subHooks": [],
762 + "value": Promise {},
763 + },
764 + ],
765 + "value": undefined,
766 + }
767 + `);
768 + });
769 +
770 describe('useDebugValue', () => {
771 it('should be ignored when called outside of a custom hook', () => {
772 function Foo(props) {
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+1 -1
@@ -885,7 +885,7 @@ describe('ReactHooksInspectionIntegration', () => {
885 "hookSource": {
886 "columnNumber": 0,
887 "fileName": "**",
888 - "functionName": undefined,
888 + "functionName": null,
889 "lineNumber": 0,
890 },
891 "id": 0,
packages/react-devtools-timeline/src/content-views/utils/moduleFilters.js
+4
@@ -51,12 +51,16 @@ export function isInternalModule(
51 const [startStackFrame, stopStackFrame] = ranges[i];
52
53 const isAfterStart =
54 + // $FlowFixMe[invalid-compare] -- TODO: Revealed when adding types to error-stack-parser
55 locationLine > startStackFrame.lineNumber ||
56 (locationLine === startStackFrame.lineNumber &&
57 + // $FlowFixMe[invalid-compare]
58 locationColumn >= startStackFrame.columnNumber);
59 const isBeforeStop =
60 + // $FlowFixMe[invalid-compare]
61 locationLine < stopStackFrame.lineNumber ||
62 (locationLine === stopStackFrame.lineNumber &&
63 + // $FlowFixMe[invalid-compare]
64 locationColumn <= stopStackFrame.columnNumber);
65
66 if (isAfterStart && isBeforeStop) {
packages/react-devtools-timeline/src/types.js
+4 -9
@@ -6,7 +6,7 @@
6 *
7 * @flow
8 */
9 -
9 +import type {StackFrame as ErrorStackFrame} from 'error-stack-parser';
10 import type {ScrollState} from './view-base/utils/scrollState';
11
12 // Source: https://github.com/facebook/flow/issues/4002#issuecomment-323612798
@@ -16,12 +16,7 @@ type Return_<R, F: (...args: Array<any>) => R> = R;
16 export type Return<T> = Return_<mixed, T>;
17
18 // Project types
19 -
20 -export type ErrorStackFrame = {
21 - fileName: string,
22 - lineNumber: number,
23 - columnNumber: number,
24 -};
19 +export type {ErrorStackFrame};
20
21 export type Milliseconds = number;
22
@@ -192,7 +187,7 @@ export type ViewState = {
187 };
188
189 export type InternalModuleSourceToRanges = Map<
195 - string,
190 + string | void,
191 Array<[ErrorStackFrame, ErrorStackFrame]>,
192 >;
193
@@ -224,7 +219,7 @@ export type TimelineDataExport = {
219 duration: number,
220 flamechart: Flamechart,
221 internalModuleSourceToRanges: Array<
227 - [string, Array<[ErrorStackFrame, ErrorStackFrame]>],
222 + [string | void, Array<[ErrorStackFrame, ErrorStackFrame]>],
223 >,
224 laneToLabelKeyValueArray: Array<[ReactLane, string]>,
225 laneToReactMeasureKeyValueArray: Array<[ReactLane, ReactMeasure[]]>,
scripts/flow/environment.js
+61
@@ -29,6 +29,67 @@ declare module 'create-react-class' {
29 declare const exports: $FlowFixMe;
30 }
31
32 +declare module 'error-stack-parser' {
33 + // flow-typed signature: 132e48034ef4756600e1d98681a166b5
34 + // flow-typed version: c6154227d1/error-stack-parser_v2.x.x/flow_>=v0.104.x
35 +
36 + declare interface StackFrame {
37 + constructor(object: StackFrame): StackFrame;
38 +
39 + isConstructor?: boolean;
40 + getIsConstructor(): boolean;
41 + setIsConstructor(): void;
42 +
43 + isEval?: boolean;
44 + getIsEval(): boolean;
45 + setIsEval(): void;
46 +
47 + isNative?: boolean;
48 + getIsNative(): boolean;
49 + setIsNative(): void;
50 +
51 + isTopLevel?: boolean;
52 + getIsTopLevel(): boolean;
53 + setIsTopLevel(): void;
54 +
55 + columnNumber?: number;
56 + getColumnNumber(): number;
57 + setColumnNumber(): void;
58 +
59 + lineNumber?: number;
60 + getLineNumber(): number;
61 + setLineNumber(): void;
62 +
63 + fileName?: string;
64 + getFileName(): string;
65 + setFileName(): void;
66 +
67 + functionName?: string;
68 + getFunctionName(): string;
69 + setFunctionName(): void;
70 +
71 + source?: string;
72 + getSource(): string;
73 + setSource(): void;
74 +
75 + args?: any[];
76 + getArgs(): any[];
77 + setArgs(): void;
78 +
79 + evalOrigin?: StackFrame;
80 + getEvalOrigin(): StackFrame;
81 + setEvalOrigin(): void;
82 +
83 + toString(): string;
84 + }
85 +
86 + declare class ErrorStackParser {
87 + parse(error: Error): Array<StackFrame>;
88 + }
89 +
90 + declare module.exports: ErrorStackParser;
91 +}
92 +
93 declare interface ConsoleTask {
94 run<T>(f: () => T): T;
95 }