diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 54a6dd3e43a33..5a49b2e073c0f 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -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; 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, ): HooksTree { const rootChildren: Array = []; @@ -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( // $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( hookLog = []; currentDispatcher.H = previousDispatcher; } - const rootStack = ErrorStackParser.parse(ancestorStackError); + const rootStack = + ancestorStackError === undefined + ? ([]: ParsedStackFrame[]) + : ErrorStackParser.parse(ancestorStackError); return buildTree(rootStack, readHookLog); } diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js index 8d159a22105c0..18dab0710f259 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspection-test.js @@ -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
{values}
; + } + 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) { diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index e245adcb1380a..a150ded10f75f 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -885,7 +885,7 @@ describe('ReactHooksInspectionIntegration', () => { "hookSource": { "columnNumber": 0, "fileName": "**", - "functionName": undefined, + "functionName": null, "lineNumber": 0, }, "id": 0, diff --git a/packages/react-devtools-timeline/src/content-views/utils/moduleFilters.js b/packages/react-devtools-timeline/src/content-views/utils/moduleFilters.js index fd323b897f9a2..ff35f0440f8b4 100644 --- a/packages/react-devtools-timeline/src/content-views/utils/moduleFilters.js +++ b/packages/react-devtools-timeline/src/content-views/utils/moduleFilters.js @@ -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) { diff --git a/packages/react-devtools-timeline/src/types.js b/packages/react-devtools-timeline/src/types.js index 6a12fc6105430..c28c78d88c80e 100644 --- a/packages/react-devtools-timeline/src/types.js +++ b/packages/react-devtools-timeline/src/types.js @@ -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> = R; export type Return = Return_; // 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[]]>, diff --git a/scripts/flow/environment.js b/scripts/flow/environment.js index 823defbd8df24..d8b2ac28f0d70 100644 --- a/scripts/flow/environment.js +++ b/scripts/flow/environment.js @@ -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; + } + + declare module.exports: ErrorStackParser; +} + declare interface ConsoleTask { run(f: () => T): T; }