Skip to content

Commit ef4556f

Browse files
author
Juan Tejada
committed
Add logic to look up a Hook name given the generated Hook Map
1 parent cee8040 commit ef4556f

File tree

3 files changed

+482
-1
lines changed

3 files changed

+482
-1
lines changed
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import {parse} from '@babel/parser';
11+
import {generateEncodedHookMap, generateHookMap} from '../generateHookMap';
12+
import {getHookNameForLocation} from '../getHookNameForLocation';
13+
14+
function expectHookMapToEqual(actual, expected) {
15+
expect(actual.names).toEqual(expected.names);
16+
17+
const formattedMappings = [];
18+
actual.mappings.forEach(lines => {
19+
lines.forEach(segment => {
20+
const name = actual.names[segment[2]];
21+
if (name == null) {
22+
throw new Error(`Expected to find name at position ${segment[2]}`);
23+
}
24+
formattedMappings.push(`${name} from ${segment[0]}:${segment[1]}`);
25+
});
26+
});
27+
expect(formattedMappings).toEqual(expected.mappings);
28+
}
29+
30+
describe('generateHookMap', () => {
31+
it('should parse names for built-in hooks', () => {
32+
const code = `
33+
import {useState, useContext, useMemo, useReducer} from 'react';
34+
35+
export function Component() {
36+
const a = useMemo(() => A);
37+
const [b, setB] = useState(0);
38+
39+
// prettier-ignore
40+
const c = useContext(A), d = useContext(B); // eslint-disable-line one-var
41+
42+
const [e, dispatch] = useReducer(reducer, initialState);
43+
const f = useRef(null)
44+
45+
return a + b + c + d + e + f.current;
46+
}`;
47+
48+
const parsed = parse(code, {
49+
sourceType: 'module',
50+
plugins: ['jsx', 'flow'],
51+
});
52+
const hookMap = generateHookMap(parsed);
53+
expectHookMapToEqual(hookMap, {
54+
names: ['<no-hook>', 'a', 'b', 'c', 'd', 'e', 'f'],
55+
mappings: [
56+
'<no-hook> from 1:0',
57+
'a from 5:12',
58+
'<no-hook> from 5:28',
59+
'b from 6:20',
60+
'<no-hook> from 6:31',
61+
'c from 9:12',
62+
'<no-hook> from 9:25',
63+
'd from 9:31',
64+
'<no-hook> from 9:44',
65+
'e from 11:24',
66+
'<no-hook> from 11:57',
67+
'f from 12:12',
68+
'<no-hook> from 12:24',
69+
],
70+
});
71+
72+
expect(getHookNameForLocation({line: 1, column: 0}, hookMap)).toEqual(null);
73+
expect(getHookNameForLocation({line: 2, column: 25}, hookMap)).toEqual(
74+
null,
75+
);
76+
expect(getHookNameForLocation({line: 5, column: 12}, hookMap)).toEqual('a');
77+
expect(getHookNameForLocation({line: 5, column: 13}, hookMap)).toEqual('a');
78+
expect(getHookNameForLocation({line: 5, column: 28}, hookMap)).toEqual(
79+
null,
80+
);
81+
expect(getHookNameForLocation({line: 5, column: 29}, hookMap)).toEqual(
82+
null,
83+
);
84+
expect(getHookNameForLocation({line: 6, column: 20}, hookMap)).toEqual('b');
85+
expect(getHookNameForLocation({line: 6, column: 30}, hookMap)).toEqual('b');
86+
expect(getHookNameForLocation({line: 6, column: 31}, hookMap)).toEqual(
87+
null,
88+
);
89+
expect(getHookNameForLocation({line: 7, column: 31}, hookMap)).toEqual(
90+
null,
91+
);
92+
expect(getHookNameForLocation({line: 8, column: 20}, hookMap)).toEqual(
93+
null,
94+
);
95+
expect(getHookNameForLocation({line: 9, column: 12}, hookMap)).toEqual('c');
96+
expect(getHookNameForLocation({line: 9, column: 13}, hookMap)).toEqual('c');
97+
expect(getHookNameForLocation({line: 9, column: 25}, hookMap)).toEqual(
98+
null,
99+
);
100+
expect(getHookNameForLocation({line: 9, column: 26}, hookMap)).toEqual(
101+
null,
102+
);
103+
expect(getHookNameForLocation({line: 9, column: 31}, hookMap)).toEqual('d');
104+
expect(getHookNameForLocation({line: 9, column: 32}, hookMap)).toEqual('d');
105+
expect(getHookNameForLocation({line: 9, column: 44}, hookMap)).toEqual(
106+
null,
107+
);
108+
expect(getHookNameForLocation({line: 9, column: 45}, hookMap)).toEqual(
109+
null,
110+
);
111+
expect(getHookNameForLocation({line: 11, column: 24}, hookMap)).toEqual(
112+
'e',
113+
);
114+
expect(getHookNameForLocation({line: 11, column: 56}, hookMap)).toEqual(
115+
'e',
116+
);
117+
expect(getHookNameForLocation({line: 11, column: 57}, hookMap)).toEqual(
118+
null,
119+
);
120+
expect(getHookNameForLocation({line: 11, column: 58}, hookMap)).toEqual(
121+
null,
122+
);
123+
expect(getHookNameForLocation({line: 12, column: 12}, hookMap)).toEqual(
124+
'f',
125+
);
126+
expect(getHookNameForLocation({line: 12, column: 23}, hookMap)).toEqual(
127+
'f',
128+
);
129+
expect(getHookNameForLocation({line: 12, column: 24}, hookMap)).toEqual(
130+
null,
131+
);
132+
expect(getHookNameForLocation({line: 100, column: 50}, hookMap)).toEqual(
133+
null,
134+
);
135+
});
136+
137+
it('should parse names for custom hooks', () => {
138+
const code = `
139+
import useTheme from 'useTheme';
140+
import useValue from 'useValue';
141+
142+
export function Component() {
143+
const theme = useTheme();
144+
const [val, setVal] = useValue();
145+
146+
return theme;
147+
}`;
148+
149+
const parsed = parse(code, {
150+
sourceType: 'module',
151+
plugins: ['jsx', 'flow'],
152+
});
153+
const hookMap = generateHookMap(parsed);
154+
expectHookMapToEqual(hookMap, {
155+
names: ['<no-hook>', 'theme', 'val'],
156+
mappings: [
157+
'<no-hook> from 1:0',
158+
'theme from 6:16',
159+
'<no-hook> from 6:26',
160+
'val from 7:24',
161+
'<no-hook> from 7:34',
162+
],
163+
});
164+
165+
expect(getHookNameForLocation({line: 1, column: 0}, hookMap)).toEqual(null);
166+
expect(getHookNameForLocation({line: 6, column: 16}, hookMap)).toEqual(
167+
'theme',
168+
);
169+
expect(getHookNameForLocation({line: 6, column: 26}, hookMap)).toEqual(
170+
null,
171+
);
172+
expect(getHookNameForLocation({line: 7, column: 24}, hookMap)).toEqual(
173+
'val',
174+
);
175+
expect(getHookNameForLocation({line: 7, column: 34}, hookMap)).toEqual(
176+
null,
177+
);
178+
});
179+
180+
it('should parse names for nested hook calls', () => {
181+
const code = `
182+
import {useMemo, useState} from 'react';
183+
184+
export function Component() {
185+
const InnerComponent = useMemo(() => () => {
186+
const [state, setState] = useState(0);
187+
188+
return state;
189+
});
190+
191+
return null;
192+
}`;
193+
194+
const parsed = parse(code, {
195+
sourceType: 'module',
196+
plugins: ['jsx', 'flow'],
197+
});
198+
const hookMap = generateHookMap(parsed);
199+
expectHookMapToEqual(hookMap, {
200+
names: ['<no-hook>', 'InnerComponent', 'state'],
201+
mappings: [
202+
'<no-hook> from 1:0',
203+
'InnerComponent from 5:25',
204+
'state from 6:30',
205+
'InnerComponent from 6:41',
206+
'<no-hook> from 9:4',
207+
],
208+
});
209+
210+
expect(getHookNameForLocation({line: 1, column: 0}, hookMap)).toEqual(null);
211+
expect(getHookNameForLocation({line: 5, column: 25}, hookMap)).toEqual(
212+
'InnerComponent',
213+
);
214+
expect(getHookNameForLocation({line: 6, column: 30}, hookMap)).toEqual(
215+
'state',
216+
);
217+
expect(getHookNameForLocation({line: 6, column: 40}, hookMap)).toEqual(
218+
'state',
219+
);
220+
expect(getHookNameForLocation({line: 6, column: 41}, hookMap)).toEqual(
221+
'InnerComponent',
222+
);
223+
expect(getHookNameForLocation({line: 9, column: 4}, hookMap)).toEqual(null);
224+
});
225+
226+
it('should skip names for non-nameable hooks', () => {
227+
const code = `
228+
import useTheme from 'useTheme';
229+
import useValue from 'useValue';
230+
231+
export function Component() {
232+
const [val, setVal] = useState(0);
233+
234+
useEffect(() => {
235+
// ...
236+
});
237+
238+
useLayoutEffect(() => {
239+
// ...
240+
});
241+
242+
return val;
243+
}`;
244+
245+
const parsed = parse(code, {
246+
sourceType: 'module',
247+
plugins: ['jsx', 'flow'],
248+
});
249+
const hookMap = generateHookMap(parsed);
250+
expectHookMapToEqual(hookMap, {
251+
names: ['<no-hook>', 'val'],
252+
mappings: ['<no-hook> from 1:0', 'val from 6:24', '<no-hook> from 6:35'],
253+
});
254+
255+
expect(getHookNameForLocation({line: 1, column: 0}, hookMap)).toEqual(null);
256+
expect(getHookNameForLocation({line: 6, column: 24}, hookMap)).toEqual(
257+
'val',
258+
);
259+
expect(getHookNameForLocation({line: 6, column: 35}, hookMap)).toEqual(
260+
null,
261+
);
262+
expect(getHookNameForLocation({line: 8, column: 2}, hookMap)).toEqual(null);
263+
});
264+
});

packages/react-devtools-extensions/src/astUtils.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export type SourceFileASTWithHookDetails = {
2525
source: string,
2626
};
2727

28+
export const NO_HOOK_NAME = '<no-hook>';
29+
2830
const AST_NODE_TYPES = Object.freeze({
2931
PROGRAM: 'Program',
3032
CALL_EXPRESSION: 'CallExpression',
@@ -358,7 +360,7 @@ export function getHookNamesMappingFromAST(
358360
traverse(sourceAST, {
359361
[AST_NODE_TYPES.PROGRAM]: {
360362
enter(path) {
361-
pushFrame('<no-hook>', path.node);
363+
pushFrame(NO_HOOK_NAME, path.node);
362364
},
363365
exit(path) {
364366
popFrame(path.node);

0 commit comments

Comments
 (0)