[Debug Tools] Always use includeHooksSource option (#28309)

This option was added defensively but it's not needed. There's no cost
to including it always.

I suspect this optional was added mainly to avoid needing to update
tests. That's not a reason to have an unnecessary public API though.

We have a praxis for dealing with source location in tests to avoid them
failing tests. I also ported them to inline snapshots so that additions
to the protocol isn't such a pain.
This commit is contained in:
Sebastian Markbåge
2024-02-14 11:07:35 -05:00
committed by GitHub
parent dc3178151b
commit f0e808e5bc
5 changed files with 2096 additions and 979 deletions

View File

@@ -569,7 +569,7 @@ export type HooksNode = {
value: mixed,
subHooks: Array<HooksNode>,
debugInfo: null | ReactDebugInfo,
hookSource?: HookSource,
hookSource: null | HookSource,
};
export type HooksTree = Array<HooksNode>;
@@ -716,7 +716,6 @@ function parseCustomHookName(functionName: void | string): string {
function buildTree(
rootStack: any,
readHookLog: Array<HookLogEntry>,
includeHooksSource: boolean,
): HooksTree {
const rootChildren: Array<HooksNode> = [];
let prevStack = null;
@@ -760,16 +759,13 @@ function buildTree(
value: undefined,
subHooks: children,
debugInfo: null,
};
if (includeHooksSource) {
levelChild.hookSource = {
hookSource: {
lineNumber: stackFrame.lineNumber,
columnNumber: stackFrame.columnNumber,
functionName: stackFrame.functionName,
fileName: stackFrame.fileName,
};
}
},
};
levelChildren.push(levelChild);
stackOfChildren.push(levelChildren);
@@ -800,26 +796,25 @@ function buildTree(
value: hook.value,
subHooks: [],
debugInfo: debugInfo,
hookSource: null,
};
if (includeHooksSource) {
const hookSource: HookSource = {
lineNumber: null,
functionName: null,
fileName: null,
columnNumber: null,
};
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;
}
levelChild.hookSource = hookSource;
const hookSource: HookSource = {
lineNumber: null,
functionName: null,
fileName: null,
columnNumber: null,
};
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;
}
levelChild.hookSource = hookSource;
levelChildren.push(levelChild);
}
@@ -897,7 +892,6 @@ export function inspectHooks<Props>(
renderFunction: Props => React$Node,
props: Props,
currentDispatcher: ?CurrentDispatcherRef,
includeHooksSource: boolean = false,
): HooksTree {
// DevTools will pass the current renderer's injected dispatcher.
// Other apps might compile debug hooks as part of their app though.
@@ -923,7 +917,7 @@ export function inspectHooks<Props>(
currentDispatcher.current = previousDispatcher;
}
const rootStack = ErrorStackParser.parse(ancestorStackError);
return buildTree(rootStack, readHookLog, includeHooksSource);
return buildTree(rootStack, readHookLog);
}
function setupContexts(contextMap: Map<ReactContext<any>, any>, fiber: Fiber) {
@@ -955,7 +949,6 @@ function inspectHooksOfForwardRef<Props, Ref>(
props: Props,
ref: Ref,
currentDispatcher: CurrentDispatcherRef,
includeHooksSource: boolean,
): HooksTree {
const previousDispatcher = currentDispatcher.current;
let readHookLog;
@@ -972,7 +965,7 @@ function inspectHooksOfForwardRef<Props, Ref>(
currentDispatcher.current = previousDispatcher;
}
const rootStack = ErrorStackParser.parse(ancestorStackError);
return buildTree(rootStack, readHookLog, includeHooksSource);
return buildTree(rootStack, readHookLog);
}
function resolveDefaultProps(Component: any, baseProps: any) {
@@ -993,7 +986,6 @@ function resolveDefaultProps(Component: any, baseProps: any) {
export function inspectHooksOfFiber(
fiber: Fiber,
currentDispatcher: ?CurrentDispatcherRef,
includeHooksSource: boolean = false,
): HooksTree {
// DevTools will pass the current renderer's injected dispatcher.
// Other apps might compile debug hooks as part of their app though.
@@ -1035,11 +1027,10 @@ export function inspectHooksOfFiber(
props,
fiber.ref,
currentDispatcher,
includeHooksSource,
);
}
return inspectHooks(type, props, currentDispatcher, includeHooksSource);
return inspectHooks(type, props, currentDispatcher);
} finally {
currentFiber = null;
currentHook = null;

View File

@@ -13,6 +13,18 @@
let React;
let ReactDebugTools;
function normalizeSourceLoc(tree) {
tree.forEach(node => {
if (node.hookSource) {
node.hookSource.fileName = '**';
node.hookSource.lineNumber = 0;
node.hookSource.columnNumber = 0;
}
normalizeSourceLoc(node.subHooks);
});
return tree;
}
describe('ReactHooksInspection', () => {
beforeEach(() => {
jest.resetModules();
@@ -26,16 +38,24 @@ describe('ReactHooksInspection', () => {
return <div>{state}</div>;
}
const tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
isStateEditable: true,
id: 0,
name: 'State',
value: 'hello world',
debugInfo: null,
subHooks: [],
},
]);
expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(`
[
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "Foo",
"lineNumber": 0,
},
"id": 0,
"isStateEditable": true,
"name": "State",
"subHooks": [],
"value": "hello world",
},
]
`);
});
it('should inspect a simple custom hook', () => {
@@ -49,25 +69,75 @@ describe('ReactHooksInspection', () => {
return <div>{value}</div>;
}
const tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
isStateEditable: false,
id: null,
name: 'Custom',
value: __DEV__ ? 'custom hook label' : undefined,
debugInfo: null,
subHooks: [
if (__DEV__) {
expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(`
[
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "Foo",
"lineNumber": 0,
},
"id": null,
"isStateEditable": false,
"name": "Custom",
"subHooks": [
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "useCustom",
"lineNumber": 0,
},
"id": 0,
"isStateEditable": true,
"name": "State",
"subHooks": [],
"value": "hello world",
},
],
"value": "custom hook label",
},
]
`);
} else {
expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(`
[
{
isStateEditable: true,
id: 0,
name: 'State',
value: 'hello world',
debugInfo: null,
subHooks: [],
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "Foo",
"lineNumber": 0,
},
"id": null,
"isStateEditable": false,
"name": "Custom",
"subHooks": [
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "useCustom",
"lineNumber": 0,
},
"id": 0,
"isStateEditable": true,
"name": "State",
"subHooks": [],
"value": "hello world",
},
],
"value": undefined,
},
],
},
]);
]
`);
}
});
it('should inspect a tree of multiple hooks', () => {
@@ -87,58 +157,96 @@ describe('ReactHooksInspection', () => {
);
}
const tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
isStateEditable: false,
id: null,
name: 'Custom',
value: undefined,
debugInfo: null,
subHooks: [
{
isStateEditable: true,
id: 0,
name: 'State',
debugInfo: null,
subHooks: [],
value: 'hello',
expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(`
[
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "Foo",
"lineNumber": 0,
},
{
isStateEditable: false,
id: 1,
name: 'Effect',
debugInfo: null,
subHooks: [],
value: effect,
"id": null,
"isStateEditable": false,
"name": "Custom",
"subHooks": [
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "useCustom",
"lineNumber": 0,
},
"id": 0,
"isStateEditable": true,
"name": "State",
"subHooks": [],
"value": "hello",
},
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "useCustom",
"lineNumber": 0,
},
"id": 1,
"isStateEditable": false,
"name": "Effect",
"subHooks": [],
"value": [Function],
},
],
"value": undefined,
},
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "Foo",
"lineNumber": 0,
},
],
},
{
isStateEditable: false,
id: null,
name: 'Custom',
value: undefined,
debugInfo: null,
subHooks: [
{
isStateEditable: true,
id: 2,
name: 'State',
value: 'world',
debugInfo: null,
subHooks: [],
},
{
isStateEditable: false,
id: 3,
name: 'Effect',
value: effect,
debugInfo: null,
subHooks: [],
},
],
},
]);
"id": null,
"isStateEditable": false,
"name": "Custom",
"subHooks": [
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "useCustom",
"lineNumber": 0,
},
"id": 2,
"isStateEditable": true,
"name": "State",
"subHooks": [],
"value": "world",
},
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "useCustom",
"lineNumber": 0,
},
"id": 3,
"isStateEditable": false,
"name": "Effect",
"subHooks": [],
"value": [Function],
},
],
"value": undefined,
},
]
`);
});
it('should inspect a tree of multiple levels of hooks', () => {
@@ -168,92 +276,154 @@ describe('ReactHooksInspection', () => {
);
}
const tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
isStateEditable: false,
id: null,
name: 'Bar',
value: undefined,
debugInfo: null,
subHooks: [
{
isStateEditable: false,
id: null,
name: 'Custom',
value: undefined,
debugInfo: null,
subHooks: [
{
isStateEditable: true,
id: 0,
name: 'Reducer',
value: 'hello',
debugInfo: null,
subHooks: [],
},
{
isStateEditable: false,
id: 1,
name: 'Effect',
value: effect,
debugInfo: null,
subHooks: [],
},
],
expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(`
[
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "Foo",
"lineNumber": 0,
},
{
isStateEditable: false,
id: 2,
name: 'LayoutEffect',
value: effect,
debugInfo: null,
subHooks: [],
},
],
},
{
isStateEditable: false,
id: null,
name: 'Baz',
value: undefined,
debugInfo: null,
subHooks: [
{
isStateEditable: false,
id: 3,
name: 'LayoutEffect',
value: effect,
debugInfo: null,
subHooks: [],
},
{
isStateEditable: false,
id: null,
name: 'Custom',
debugInfo: null,
subHooks: [
{
isStateEditable: true,
id: 4,
name: 'Reducer',
debugInfo: null,
subHooks: [],
value: 'world',
"id": null,
"isStateEditable": false,
"name": "Bar",
"subHooks": [
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "useBar",
"lineNumber": 0,
},
{
isStateEditable: false,
id: 5,
name: 'Effect',
debugInfo: null,
subHooks: [],
value: effect,
"id": null,
"isStateEditable": false,
"name": "Custom",
"subHooks": [
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "useCustom",
"lineNumber": 0,
},
"id": 0,
"isStateEditable": true,
"name": "Reducer",
"subHooks": [],
"value": "hello",
},
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "useCustom",
"lineNumber": 0,
},
"id": 1,
"isStateEditable": false,
"name": "Effect",
"subHooks": [],
"value": [Function],
},
],
"value": undefined,
},
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "useBar",
"lineNumber": 0,
},
],
value: undefined,
"id": 2,
"isStateEditable": false,
"name": "LayoutEffect",
"subHooks": [],
"value": [Function],
},
],
"value": undefined,
},
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "Foo",
"lineNumber": 0,
},
],
},
]);
"id": null,
"isStateEditable": false,
"name": "Baz",
"subHooks": [
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "useBaz",
"lineNumber": 0,
},
"id": 3,
"isStateEditable": false,
"name": "LayoutEffect",
"subHooks": [],
"value": [Function],
},
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "useBaz",
"lineNumber": 0,
},
"id": null,
"isStateEditable": false,
"name": "Custom",
"subHooks": [
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "useCustom",
"lineNumber": 0,
},
"id": 4,
"isStateEditable": true,
"name": "Reducer",
"subHooks": [],
"value": "world",
},
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "useCustom",
"lineNumber": 0,
},
"id": 5,
"isStateEditable": false,
"name": "Effect",
"subHooks": [],
"value": [Function],
},
],
"value": undefined,
},
],
"value": undefined,
},
]
`);
});
it('should inspect the default value using the useContext hook', () => {
@@ -263,16 +433,24 @@ describe('ReactHooksInspection', () => {
return <div>{value}</div>;
}
const tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
isStateEditable: false,
id: null,
name: 'Context',
value: 'default',
debugInfo: null,
subHooks: [],
},
]);
expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(`
[
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "Foo",
"lineNumber": 0,
},
"id": null,
"isStateEditable": false,
"name": "Context",
"subHooks": [],
"value": "default",
},
]
`);
});
it('should support an injected dispatcher', () => {
@@ -331,41 +509,71 @@ describe('ReactHooksInspection', () => {
);
}
const tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
isStateEditable: false,
id: null,
name: 'Context',
value: 'hi',
debugInfo: null,
subHooks: [],
},
{
isStateEditable: false,
id: null,
name: 'Custom',
value: undefined,
debugInfo: null,
subHooks: [
{
isStateEditable: false,
id: null,
name: 'Promise',
value: 'world',
debugInfo: [{name: 'Hello'}],
subHooks: [],
expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(`
[
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "Foo",
"lineNumber": 0,
},
{
isStateEditable: true,
id: 0,
name: 'State',
value: 'world',
debugInfo: null,
subHooks: [],
"id": null,
"isStateEditable": false,
"name": "Context",
"subHooks": [],
"value": "hi",
},
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "Foo",
"lineNumber": 0,
},
],
},
]);
"id": null,
"isStateEditable": false,
"name": "Custom",
"subHooks": [
{
"debugInfo": [
{
"name": "Hello",
},
],
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "useCustom",
"lineNumber": 0,
},
"id": null,
"isStateEditable": false,
"name": "Promise",
"subHooks": [],
"value": "world",
},
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "useCustom",
"lineNumber": 0,
},
"id": 0,
"isStateEditable": true,
"name": "State",
"subHooks": [],
"value": "world",
},
],
"value": undefined,
},
]
`);
});
it('should inspect use() calls for unresolved Promise', () => {
@@ -376,16 +584,24 @@ describe('ReactHooksInspection', () => {
return <div>{value}</div>;
}
const tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
isStateEditable: false,
id: null,
name: 'Unresolved',
value: promise,
debugInfo: null,
subHooks: [],
},
]);
expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(`
[
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "Foo",
"lineNumber": 0,
},
"id": null,
"isStateEditable": false,
"name": "Unresolved",
"subHooks": [],
"value": Promise {},
},
]
`);
});
describe('useDebugValue', () => {
@@ -408,25 +624,75 @@ describe('ReactHooksInspection', () => {
return null;
}
const tree = ReactDebugTools.inspectHooks(Foo, {});
expect(tree).toEqual([
{
isStateEditable: false,
id: null,
name: 'Custom',
value: __DEV__ ? 'bar:123' : undefined,
debugInfo: null,
subHooks: [
if (__DEV__) {
expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(`
[
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "Foo",
"lineNumber": 0,
},
"id": null,
"isStateEditable": false,
"name": "Custom",
"subHooks": [
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "useCustom",
"lineNumber": 0,
},
"id": 0,
"isStateEditable": true,
"name": "State",
"subHooks": [],
"value": 0,
},
],
"value": "bar:123",
},
]
`);
} else {
expect(normalizeSourceLoc(tree)).toMatchInlineSnapshot(`
[
{
isStateEditable: true,
id: 0,
name: 'State',
debugInfo: null,
subHooks: [],
value: 0,
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "Foo",
"lineNumber": 0,
},
"id": null,
"isStateEditable": false,
"name": "Custom",
"subHooks": [
{
"debugInfo": null,
"hookSource": {
"columnNumber": 0,
"fileName": "**",
"functionName": "useCustom",
"lineNumber": 0,
},
"id": 0,
"isStateEditable": true,
"name": "State",
"subHooks": [],
"value": 0,
},
],
"value": undefined,
},
],
},
]);
]
`);
}
});
});
});

View File

@@ -3301,7 +3301,6 @@ export function attach(
hooks = inspectHooksOfFiber(
fiber,
(renderer.currentDispatcherRef: any),
true, // Include source location info for hooks
);
} finally {
// Restore original console functionality.

View File

@@ -98,7 +98,7 @@ describe('parseHookNames', () => {
});
async function getHookNamesForComponent(Component, props = {}) {
const hooksTree = inspectHooks(Component, props, undefined, true);
const hooksTree = inspectHooks(Component, props, undefined);
const hookNames = await parseHookNames(hooksTree);
return hookNames;
}
@@ -928,7 +928,7 @@ describe('parseHookNames worker', () => {
});
async function getHookNamesForComponent(Component, props = {}) {
const hooksTree = inspectHooks(Component, props, undefined, true);
const hooksTree = inspectHooks(Component, props, undefined);
const hookNames = await parseHookNames(hooksTree);
return hookNames;
}