[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
}