1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and 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 {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. });