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

This commit is contained in:
Sebastian "Sebbie" Silbermann
2025-08-20 09:34:06 +02:00
committed by GitHub
parent 06b5050d65
commit e9dda80e60
6 changed files with 161 additions and 27 deletions
+44 -17
View File
@@ -6,7 +6,7 @@
*
* @flow
*/
import type {StackFrame as ParsedStackFrame} from 'error-stack-parser';
import type {
Awaited,
ReactContext,
@@ -844,7 +844,11 @@ export type HooksTree = Array<HooksNode>;
let mostLikelyAncestorIndex = 0;
function findSharedIndex(hookStack: any, rootStack: any, rootIndex: number) {
function findSharedIndex(
hookStack: ParsedStackFrame[],
rootStack: ParsedStackFrame[],
rootIndex: number,
) {
const source = rootStack[rootIndex].source;
hookSearch: for (let i = 0; i < hookStack.length; i++) {
if (hookStack[i].source === source) {
@@ -865,7 +869,10 @@ function findSharedIndex(hookStack: any, rootStack: any, rootIndex: number) {
return -1;
}
function findCommonAncestorIndex(rootStack: any, hookStack: any) {
function findCommonAncestorIndex(
rootStack: ParsedStackFrame[],
hookStack: ParsedStackFrame[],
) {
let rootIndex = findSharedIndex(
hookStack,
rootStack,
@@ -886,7 +893,7 @@ function findCommonAncestorIndex(rootStack: any, hookStack: any) {
return -1;
}
function isReactWrapper(functionName: any, wrapperName: string) {
function isReactWrapper(functionName: void | string, wrapperName: string) {
const hookName = parseHookName(functionName);
if (wrapperName === 'HostTransitionStatus') {
return hookName === wrapperName || hookName === 'FormStatus';
@@ -895,7 +902,7 @@ function isReactWrapper(functionName: any, wrapperName: string) {
return hookName === wrapperName;
}
function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) {
function findPrimitiveIndex(hookStack: ParsedStackFrame[], hook: HookLogEntry) {
const stackCache = getPrimitiveStackCache();
const primitiveStack = stackCache.get(hook.primitive);
if (primitiveStack === undefined) {
@@ -926,7 +933,7 @@ function findPrimitiveIndex(hookStack: any, hook: HookLogEntry) {
return -1;
}
function parseTrimmedStack(rootStack: any, hook: HookLogEntry) {
function parseTrimmedStack(rootStack: ParsedStackFrame[], hook: HookLogEntry) {
// Get the stack trace between the primitive hook function and
// the root function call. I.e. the stack frames of custom hooks.
const hookStack = ErrorStackParser.parse(hook.stackError);
@@ -987,7 +994,7 @@ function parseHookName(functionName: void | string): string {
}
function buildTree(
rootStack: any,
rootStack: ParsedStackFrame[],
readHookLog: Array<HookLogEntry>,
): HooksTree {
const rootChildren: Array<HooksNode> = [];
@@ -1044,10 +1051,20 @@ function buildTree(
subHooks: children,
debugInfo: null,
hookSource: {
lineNumber: stackFrame.lineNumber,
columnNumber: stackFrame.columnNumber,
functionName: stackFrame.functionName,
fileName: stackFrame.fileName,
lineNumber:
stackFrame.lineNumber === undefined
? null
: stackFrame.lineNumber,
columnNumber:
stackFrame.columnNumber === undefined
? null
: stackFrame.columnNumber,
functionName:
stackFrame.functionName === undefined
? null
: stackFrame.functionName,
fileName:
stackFrame.fileName === undefined ? null : stackFrame.fileName,
},
};
@@ -1092,10 +1109,14 @@ function buildTree(
};
if (stack && stack.length >= 1) {
const stackFrame = stack[0];
hookSource.lineNumber = stackFrame.lineNumber;
hookSource.functionName = stackFrame.functionName;
hookSource.fileName = stackFrame.fileName;
hookSource.columnNumber = stackFrame.columnNumber;
hookSource.lineNumber =
stackFrame.lineNumber === undefined ? null : stackFrame.lineNumber;
hookSource.functionName =
stackFrame.functionName === undefined ? null : stackFrame.functionName;
hookSource.fileName =
stackFrame.fileName === undefined ? null : stackFrame.fileName;
hookSource.columnNumber =
stackFrame.columnNumber === undefined ? null : stackFrame.columnNumber;
}
levelChild.hookSource = hookSource;
@@ -1201,7 +1222,10 @@ export function inspectHooks<Props>(
// $FlowFixMe[incompatible-use] found when upgrading Flow
currentDispatcher.H = previousDispatcher;
}
const rootStack = ErrorStackParser.parse(ancestorStackError);
const rootStack =
ancestorStackError === undefined
? ([]: ParsedStackFrame[])
: ErrorStackParser.parse(ancestorStackError);
return buildTree(rootStack, readHookLog);
}
@@ -1249,7 +1273,10 @@ function inspectHooksOfForwardRef<Props, Ref>(
hookLog = [];
currentDispatcher.H = previousDispatcher;
}
const rootStack = ErrorStackParser.parse(ancestorStackError);
const rootStack =
ancestorStackError === undefined
? ([]: ParsedStackFrame[])
: ErrorStackParser.parse(ancestorStackError);
return buildTree(rootStack, readHookLog);
}
@@ -720,6 +720,53 @@ describe('ReactHooksInspection', () => {
);
});
it('should inspect use() calls in anonymous loops', () => {
function Foo({entries}) {
const values = Object.fromEntries(
Object.entries(entries).map(([key, value]) => {
return [key, React.use(value)];
}),
);
return <div>{values}</div>;
}
const tree = ReactDebugTools.inspectHooks(Foo, {
entries: {one: Promise.resolve('one'), two: Promise.resolve('two')},
});
const results = normalizeSourceLoc(tree);
expect(results).toHaveLength(1);
expect(results[0]).toMatchInlineSnapshot(`
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "Foo",
"lineNumber": 0,
},
"id": null,
"isStateEditable": false,
"name": "",
"subHooks": [
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": null,
"lineNumber": 0,
},
"id": null,
"isStateEditable": false,
"name": "Use",
"subHooks": [],
"value": Promise {},
},
],
"value": undefined,
}
`);
});
describe('useDebugValue', () => {
it('should be ignored when called outside of a custom hook', () => {
function Foo(props) {
@@ -885,7 +885,7 @@ describe('ReactHooksInspectionIntegration', () => {
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": undefined,
"functionName": null,
"lineNumber": 0,
},
"id": 0,
@@ -51,12 +51,16 @@ export function isInternalModule(
const [startStackFrame, stopStackFrame] = ranges[i];
const isAfterStart =
// $FlowFixMe[invalid-compare] -- TODO: Revealed when adding types to error-stack-parser
locationLine > startStackFrame.lineNumber ||
(locationLine === startStackFrame.lineNumber &&
// $FlowFixMe[invalid-compare]
locationColumn >= startStackFrame.columnNumber);
const isBeforeStop =
// $FlowFixMe[invalid-compare]
locationLine < stopStackFrame.lineNumber ||
(locationLine === stopStackFrame.lineNumber &&
// $FlowFixMe[invalid-compare]
locationColumn <= stopStackFrame.columnNumber);
if (isAfterStart && isBeforeStop) {
+4 -9
View File
@@ -6,7 +6,7 @@
*
* @flow
*/
import type {StackFrame as ErrorStackFrame} from 'error-stack-parser';
import type {ScrollState} from './view-base/utils/scrollState';
// Source: https://github.com/facebook/flow/issues/4002#issuecomment-323612798
@@ -16,12 +16,7 @@ type Return_<R, F: (...args: Array<any>) => R> = R;
export type Return<T> = Return_<mixed, T>;
// Project types
export type ErrorStackFrame = {
fileName: string,
lineNumber: number,
columnNumber: number,
};
export type {ErrorStackFrame};
export type Milliseconds = number;
@@ -192,7 +187,7 @@ export type ViewState = {
};
export type InternalModuleSourceToRanges = Map<
string,
string | void,
Array<[ErrorStackFrame, ErrorStackFrame]>,
>;
@@ -224,7 +219,7 @@ export type TimelineDataExport = {
duration: number,
flamechart: Flamechart,
internalModuleSourceToRanges: Array<
[string, Array<[ErrorStackFrame, ErrorStackFrame]>],
[string | void, Array<[ErrorStackFrame, ErrorStackFrame]>],
>,
laneToLabelKeyValueArray: Array<[ReactLane, string]>,
laneToReactMeasureKeyValueArray: Array<[ReactLane, ReactMeasure[]]>,
+61
View File
@@ -29,6 +29,67 @@ declare module 'create-react-class' {
declare const exports: $FlowFixMe;
}
declare module 'error-stack-parser' {
// flow-typed signature: 132e48034ef4756600e1d98681a166b5
// flow-typed version: c6154227d1/error-stack-parser_v2.x.x/flow_>=v0.104.x
declare interface StackFrame {
constructor(object: StackFrame): StackFrame;
isConstructor?: boolean;
getIsConstructor(): boolean;
setIsConstructor(): void;
isEval?: boolean;
getIsEval(): boolean;
setIsEval(): void;
isNative?: boolean;
getIsNative(): boolean;
setIsNative(): void;
isTopLevel?: boolean;
getIsTopLevel(): boolean;
setIsTopLevel(): void;
columnNumber?: number;
getColumnNumber(): number;
setColumnNumber(): void;
lineNumber?: number;
getLineNumber(): number;
setLineNumber(): void;
fileName?: string;
getFileName(): string;
setFileName(): void;
functionName?: string;
getFunctionName(): string;
setFunctionName(): void;
source?: string;
getSource(): string;
setSource(): void;
args?: any[];
getArgs(): any[];
setArgs(): void;
evalOrigin?: StackFrame;
getEvalOrigin(): StackFrame;
setEvalOrigin(): void;
toString(): string;
}
declare class ErrorStackParser {
parse(error: Error): Array<StackFrame>;
}
declare module.exports: ErrorStackParser;
}
declare interface ConsoleTask {
run<T>(f: () => T): T;
}