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 {withSyncPerfMeasurements} from 'react-devtools-shared/src/PerformanceLoggingUtils';
    
  11. import traverse from '@babel/traverse';
    
  12. 
    
  13. import type {HooksNode} from 'react-debug-tools/src/ReactDebugHooks';
    
  14. 
    
  15. // Missing types in @babel/traverse
    
  16. type NodePath = any;
    
  17. type Node = any;
    
  18. // Missing types in @babel/types
    
  19. type File = any;
    
  20. 
    
  21. export type Position = {
    
  22.   line: number,
    
  23.   column: number,
    
  24. };
    
  25. 
    
  26. export type SourceFileASTWithHookDetails = {
    
  27.   sourceFileAST: File,
    
  28.   line: number,
    
  29.   source: string,
    
  30. };
    
  31. 
    
  32. export const NO_HOOK_NAME = '<no-hook>';
    
  33. 
    
  34. const AST_NODE_TYPES = Object.freeze({
    
  35.   PROGRAM: 'Program',
    
  36.   CALL_EXPRESSION: 'CallExpression',
    
  37.   MEMBER_EXPRESSION: 'MemberExpression',
    
  38.   ARRAY_PATTERN: 'ArrayPattern',
    
  39.   IDENTIFIER: 'Identifier',
    
  40.   NUMERIC_LITERAL: 'NumericLiteral',
    
  41.   VARIABLE_DECLARATOR: 'VariableDeclarator',
    
  42. });
    
  43. 
    
  44. // Check if line number obtained from source map and the line number in hook node match
    
  45. function checkNodeLocation(
    
  46.   path: NodePath,
    
  47.   line: number,
    
  48.   column?: number | null = null,
    
  49. ): boolean {
    
  50.   const {start, end} = path.node.loc;
    
  51. 
    
  52.   if (line !== start.line) {
    
  53.     return false;
    
  54.   }
    
  55. 
    
  56.   if (column !== null) {
    
  57.     // Column numbers are represented differently between tools/engines.
    
  58.     // Error.prototype.stack columns are 1-based (like most IDEs) but ASTs are 0-based.
    
  59.     //
    
  60.     // In practice this will probably never matter,
    
  61.     // because this code matches the 1-based Error stack location for the hook Identifier (e.g. useState)
    
  62.     // with the larger 0-based VariableDeclarator (e.g. [foo, setFoo] = useState())
    
  63.     // so the ranges should always overlap.
    
  64.     //
    
  65.     // For more info see https://github.com/facebook/react/pull/21833#discussion_r666831276
    
  66.     column -= 1;
    
  67.     if (
    
  68.       (line === start.line && column < start.column) ||
    
  69.       (line === end.line && column > end.column)
    
  70.     ) {
    
  71.       return false;
    
  72.     }
    
  73.   }
    
  74. 
    
  75.   return true;
    
  76. }
    
  77. 
    
  78. // Checks whether hookNode is a member of targetHookNode
    
  79. function filterMemberNodesOfTargetHook(
    
  80.   targetHookNode: NodePath,
    
  81.   hookNode: NodePath,
    
  82. ): boolean {
    
  83.   const targetHookName = targetHookNode.node.id.name;
    
  84.   return (
    
  85.     targetHookName != null &&
    
  86.     (targetHookName ===
    
  87.       (hookNode.node.init.object && hookNode.node.init.object.name) ||
    
  88.       targetHookName === hookNode.node.init.name)
    
  89.   );
    
  90. }
    
  91. 
    
  92. // Checks whether hook is the first member node of a state variable declaration node
    
  93. function filterMemberWithHookVariableName(hook: NodePath): boolean {
    
  94.   return (
    
  95.     hook.node.init.property.type === AST_NODE_TYPES.NUMERIC_LITERAL &&
    
  96.     hook.node.init.property.value === 0
    
  97.   );
    
  98. }
    
  99. 
    
  100. // Returns all AST Nodes associated with 'potentialReactHookASTNode'
    
  101. function getFilteredHookASTNodes(
    
  102.   potentialReactHookASTNode: NodePath,
    
  103.   potentialHooksFound: Array<NodePath>,
    
  104.   source: string,
    
  105. ): Array<NodePath> {
    
  106.   let nodesAssociatedWithReactHookASTNode: NodePath[] = [];
    
  107.   if (nodeContainsHookVariableName(potentialReactHookASTNode)) {
    
  108.     // made custom hooks to enter this, always
    
  109.     // Case 1.
    
  110.     // Directly usable Node -> const ref = useRef(null);
    
  111.     //                      -> const [tick, setTick] = useState(1);
    
  112.     // Case 2.
    
  113.     // Custom Hooks -> const someVariable = useSomeCustomHook();
    
  114.     //              -> const [someVariable, someFunction] = useAnotherCustomHook();
    
  115.     nodesAssociatedWithReactHookASTNode.unshift(potentialReactHookASTNode);
    
  116.   } else {
    
  117.     // Case 3.
    
  118.     // Indirectly usable Node -> const tickState = useState(1);
    
  119.     //                           [tick, setTick] = tickState;
    
  120.     //                        -> const tickState = useState(1);
    
  121.     //                           const tick = tickState[0];
    
  122.     //                           const setTick = tickState[1];
    
  123.     nodesAssociatedWithReactHookASTNode = potentialHooksFound.filter(hookNode =>
    
  124.       filterMemberNodesOfTargetHook(potentialReactHookASTNode, hookNode),
    
  125.     );
    
  126.   }
    
  127.   return nodesAssociatedWithReactHookASTNode;
    
  128. }
    
  129. 
    
  130. // Returns Hook name
    
  131. export function getHookName(
    
  132.   hook: HooksNode,
    
  133.   originalSourceAST: mixed,
    
  134.   originalSourceCode: string,
    
  135.   originalSourceLineNumber: number,
    
  136.   originalSourceColumnNumber: number,
    
  137. ): string | null {
    
  138.   const hooksFromAST = withSyncPerfMeasurements(
    
  139.     'getPotentialHookDeclarationsFromAST(originalSourceAST)',
    
  140.     () => getPotentialHookDeclarationsFromAST(originalSourceAST),
    
  141.   );
    
  142. 
    
  143.   let potentialReactHookASTNode = null;
    
  144.   if (originalSourceColumnNumber === 0) {
    
  145.     // This most likely indicates a source map type like 'cheap-module-source-map'
    
  146.     // that intentionally drops column numbers for compilation speed in DEV builds.
    
  147.     // In this case, we can assume there's probably only one hook per line (true in most cases)
    
  148.     // and just fail if we find more than one match.
    
  149.     const matchingNodes = hooksFromAST.filter(node => {
    
  150.       const nodeLocationCheck = checkNodeLocation(
    
  151.         node,
    
  152.         originalSourceLineNumber,
    
  153.       );
    
  154. 
    
  155.       const hookDeclarationCheck = isConfirmedHookDeclaration(node);
    
  156.       return nodeLocationCheck && hookDeclarationCheck;
    
  157.     });
    
  158. 
    
  159.     if (matchingNodes.length === 1) {
    
  160.       potentialReactHookASTNode = matchingNodes[0];
    
  161.     }
    
  162.   } else {
    
  163.     potentialReactHookASTNode = hooksFromAST.find(node => {
    
  164.       const nodeLocationCheck = checkNodeLocation(
    
  165.         node,
    
  166.         originalSourceLineNumber,
    
  167.         originalSourceColumnNumber,
    
  168.       );
    
  169. 
    
  170.       const hookDeclarationCheck = isConfirmedHookDeclaration(node);
    
  171.       return nodeLocationCheck && hookDeclarationCheck;
    
  172.     });
    
  173.   }
    
  174. 
    
  175.   if (!potentialReactHookASTNode) {
    
  176.     return null;
    
  177.   }
    
  178. 
    
  179.   // nodesAssociatedWithReactHookASTNode could directly be used to obtain the hook variable name
    
  180.   // depending on the type of potentialReactHookASTNode
    
  181.   try {
    
  182.     const nodesAssociatedWithReactHookASTNode = withSyncPerfMeasurements(
    
  183.       'getFilteredHookASTNodes()',
    
  184.       () =>
    
  185.         getFilteredHookASTNodes(
    
  186.           potentialReactHookASTNode,
    
  187.           hooksFromAST,
    
  188.           originalSourceCode,
    
  189.         ),
    
  190.     );
    
  191. 
    
  192.     const name = withSyncPerfMeasurements('getHookNameFromNode()', () =>
    
  193.       getHookNameFromNode(
    
  194.         hook,
    
  195.         nodesAssociatedWithReactHookASTNode,
    
  196.         potentialReactHookASTNode,
    
  197.       ),
    
  198.     );
    
  199. 
    
  200.     return name;
    
  201.   } catch (error) {
    
  202.     console.error(error);
    
  203.     return null;
    
  204.   }
    
  205. }
    
  206. 
    
  207. function getHookNameFromNode(
    
  208.   originalHook: HooksNode,
    
  209.   nodesAssociatedWithReactHookASTNode: NodePath[],
    
  210.   potentialReactHookASTNode: NodePath,
    
  211. ): string | null {
    
  212.   let hookVariableName: string | null;
    
  213.   const isCustomHook = originalHook.id === null;
    
  214. 
    
  215.   switch (nodesAssociatedWithReactHookASTNode.length) {
    
  216.     case 1:
    
  217.       // CASE 1A (nodesAssociatedWithReactHookASTNode[0] !== potentialReactHookASTNode):
    
  218.       // const flagState = useState(true); -> later referenced as
    
  219.       // const [flag, setFlag] = flagState;
    
  220.       //
    
  221.       // CASE 1B (nodesAssociatedWithReactHookASTNode[0] === potentialReactHookASTNode):
    
  222.       // const [flag, setFlag] = useState(true); -> we have access to the hook variable straight away
    
  223.       //
    
  224.       // CASE 1C (isCustomHook && nodesAssociatedWithReactHookASTNode[0] === potentialReactHookASTNode):
    
  225.       // const someVariable = useSomeCustomHook(); -> we have access to hook variable straight away
    
  226.       // const [someVariable, someFunction] = useAnotherCustomHook(); -> we ignore variable names in this case
    
  227.       //                                                                 as it is unclear what variable name to show
    
  228.       if (
    
  229.         isCustomHook &&
    
  230.         nodesAssociatedWithReactHookASTNode[0] === potentialReactHookASTNode
    
  231.       ) {
    
  232.         hookVariableName = getHookVariableName(
    
  233.           potentialReactHookASTNode,
    
  234.           isCustomHook,
    
  235.         );
    
  236.         break;
    
  237.       }
    
  238.       hookVariableName = getHookVariableName(
    
  239.         nodesAssociatedWithReactHookASTNode[0],
    
  240.       );
    
  241.       break;
    
  242. 
    
  243.     case 2:
    
  244.       // const flagState = useState(true); -> later referenced as
    
  245.       // const flag = flagState[0];
    
  246.       // const setFlag = flagState[1];
    
  247.       nodesAssociatedWithReactHookASTNode =
    
  248.         nodesAssociatedWithReactHookASTNode.filter(hookPath =>
    
  249.           filterMemberWithHookVariableName(hookPath),
    
  250.         );
    
  251. 
    
  252.       if (nodesAssociatedWithReactHookASTNode.length !== 1) {
    
  253.         // Something went wrong, only a single desirable hook should remain here
    
  254.         throw new Error("Couldn't isolate AST Node containing hook variable.");
    
  255.       }
    
  256.       hookVariableName = getHookVariableName(
    
  257.         nodesAssociatedWithReactHookASTNode[0],
    
  258.       );
    
  259.       break;
    
  260. 
    
  261.     default:
    
  262.       // Case 0:
    
  263.       // const flagState = useState(true); -> which is not accessed anywhere
    
  264.       //
    
  265.       // Case > 2 (fallback):
    
  266.       // const someState = React.useState(() => 0)
    
  267.       //
    
  268.       // const stateVariable = someState[0]
    
  269.       // const setStateVariable = someState[1]
    
  270.       //
    
  271.       // const [number2, setNumber2] = someState
    
  272.       //
    
  273.       // We assign the state variable for 'someState' to multiple variables,
    
  274.       // and hence cannot isolate a unique variable name. In such cases,
    
  275.       // default to showing 'someState'
    
  276. 
    
  277.       hookVariableName = getHookVariableName(potentialReactHookASTNode);
    
  278.       break;
    
  279.   }
    
  280. 
    
  281.   return hookVariableName;
    
  282. }
    
  283. 
    
  284. // Extracts the variable name from hook node path
    
  285. function getHookVariableName(
    
  286.   hook: NodePath,
    
  287.   isCustomHook: boolean = false,
    
  288. ): string | null {
    
  289.   const nodeType = hook.node.id.type;
    
  290.   switch (nodeType) {
    
  291.     case AST_NODE_TYPES.ARRAY_PATTERN:
    
  292.       return !isCustomHook ? hook.node.id.elements[0]?.name ?? null : null;
    
  293. 
    
  294.     case AST_NODE_TYPES.IDENTIFIER:
    
  295.       return hook.node.id.name;
    
  296. 
    
  297.     default:
    
  298.       return null;
    
  299.   }
    
  300. }
    
  301. 
    
  302. function getPotentialHookDeclarationsFromAST(sourceAST: File): NodePath[] {
    
  303.   const potentialHooksFound: NodePath[] = [];
    
  304.   withSyncPerfMeasurements('traverse(sourceAST)', () =>
    
  305.     traverse(sourceAST, {
    
  306.       enter(path) {
    
  307.         if (path.isVariableDeclarator() && isPotentialHookDeclaration(path)) {
    
  308.           potentialHooksFound.push(path);
    
  309.         }
    
  310.       },
    
  311.     }),
    
  312.   );
    
  313.   return potentialHooksFound;
    
  314. }
    
  315. 
    
  316. /**
    
  317.  * This function traverses the sourceAST and returns a mapping
    
  318.  * that maps locations in the source code to their corresponding
    
  319.  * Hook name, if there is a relevant Hook name for that location.
    
  320.  *
    
  321.  * A location in the source code is represented by line and column
    
  322.  * numbers as a Position object: { line, column }.
    
  323.  *   - line is 1-indexed.
    
  324.  *   - column is 0-indexed.
    
  325.  *
    
  326.  * A Hook name will be assigned to a Hook CallExpression if the
    
  327.  * CallExpression is for a variable declaration (i.e. it returns
    
  328.  * a value that is assigned to a variable), and if we can reliably
    
  329.  * infer the correct name to use (see comments in the function body
    
  330.  * for more details).
    
  331.  *
    
  332.  * The returned mapping is an array of locations and their assigned
    
  333.  * names, sorted by location. Specifically, each entry in the array
    
  334.  * contains a `name` and a `start` Position. The `name` of a given
    
  335.  * entry is the "assigned" name in the source code until the `start`
    
  336.  * of the **next** entry. This means that given the mapping, in order
    
  337.  * to determine the Hook name assigned for a given source location, we
    
  338.  * need to find the adjacent entries that most closely contain the given
    
  339.  * location.
    
  340.  *
    
  341.  * E.g. for the following code:
    
  342.  *
    
  343.  * 1|  function Component() {
    
  344.  * 2|    const [state, setState] = useState(0);
    
  345.  * 3|                              ^---------^ -> Cols 28 - 38: Hook CallExpression
    
  346.  * 4|
    
  347.  * 5|    useEffect(() => {...}); -> call ignored since not declaring a variable
    
  348.  * 6|
    
  349.  * 7|    return (...);
    
  350.  * 8|  }
    
  351.  *
    
  352.  * The returned "mapping" would be something like:
    
  353.  *   [
    
  354.  *     {name: '<no-hook>', start: {line: 1, column: 0}},
    
  355.  *     {name: 'state', start: {line: 2, column: 28}},
    
  356.  *     {name: '<no-hook>', start: {line: 2, column: 38}},
    
  357.  *   ]
    
  358.  *
    
  359.  * Where the Hook name `state` (corresponding to the `state` variable)
    
  360.  * is assigned to the location in the code for the CallExpression
    
  361.  * representing the call to `useState(0)` (line 2, col 28-38).
    
  362.  */
    
  363. export function getHookNamesMappingFromAST(
    
  364.   sourceAST: File,
    
  365. ): $ReadOnlyArray<{name: string, start: Position}> {
    
  366.   const hookStack: Array<{name: string, start: $FlowFixMe}> = [];
    
  367.   const hookNames = [];
    
  368.   const pushFrame = (name: string, node: Node) => {
    
  369.     const nameInfo = {name, start: {...node.loc.start}};
    
  370.     hookStack.unshift(nameInfo);
    
  371.     hookNames.push(nameInfo);
    
  372.   };
    
  373.   const popFrame = (node: Node) => {
    
  374.     hookStack.shift();
    
  375.     const top = hookStack[0];
    
  376.     if (top != null) {
    
  377.       hookNames.push({name: top.name, start: {...node.loc.end}});
    
  378.     }
    
  379.   };
    
  380. 
    
  381.   traverse(sourceAST, {
    
  382.     [AST_NODE_TYPES.PROGRAM]: {
    
  383.       enter(path) {
    
  384.         pushFrame(NO_HOOK_NAME, path.node);
    
  385.       },
    
  386.       exit(path) {
    
  387.         popFrame(path.node);
    
  388.       },
    
  389.     },
    
  390.     [AST_NODE_TYPES.VARIABLE_DECLARATOR]: {
    
  391.       enter(path) {
    
  392.         // Check if this variable declaration corresponds to a variable
    
  393.         // declared by calling a Hook.
    
  394.         if (isConfirmedHookDeclaration(path)) {
    
  395.           const hookDeclaredVariableName = getHookVariableName(path);
    
  396.           if (!hookDeclaredVariableName) {
    
  397.             return;
    
  398.           }
    
  399.           const callExpressionNode = assertCallExpression(path.node.init);
    
  400. 
    
  401.           // Check if this variable declaration corresponds to a call to a
    
  402.           // built-in Hook that returns a tuple (useState, useReducer,
    
  403.           // useTransition).
    
  404.           // If it doesn't, we immediately use the declared variable name
    
  405.           // as the Hook name. We do this because for any other Hooks that
    
  406.           // aren't the built-in Hooks that return a tuple, we can't reliably
    
  407.           // extract a Hook name from other variable declarations derived from
    
  408.           // this one, since we don't know which of the declared variables
    
  409.           // are the relevant ones to track and show in dev tools.
    
  410.           if (!isBuiltInHookThatReturnsTuple(path)) {
    
  411.             pushFrame(hookDeclaredVariableName, callExpressionNode);
    
  412.             return;
    
  413.           }
    
  414. 
    
  415.           // Check if the variable declared by the Hook call is referenced
    
  416.           // anywhere else in the code. If not, we immediately use the
    
  417.           // declared variable name as the Hook name.
    
  418.           const referencePaths =
    
  419.             hookDeclaredVariableName != null
    
  420.               ? path.scope.bindings[hookDeclaredVariableName]?.referencePaths
    
  421.               : null;
    
  422.           if (referencePaths == null) {
    
  423.             pushFrame(hookDeclaredVariableName, callExpressionNode);
    
  424.             return;
    
  425.           }
    
  426. 
    
  427.           // Check each reference to the variable declared by the Hook call,
    
  428.           // and for each, we do the following:
    
  429.           let declaredVariableName = null;
    
  430.           for (let i = 0; i <= referencePaths.length; i++) {
    
  431.             const referencePath = referencePaths[i];
    
  432.             if (declaredVariableName != null) {
    
  433.               break;
    
  434.             }
    
  435. 
    
  436.             // 1. Check if the reference is contained within a VariableDeclarator
    
  437.             // Node. This will allow us to determine if the variable declared by
    
  438.             // the Hook call is being used to declare other variables.
    
  439.             let variableDeclaratorPath = referencePath;
    
  440.             while (
    
  441.               variableDeclaratorPath != null &&
    
  442.               variableDeclaratorPath.node.type !==
    
  443.                 AST_NODE_TYPES.VARIABLE_DECLARATOR
    
  444.             ) {
    
  445.               variableDeclaratorPath = variableDeclaratorPath.parentPath;
    
  446.             }
    
  447. 
    
  448.             // 2. If we find a VariableDeclarator containing the
    
  449.             // referenced variable, we extract the Hook name from the new
    
  450.             // variable declaration.
    
  451.             // E.g., a case like the following:
    
  452.             //    const countState = useState(0);
    
  453.             //    const count = countState[0];
    
  454.             //    const setCount = countState[1]
    
  455.             // Where the reference to `countState` is later referenced
    
  456.             // within a VariableDeclarator, so we can extract `count` as
    
  457.             // the Hook name.
    
  458.             const varDeclInit = variableDeclaratorPath?.node.init;
    
  459.             if (varDeclInit != null) {
    
  460.               switch (varDeclInit.type) {
    
  461.                 case AST_NODE_TYPES.MEMBER_EXPRESSION: {
    
  462.                   // When encountering a MemberExpression inside the new
    
  463.                   // variable declaration, we only want to extract the variable
    
  464.                   // name if we're assigning the value of the first member,
    
  465.                   // which is handled by `filterMemberWithHookVariableName`.
    
  466.                   // E.g.
    
  467.                   //    const countState = useState(0);
    
  468.                   //    const count = countState[0];    -> extract the name from this reference
    
  469.                   //    const setCount = countState[1]; -> ignore this reference
    
  470.                   if (
    
  471.                     filterMemberWithHookVariableName(variableDeclaratorPath)
    
  472.                   ) {
    
  473.                     declaredVariableName = getHookVariableName(
    
  474.                       variableDeclaratorPath,
    
  475.                     );
    
  476.                   }
    
  477.                   break;
    
  478.                 }
    
  479.                 case AST_NODE_TYPES.IDENTIFIER: {
    
  480.                   declaredVariableName = getHookVariableName(
    
  481.                     variableDeclaratorPath,
    
  482.                   );
    
  483.                   break;
    
  484.                 }
    
  485.                 default:
    
  486.                   break;
    
  487.               }
    
  488.             }
    
  489.           }
    
  490. 
    
  491.           // If we were able to extract a name from the new variable
    
  492.           // declaration, use it as the Hook name. Otherwise, use the
    
  493.           // original declared variable as the variable name.
    
  494.           if (declaredVariableName != null) {
    
  495.             pushFrame(declaredVariableName, callExpressionNode);
    
  496.           } else {
    
  497.             pushFrame(hookDeclaredVariableName, callExpressionNode);
    
  498.           }
    
  499.         }
    
  500.       },
    
  501.       exit(path) {
    
  502.         if (isConfirmedHookDeclaration(path)) {
    
  503.           const callExpressionNode = assertCallExpression(path.node.init);
    
  504.           popFrame(callExpressionNode);
    
  505.         }
    
  506.       },
    
  507.     },
    
  508.   });
    
  509.   return hookNames;
    
  510. }
    
  511. 
    
  512. // Check if 'path' contains declaration of the form const X = useState(0);
    
  513. function isConfirmedHookDeclaration(path: NodePath): boolean {
    
  514.   const nodeInit = path.node.init;
    
  515.   if (nodeInit == null || nodeInit.type !== AST_NODE_TYPES.CALL_EXPRESSION) {
    
  516.     return false;
    
  517.   }
    
  518.   const callee = nodeInit.callee;
    
  519.   return isHook(callee);
    
  520. }
    
  521. 
    
  522. // We consider hooks to be a hook name identifier or a member expression containing a hook name.
    
  523. function isHook(node: Node): boolean {
    
  524.   if (node.type === AST_NODE_TYPES.IDENTIFIER) {
    
  525.     return isHookName(node.name);
    
  526.   } else if (
    
  527.     node.type === AST_NODE_TYPES.MEMBER_EXPRESSION &&
    
  528.     !node.computed &&
    
  529.     isHook(node.property)
    
  530.   ) {
    
  531.     const obj = node.object;
    
  532.     const isPascalCaseNameSpace = /^[A-Z].*/;
    
  533.     return (
    
  534.       obj.type === AST_NODE_TYPES.IDENTIFIER &&
    
  535.       isPascalCaseNameSpace.test(obj.name)
    
  536.     );
    
  537.   } else {
    
  538.     // TODO Possibly handle inline require statements e.g. require("useStable")(...)
    
  539.     // This does not seem like a high priority, since inline requires are probably
    
  540.     // not common and are also typically in compiled code rather than source code.
    
  541. 
    
  542.     return false;
    
  543.   }
    
  544. }
    
  545. 
    
  546. // Catch all identifiers that begin with "use"
    
  547. // followed by an uppercase Latin character to exclude identifiers like "user".
    
  548. // Copied from packages/eslint-plugin-react-hooks/src/RulesOfHooks
    
  549. function isHookName(name: string): boolean {
    
  550.   return /^use[A-Z0-9].*$/.test(name);
    
  551. }
    
  552. 
    
  553. // Check if the AST Node COULD be a React Hook
    
  554. function isPotentialHookDeclaration(path: NodePath): boolean {
    
  555.   // The array potentialHooksFound will contain all potential hook declaration cases we support
    
  556.   const nodePathInit = path.node.init;
    
  557.   if (nodePathInit != null) {
    
  558.     if (nodePathInit.type === AST_NODE_TYPES.CALL_EXPRESSION) {
    
  559.       // CASE: CallExpression
    
  560.       // 1. const [count, setCount] = useState(0); -> destructured pattern
    
  561.       // 2. const [A, setA] = useState(0), const [B, setB] = useState(0); -> multiple inline declarations
    
  562.       // 3. const [
    
  563.       //      count,
    
  564.       //      setCount
    
  565.       //    ] = useState(0); -> multiline hook declaration
    
  566.       // 4. const ref = useRef(null); -> generic hooks
    
  567.       const callee = nodePathInit.callee;
    
  568.       return isHook(callee);
    
  569.     } else if (
    
  570.       nodePathInit.type === AST_NODE_TYPES.MEMBER_EXPRESSION ||
    
  571.       nodePathInit.type === AST_NODE_TYPES.IDENTIFIER
    
  572.     ) {
    
  573.       // CASE: MemberExpression
    
  574.       //    const countState = React.useState(0);
    
  575.       //    const count = countState[0];
    
  576.       //    const setCount = countState[1]; -> Accessing members following hook declaration
    
  577. 
    
  578.       // CASE: Identifier
    
  579.       //    const countState = React.useState(0);
    
  580.       //    const [count, setCount] = countState; ->  destructuring syntax following hook declaration
    
  581.       return true;
    
  582.     }
    
  583.   }
    
  584.   return false;
    
  585. }
    
  586. 
    
  587. /// Check whether 'node' is hook declaration of form useState(0); OR React.useState(0);
    
  588. function isReactFunction(node: Node, functionName: string): boolean {
    
  589.   return (
    
  590.     node.name === functionName ||
    
  591.     (node.type === 'MemberExpression' &&
    
  592.       node.object.name === 'React' &&
    
  593.       node.property.name === functionName)
    
  594.   );
    
  595. }
    
  596. 
    
  597. // Check if 'path' is either State or Reducer hook
    
  598. function isBuiltInHookThatReturnsTuple(path: NodePath): boolean {
    
  599.   const callee = path.node.init.callee;
    
  600.   return (
    
  601.     isReactFunction(callee, 'useState') ||
    
  602.     isReactFunction(callee, 'useReducer') ||
    
  603.     isReactFunction(callee, 'useTransition')
    
  604.   );
    
  605. }
    
  606. 
    
  607. // Check whether hookNode of a declaration contains obvious variable name
    
  608. function nodeContainsHookVariableName(hookNode: NodePath): boolean {
    
  609.   // We determine cases where variable names are obvious in declarations. Examples:
    
  610.   // const [tick, setTick] = useState(1); OR const ref = useRef(null);
    
  611.   // Here tick/ref are obvious hook variables in the hook declaration node itself
    
  612.   // 1. True for satisfying above cases
    
  613.   // 2. False for everything else. Examples:
    
  614.   //    const countState = React.useState(0);
    
  615.   //    const count = countState[0];
    
  616.   //    const setCount = countState[1]; -> not obvious, hook variable can't be determined
    
  617.   //                                       from the hook declaration node alone
    
  618.   // 3. For custom hooks we force pass true since we are only concerned with the AST node
    
  619.   //    regardless of how it is accessed in source code. (See: getHookVariableName)
    
  620. 
    
  621.   const node = hookNode.node.id;
    
  622.   if (
    
  623.     node.type === AST_NODE_TYPES.ARRAY_PATTERN ||
    
  624.     (node.type === AST_NODE_TYPES.IDENTIFIER &&
    
  625.       !isBuiltInHookThatReturnsTuple(hookNode))
    
  626.   ) {
    
  627.     return true;
    
  628.   }
    
  629.   return false;
    
  630. }
    
  631. 
    
  632. function assertCallExpression(node: Node): Node {
    
  633.   if (node.type !== AST_NODE_TYPES.CALL_EXPRESSION) {
    
  634.     throw new Error('Expected a CallExpression node for a Hook declaration.');
    
  635.   }
    
  636.   return node;
    
  637. }