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. 
    
  8. /* eslint-disable no-for-of-loops/no-for-of-loops */
    
  9. 
    
  10. 'use strict';
    
  11. 
    
  12. export default {
    
  13.   meta: {
    
  14.     type: 'suggestion',
    
  15.     docs: {
    
  16.       description:
    
  17.         'verifies the list of dependencies for Hooks like useEffect and similar',
    
  18.       recommended: true,
    
  19.       url: 'https://github.com/facebook/react/issues/14920',
    
  20.     },
    
  21.     fixable: 'code',
    
  22.     hasSuggestions: true,
    
  23.     schema: [
    
  24.       {
    
  25.         type: 'object',
    
  26.         additionalProperties: false,
    
  27.         enableDangerousAutofixThisMayCauseInfiniteLoops: false,
    
  28.         properties: {
    
  29.           additionalHooks: {
    
  30.             type: 'string',
    
  31.           },
    
  32.           enableDangerousAutofixThisMayCauseInfiniteLoops: {
    
  33.             type: 'boolean',
    
  34.           },
    
  35.         },
    
  36.       },
    
  37.     ],
    
  38.   },
    
  39.   create(context) {
    
  40.     // Parse the `additionalHooks` regex.
    
  41.     const additionalHooks =
    
  42.       context.options &&
    
  43.       context.options[0] &&
    
  44.       context.options[0].additionalHooks
    
  45.         ? new RegExp(context.options[0].additionalHooks)
    
  46.         : undefined;
    
  47. 
    
  48.     const enableDangerousAutofixThisMayCauseInfiniteLoops =
    
  49.       (context.options &&
    
  50.         context.options[0] &&
    
  51.         context.options[0].enableDangerousAutofixThisMayCauseInfiniteLoops) ||
    
  52.       false;
    
  53. 
    
  54.     const options = {
    
  55.       additionalHooks,
    
  56.       enableDangerousAutofixThisMayCauseInfiniteLoops,
    
  57.     };
    
  58. 
    
  59.     function reportProblem(problem) {
    
  60.       if (enableDangerousAutofixThisMayCauseInfiniteLoops) {
    
  61.         // Used to enable legacy behavior. Dangerous.
    
  62.         // Keep this as an option until major IDEs upgrade (including VSCode FB ESLint extension).
    
  63.         if (Array.isArray(problem.suggest) && problem.suggest.length > 0) {
    
  64.           problem.fix = problem.suggest[0].fix;
    
  65.         }
    
  66.       }
    
  67.       context.report(problem);
    
  68.     }
    
  69. 
    
  70.     const scopeManager = context.getSourceCode().scopeManager;
    
  71. 
    
  72.     // Should be shared between visitors.
    
  73.     const setStateCallSites = new WeakMap();
    
  74.     const stateVariables = new WeakSet();
    
  75.     const stableKnownValueCache = new WeakMap();
    
  76.     const functionWithoutCapturedValueCache = new WeakMap();
    
  77.     const useEffectEventVariables = new WeakSet();
    
  78.     function memoizeWithWeakMap(fn, map) {
    
  79.       return function (arg) {
    
  80.         if (map.has(arg)) {
    
  81.           // to verify cache hits:
    
  82.           // console.log(arg.name)
    
  83.           return map.get(arg);
    
  84.         }
    
  85.         const result = fn(arg);
    
  86.         map.set(arg, result);
    
  87.         return result;
    
  88.       };
    
  89.     }
    
  90.     /**
    
  91.      * Visitor for both function expressions and arrow function expressions.
    
  92.      */
    
  93.     function visitFunctionWithDependencies(
    
  94.       node,
    
  95.       declaredDependenciesNode,
    
  96.       reactiveHook,
    
  97.       reactiveHookName,
    
  98.       isEffect,
    
  99.     ) {
    
  100.       if (isEffect && node.async) {
    
  101.         reportProblem({
    
  102.           node: node,
    
  103.           message:
    
  104.             `Effect callbacks are synchronous to prevent race conditions. ` +
    
  105.             `Put the async function inside:\n\n` +
    
  106.             'useEffect(() => {\n' +
    
  107.             '  async function fetchData() {\n' +
    
  108.             '    // You can await here\n' +
    
  109.             '    const response = await MyAPI.getData(someId);\n' +
    
  110.             '    // ...\n' +
    
  111.             '  }\n' +
    
  112.             '  fetchData();\n' +
    
  113.             `}, [someId]); // Or [] if effect doesn't need props or state\n\n` +
    
  114.             'Learn more about data fetching with Hooks: https://reactjs.org/link/hooks-data-fetching',
    
  115.         });
    
  116.       }
    
  117. 
    
  118.       // Get the current scope.
    
  119.       const scope = scopeManager.acquire(node);
    
  120. 
    
  121.       // Find all our "pure scopes". On every re-render of a component these
    
  122.       // pure scopes may have changes to the variables declared within. So all
    
  123.       // variables used in our reactive hook callback but declared in a pure
    
  124.       // scope need to be listed as dependencies of our reactive hook callback.
    
  125.       //
    
  126.       // According to the rules of React you can't read a mutable value in pure
    
  127.       // scope. We can't enforce this in a lint so we trust that all variables
    
  128.       // declared outside of pure scope are indeed frozen.
    
  129.       const pureScopes = new Set();
    
  130.       let componentScope = null;
    
  131.       {
    
  132.         let currentScope = scope.upper;
    
  133.         while (currentScope) {
    
  134.           pureScopes.add(currentScope);
    
  135.           if (currentScope.type === 'function') {
    
  136.             break;
    
  137.           }
    
  138.           currentScope = currentScope.upper;
    
  139.         }
    
  140.         // If there is no parent function scope then there are no pure scopes.
    
  141.         // The ones we've collected so far are incorrect. So don't continue with
    
  142.         // the lint.
    
  143.         if (!currentScope) {
    
  144.           return;
    
  145.         }
    
  146.         componentScope = currentScope;
    
  147.       }
    
  148. 
    
  149.       const isArray = Array.isArray;
    
  150. 
    
  151.       // Next we'll define a few helpers that helps us
    
  152.       // tell if some values don't have to be declared as deps.
    
  153. 
    
  154.       // Some are known to be stable based on Hook calls.
    
  155.       // const [state, setState] = useState() / React.useState()
    
  156.       //               ^^^ true for this reference
    
  157.       // const [state, dispatch] = useReducer() / React.useReducer()
    
  158.       //               ^^^ true for this reference
    
  159.       // const ref = useRef()
    
  160.       //       ^^^ true for this reference
    
  161.       // const onStuff = useEffectEvent(() => {})
    
  162.       //       ^^^ true for this reference
    
  163.       // False for everything else.
    
  164.       function isStableKnownHookValue(resolved) {
    
  165.         if (!isArray(resolved.defs)) {
    
  166.           return false;
    
  167.         }
    
  168.         const def = resolved.defs[0];
    
  169.         if (def == null) {
    
  170.           return false;
    
  171.         }
    
  172.         // Look for `let stuff = ...`
    
  173.         if (def.node.type !== 'VariableDeclarator') {
    
  174.           return false;
    
  175.         }
    
  176.         let init = def.node.init;
    
  177.         if (init == null) {
    
  178.           return false;
    
  179.         }
    
  180.         while (init.type === 'TSAsExpression' || init.type === 'AsExpression') {
    
  181.           init = init.expression;
    
  182.         }
    
  183.         // Detect primitive constants
    
  184.         // const foo = 42
    
  185.         let declaration = def.node.parent;
    
  186.         if (declaration == null) {
    
  187.           // This might happen if variable is declared after the callback.
    
  188.           // In that case ESLint won't set up .parent refs.
    
  189.           // So we'll set them up manually.
    
  190.           fastFindReferenceWithParent(componentScope.block, def.node.id);
    
  191.           declaration = def.node.parent;
    
  192.           if (declaration == null) {
    
  193.             return false;
    
  194.           }
    
  195.         }
    
  196.         if (
    
  197.           declaration.kind === 'const' &&
    
  198.           init.type === 'Literal' &&
    
  199.           (typeof init.value === 'string' ||
    
  200.             typeof init.value === 'number' ||
    
  201.             init.value === null)
    
  202.         ) {
    
  203.           // Definitely stable
    
  204.           return true;
    
  205.         }
    
  206.         // Detect known Hook calls
    
  207.         // const [_, setState] = useState()
    
  208.         if (init.type !== 'CallExpression') {
    
  209.           return false;
    
  210.         }
    
  211.         let callee = init.callee;
    
  212.         // Step into `= React.something` initializer.
    
  213.         if (
    
  214.           callee.type === 'MemberExpression' &&
    
  215.           callee.object.name === 'React' &&
    
  216.           callee.property != null &&
    
  217.           !callee.computed
    
  218.         ) {
    
  219.           callee = callee.property;
    
  220.         }
    
  221.         if (callee.type !== 'Identifier') {
    
  222.           return false;
    
  223.         }
    
  224.         const id = def.node.id;
    
  225.         const {name} = callee;
    
  226.         if (name === 'useRef' && id.type === 'Identifier') {
    
  227.           // useRef() return value is stable.
    
  228.           return true;
    
  229.         } else if (
    
  230.           isUseEffectEventIdentifier(callee) &&
    
  231.           id.type === 'Identifier'
    
  232.         ) {
    
  233.           for (const ref of resolved.references) {
    
  234.             if (ref !== id) {
    
  235.               useEffectEventVariables.add(ref.identifier);
    
  236.             }
    
  237.           }
    
  238.           // useEffectEvent() return value is always unstable.
    
  239.           return true;
    
  240.         } else if (name === 'useState' || name === 'useReducer') {
    
  241.           // Only consider second value in initializing tuple stable.
    
  242.           if (
    
  243.             id.type === 'ArrayPattern' &&
    
  244.             id.elements.length === 2 &&
    
  245.             isArray(resolved.identifiers)
    
  246.           ) {
    
  247.             // Is second tuple value the same reference we're checking?
    
  248.             if (id.elements[1] === resolved.identifiers[0]) {
    
  249.               if (name === 'useState') {
    
  250.                 const references = resolved.references;
    
  251.                 let writeCount = 0;
    
  252.                 for (let i = 0; i < references.length; i++) {
    
  253.                   if (references[i].isWrite()) {
    
  254.                     writeCount++;
    
  255.                   }
    
  256.                   if (writeCount > 1) {
    
  257.                     return false;
    
  258.                   }
    
  259.                   setStateCallSites.set(
    
  260.                     references[i].identifier,
    
  261.                     id.elements[0],
    
  262.                   );
    
  263.                 }
    
  264.               }
    
  265.               // Setter is stable.
    
  266.               return true;
    
  267.             } else if (id.elements[0] === resolved.identifiers[0]) {
    
  268.               if (name === 'useState') {
    
  269.                 const references = resolved.references;
    
  270.                 for (let i = 0; i < references.length; i++) {
    
  271.                   stateVariables.add(references[i].identifier);
    
  272.                 }
    
  273.               }
    
  274.               // State variable itself is dynamic.
    
  275.               return false;
    
  276.             }
    
  277.           }
    
  278.         } else if (name === 'useTransition') {
    
  279.           // Only consider second value in initializing tuple stable.
    
  280.           if (
    
  281.             id.type === 'ArrayPattern' &&
    
  282.             id.elements.length === 2 &&
    
  283.             Array.isArray(resolved.identifiers)
    
  284.           ) {
    
  285.             // Is second tuple value the same reference we're checking?
    
  286.             if (id.elements[1] === resolved.identifiers[0]) {
    
  287.               // Setter is stable.
    
  288.               return true;
    
  289.             }
    
  290.           }
    
  291.         }
    
  292.         // By default assume it's dynamic.
    
  293.         return false;
    
  294.       }
    
  295. 
    
  296.       // Some are just functions that don't reference anything dynamic.
    
  297.       function isFunctionWithoutCapturedValues(resolved) {
    
  298.         if (!isArray(resolved.defs)) {
    
  299.           return false;
    
  300.         }
    
  301.         const def = resolved.defs[0];
    
  302.         if (def == null) {
    
  303.           return false;
    
  304.         }
    
  305.         if (def.node == null || def.node.id == null) {
    
  306.           return false;
    
  307.         }
    
  308.         // Search the direct component subscopes for
    
  309.         // top-level function definitions matching this reference.
    
  310.         const fnNode = def.node;
    
  311.         const childScopes = componentScope.childScopes;
    
  312.         let fnScope = null;
    
  313.         let i;
    
  314.         for (i = 0; i < childScopes.length; i++) {
    
  315.           const childScope = childScopes[i];
    
  316.           const childScopeBlock = childScope.block;
    
  317.           if (
    
  318.             // function handleChange() {}
    
  319.             (fnNode.type === 'FunctionDeclaration' &&
    
  320.               childScopeBlock === fnNode) ||
    
  321.             // const handleChange = () => {}
    
  322.             // const handleChange = function() {}
    
  323.             (fnNode.type === 'VariableDeclarator' &&
    
  324.               childScopeBlock.parent === fnNode)
    
  325.           ) {
    
  326.             // Found it!
    
  327.             fnScope = childScope;
    
  328.             break;
    
  329.           }
    
  330.         }
    
  331.         if (fnScope == null) {
    
  332.           return false;
    
  333.         }
    
  334.         // Does this function capture any values
    
  335.         // that are in pure scopes (aka render)?
    
  336.         for (i = 0; i < fnScope.through.length; i++) {
    
  337.           const ref = fnScope.through[i];
    
  338.           if (ref.resolved == null) {
    
  339.             continue;
    
  340.           }
    
  341.           if (
    
  342.             pureScopes.has(ref.resolved.scope) &&
    
  343.             // Stable values are fine though,
    
  344.             // although we won't check functions deeper.
    
  345.             !memoizedIsStableKnownHookValue(ref.resolved)
    
  346.           ) {
    
  347.             return false;
    
  348.           }
    
  349.         }
    
  350.         // If we got here, this function doesn't capture anything
    
  351.         // from render--or everything it captures is known stable.
    
  352.         return true;
    
  353.       }
    
  354. 
    
  355.       // Remember such values. Avoid re-running extra checks on them.
    
  356.       const memoizedIsStableKnownHookValue = memoizeWithWeakMap(
    
  357.         isStableKnownHookValue,
    
  358.         stableKnownValueCache,
    
  359.       );
    
  360.       const memoizedIsFunctionWithoutCapturedValues = memoizeWithWeakMap(
    
  361.         isFunctionWithoutCapturedValues,
    
  362.         functionWithoutCapturedValueCache,
    
  363.       );
    
  364. 
    
  365.       // These are usually mistaken. Collect them.
    
  366.       const currentRefsInEffectCleanup = new Map();
    
  367. 
    
  368.       // Is this reference inside a cleanup function for this effect node?
    
  369.       // We can check by traversing scopes upwards from the reference, and checking
    
  370.       // if the last "return () => " we encounter is located directly inside the effect.
    
  371.       function isInsideEffectCleanup(reference) {
    
  372.         let curScope = reference.from;
    
  373.         let isInReturnedFunction = false;
    
  374.         while (curScope.block !== node) {
    
  375.           if (curScope.type === 'function') {
    
  376.             isInReturnedFunction =
    
  377.               curScope.block.parent != null &&
    
  378.               curScope.block.parent.type === 'ReturnStatement';
    
  379.           }
    
  380.           curScope = curScope.upper;
    
  381.         }
    
  382.         return isInReturnedFunction;
    
  383.       }
    
  384. 
    
  385.       // Get dependencies from all our resolved references in pure scopes.
    
  386.       // Key is dependency string, value is whether it's stable.
    
  387.       const dependencies = new Map();
    
  388.       const optionalChains = new Map();
    
  389.       gatherDependenciesRecursively(scope);
    
  390. 
    
  391.       function gatherDependenciesRecursively(currentScope) {
    
  392.         for (const reference of currentScope.references) {
    
  393.           // If this reference is not resolved or it is not declared in a pure
    
  394.           // scope then we don't care about this reference.
    
  395.           if (!reference.resolved) {
    
  396.             continue;
    
  397.           }
    
  398.           if (!pureScopes.has(reference.resolved.scope)) {
    
  399.             continue;
    
  400.           }
    
  401. 
    
  402.           // Narrow the scope of a dependency if it is, say, a member expression.
    
  403.           // Then normalize the narrowed dependency.
    
  404.           const referenceNode = fastFindReferenceWithParent(
    
  405.             node,
    
  406.             reference.identifier,
    
  407.           );
    
  408.           const dependencyNode = getDependency(referenceNode);
    
  409.           const dependency = analyzePropertyChain(
    
  410.             dependencyNode,
    
  411.             optionalChains,
    
  412.           );
    
  413. 
    
  414.           // Accessing ref.current inside effect cleanup is bad.
    
  415.           if (
    
  416.             // We're in an effect...
    
  417.             isEffect &&
    
  418.             // ... and this look like accessing .current...
    
  419.             dependencyNode.type === 'Identifier' &&
    
  420.             (dependencyNode.parent.type === 'MemberExpression' ||
    
  421.               dependencyNode.parent.type === 'OptionalMemberExpression') &&
    
  422.             !dependencyNode.parent.computed &&
    
  423.             dependencyNode.parent.property.type === 'Identifier' &&
    
  424.             dependencyNode.parent.property.name === 'current' &&
    
  425.             // ...in a cleanup function or below...
    
  426.             isInsideEffectCleanup(reference)
    
  427.           ) {
    
  428.             currentRefsInEffectCleanup.set(dependency, {
    
  429.               reference,
    
  430.               dependencyNode,
    
  431.             });
    
  432.           }
    
  433. 
    
  434.           if (
    
  435.             dependencyNode.parent.type === 'TSTypeQuery' ||
    
  436.             dependencyNode.parent.type === 'TSTypeReference'
    
  437.           ) {
    
  438.             continue;
    
  439.           }
    
  440. 
    
  441.           const def = reference.resolved.defs[0];
    
  442.           if (def == null) {
    
  443.             continue;
    
  444.           }
    
  445.           // Ignore references to the function itself as it's not defined yet.
    
  446.           if (def.node != null && def.node.init === node.parent) {
    
  447.             continue;
    
  448.           }
    
  449.           // Ignore Flow type parameters
    
  450.           if (def.type === 'TypeParameter') {
    
  451.             continue;
    
  452.           }
    
  453. 
    
  454.           // Add the dependency to a map so we can make sure it is referenced
    
  455.           // again in our dependencies array. Remember whether it's stable.
    
  456.           if (!dependencies.has(dependency)) {
    
  457.             const resolved = reference.resolved;
    
  458.             const isStable =
    
  459.               memoizedIsStableKnownHookValue(resolved) ||
    
  460.               memoizedIsFunctionWithoutCapturedValues(resolved);
    
  461.             dependencies.set(dependency, {
    
  462.               isStable,
    
  463.               references: [reference],
    
  464.             });
    
  465.           } else {
    
  466.             dependencies.get(dependency).references.push(reference);
    
  467.           }
    
  468.         }
    
  469. 
    
  470.         for (const childScope of currentScope.childScopes) {
    
  471.           gatherDependenciesRecursively(childScope);
    
  472.         }
    
  473.       }
    
  474. 
    
  475.       // Warn about accessing .current in cleanup effects.
    
  476.       currentRefsInEffectCleanup.forEach(
    
  477.         ({reference, dependencyNode}, dependency) => {
    
  478.           const references = reference.resolved.references;
    
  479.           // Is React managing this ref or us?
    
  480.           // Let's see if we can find a .current assignment.
    
  481.           let foundCurrentAssignment = false;
    
  482.           for (let i = 0; i < references.length; i++) {
    
  483.             const {identifier} = references[i];
    
  484.             const {parent} = identifier;
    
  485.             if (
    
  486.               parent != null &&
    
  487.               // ref.current
    
  488.               // Note: no need to handle OptionalMemberExpression because it can't be LHS.
    
  489.               parent.type === 'MemberExpression' &&
    
  490.               !parent.computed &&
    
  491.               parent.property.type === 'Identifier' &&
    
  492.               parent.property.name === 'current' &&
    
  493.               // ref.current = <something>
    
  494.               parent.parent.type === 'AssignmentExpression' &&
    
  495.               parent.parent.left === parent
    
  496.             ) {
    
  497.               foundCurrentAssignment = true;
    
  498.               break;
    
  499.             }
    
  500.           }
    
  501.           // We only want to warn about React-managed refs.
    
  502.           if (foundCurrentAssignment) {
    
  503.             return;
    
  504.           }
    
  505.           reportProblem({
    
  506.             node: dependencyNode.parent.property,
    
  507.             message:
    
  508.               `The ref value '${dependency}.current' will likely have ` +
    
  509.               `changed by the time this effect cleanup function runs. If ` +
    
  510.               `this ref points to a node rendered by React, copy ` +
    
  511.               `'${dependency}.current' to a variable inside the effect, and ` +
    
  512.               `use that variable in the cleanup function.`,
    
  513.           });
    
  514.         },
    
  515.       );
    
  516. 
    
  517.       // Warn about assigning to variables in the outer scope.
    
  518.       // Those are usually bugs.
    
  519.       const staleAssignments = new Set();
    
  520.       function reportStaleAssignment(writeExpr, key) {
    
  521.         if (staleAssignments.has(key)) {
    
  522.           return;
    
  523.         }
    
  524.         staleAssignments.add(key);
    
  525.         reportProblem({
    
  526.           node: writeExpr,
    
  527.           message:
    
  528.             `Assignments to the '${key}' variable from inside React Hook ` +
    
  529.             `${context.getSource(reactiveHook)} will be lost after each ` +
    
  530.             `render. To preserve the value over time, store it in a useRef ` +
    
  531.             `Hook and keep the mutable value in the '.current' property. ` +
    
  532.             `Otherwise, you can move this variable directly inside ` +
    
  533.             `${context.getSource(reactiveHook)}.`,
    
  534.         });
    
  535.       }
    
  536. 
    
  537.       // Remember which deps are stable and report bad usage first.
    
  538.       const stableDependencies = new Set();
    
  539.       dependencies.forEach(({isStable, references}, key) => {
    
  540.         if (isStable) {
    
  541.           stableDependencies.add(key);
    
  542.         }
    
  543.         references.forEach(reference => {
    
  544.           if (reference.writeExpr) {
    
  545.             reportStaleAssignment(reference.writeExpr, key);
    
  546.           }
    
  547.         });
    
  548.       });
    
  549. 
    
  550.       if (staleAssignments.size > 0) {
    
  551.         // The intent isn't clear so we'll wait until you fix those first.
    
  552.         return;
    
  553.       }
    
  554. 
    
  555.       if (!declaredDependenciesNode) {
    
  556.         // Check if there are any top-level setState() calls.
    
  557.         // Those tend to lead to infinite loops.
    
  558.         let setStateInsideEffectWithoutDeps = null;
    
  559.         dependencies.forEach(({isStable, references}, key) => {
    
  560.           if (setStateInsideEffectWithoutDeps) {
    
  561.             return;
    
  562.           }
    
  563.           references.forEach(reference => {
    
  564.             if (setStateInsideEffectWithoutDeps) {
    
  565.               return;
    
  566.             }
    
  567. 
    
  568.             const id = reference.identifier;
    
  569.             const isSetState = setStateCallSites.has(id);
    
  570.             if (!isSetState) {
    
  571.               return;
    
  572.             }
    
  573. 
    
  574.             let fnScope = reference.from;
    
  575.             while (fnScope.type !== 'function') {
    
  576.               fnScope = fnScope.upper;
    
  577.             }
    
  578.             const isDirectlyInsideEffect = fnScope.block === node;
    
  579.             if (isDirectlyInsideEffect) {
    
  580.               // TODO: we could potentially ignore early returns.
    
  581.               setStateInsideEffectWithoutDeps = key;
    
  582.             }
    
  583.           });
    
  584.         });
    
  585.         if (setStateInsideEffectWithoutDeps) {
    
  586.           const {suggestedDependencies} = collectRecommendations({
    
  587.             dependencies,
    
  588.             declaredDependencies: [],
    
  589.             stableDependencies,
    
  590.             externalDependencies: new Set(),
    
  591.             isEffect: true,
    
  592.           });
    
  593.           reportProblem({
    
  594.             node: reactiveHook,
    
  595.             message:
    
  596.               `React Hook ${reactiveHookName} contains a call to '${setStateInsideEffectWithoutDeps}'. ` +
    
  597.               `Without a list of dependencies, this can lead to an infinite chain of updates. ` +
    
  598.               `To fix this, pass [` +
    
  599.               suggestedDependencies.join(', ') +
    
  600.               `] as a second argument to the ${reactiveHookName} Hook.`,
    
  601.             suggest: [
    
  602.               {
    
  603.                 desc: `Add dependencies array: [${suggestedDependencies.join(
    
  604.                   ', ',
    
  605.                 )}]`,
    
  606.                 fix(fixer) {
    
  607.                   return fixer.insertTextAfter(
    
  608.                     node,
    
  609.                     `, [${suggestedDependencies.join(', ')}]`,
    
  610.                   );
    
  611.                 },
    
  612.               },
    
  613.             ],
    
  614.           });
    
  615.         }
    
  616.         return;
    
  617.       }
    
  618. 
    
  619.       const declaredDependencies = [];
    
  620.       const externalDependencies = new Set();
    
  621.       if (declaredDependenciesNode.type !== 'ArrayExpression') {
    
  622.         // If the declared dependencies are not an array expression then we
    
  623.         // can't verify that the user provided the correct dependencies. Tell
    
  624.         // the user this in an error.
    
  625.         reportProblem({
    
  626.           node: declaredDependenciesNode,
    
  627.           message:
    
  628.             `React Hook ${context.getSource(reactiveHook)} was passed a ` +
    
  629.             'dependency list that is not an array literal. This means we ' +
    
  630.             "can't statically verify whether you've passed the correct " +
    
  631.             'dependencies.',
    
  632.         });
    
  633.       } else {
    
  634.         declaredDependenciesNode.elements.forEach(declaredDependencyNode => {
    
  635.           // Skip elided elements.
    
  636.           if (declaredDependencyNode === null) {
    
  637.             return;
    
  638.           }
    
  639.           // If we see a spread element then add a special warning.
    
  640.           if (declaredDependencyNode.type === 'SpreadElement') {
    
  641.             reportProblem({
    
  642.               node: declaredDependencyNode,
    
  643.               message:
    
  644.                 `React Hook ${context.getSource(reactiveHook)} has a spread ` +
    
  645.                 "element in its dependency array. This means we can't " +
    
  646.                 "statically verify whether you've passed the " +
    
  647.                 'correct dependencies.',
    
  648.             });
    
  649.             return;
    
  650.           }
    
  651.           if (useEffectEventVariables.has(declaredDependencyNode)) {
    
  652.             reportProblem({
    
  653.               node: declaredDependencyNode,
    
  654.               message:
    
  655.                 'Functions returned from `useEffectEvent` must not be included in the dependency array. ' +
    
  656.                 `Remove \`${context.getSource(
    
  657.                   declaredDependencyNode,
    
  658.                 )}\` from the list.`,
    
  659.               suggest: [
    
  660.                 {
    
  661.                   desc: `Remove the dependency \`${context.getSource(
    
  662.                     declaredDependencyNode,
    
  663.                   )}\``,
    
  664.                   fix(fixer) {
    
  665.                     return fixer.removeRange(declaredDependencyNode.range);
    
  666.                   },
    
  667.                 },
    
  668.               ],
    
  669.             });
    
  670.           }
    
  671.           // Try to normalize the declared dependency. If we can't then an error
    
  672.           // will be thrown. We will catch that error and report an error.
    
  673.           let declaredDependency;
    
  674.           try {
    
  675.             declaredDependency = analyzePropertyChain(
    
  676.               declaredDependencyNode,
    
  677.               null,
    
  678.             );
    
  679.           } catch (error) {
    
  680.             if (/Unsupported node type/.test(error.message)) {
    
  681.               if (declaredDependencyNode.type === 'Literal') {
    
  682.                 if (dependencies.has(declaredDependencyNode.value)) {
    
  683.                   reportProblem({
    
  684.                     node: declaredDependencyNode,
    
  685.                     message:
    
  686.                       `The ${declaredDependencyNode.raw} literal is not a valid dependency ` +
    
  687.                       `because it never changes. ` +
    
  688.                       `Did you mean to include ${declaredDependencyNode.value} in the array instead?`,
    
  689.                   });
    
  690.                 } else {
    
  691.                   reportProblem({
    
  692.                     node: declaredDependencyNode,
    
  693.                     message:
    
  694.                       `The ${declaredDependencyNode.raw} literal is not a valid dependency ` +
    
  695.                       'because it never changes. You can safely remove it.',
    
  696.                   });
    
  697.                 }
    
  698.               } else {
    
  699.                 reportProblem({
    
  700.                   node: declaredDependencyNode,
    
  701.                   message:
    
  702.                     `React Hook ${context.getSource(reactiveHook)} has a ` +
    
  703.                     `complex expression in the dependency array. ` +
    
  704.                     'Extract it to a separate variable so it can be statically checked.',
    
  705.                 });
    
  706.               }
    
  707. 
    
  708.               return;
    
  709.             } else {
    
  710.               throw error;
    
  711.             }
    
  712.           }
    
  713. 
    
  714.           let maybeID = declaredDependencyNode;
    
  715.           while (
    
  716.             maybeID.type === 'MemberExpression' ||
    
  717.             maybeID.type === 'OptionalMemberExpression' ||
    
  718.             maybeID.type === 'ChainExpression'
    
  719.           ) {
    
  720.             maybeID = maybeID.object || maybeID.expression.object;
    
  721.           }
    
  722.           const isDeclaredInComponent = !componentScope.through.some(
    
  723.             ref => ref.identifier === maybeID,
    
  724.           );
    
  725. 
    
  726.           // Add the dependency to our declared dependency map.
    
  727.           declaredDependencies.push({
    
  728.             key: declaredDependency,
    
  729.             node: declaredDependencyNode,
    
  730.           });
    
  731. 
    
  732.           if (!isDeclaredInComponent) {
    
  733.             externalDependencies.add(declaredDependency);
    
  734.           }
    
  735.         });
    
  736.       }
    
  737. 
    
  738.       const {
    
  739.         suggestedDependencies,
    
  740.         unnecessaryDependencies,
    
  741.         missingDependencies,
    
  742.         duplicateDependencies,
    
  743.       } = collectRecommendations({
    
  744.         dependencies,
    
  745.         declaredDependencies,
    
  746.         stableDependencies,
    
  747.         externalDependencies,
    
  748.         isEffect,
    
  749.       });
    
  750. 
    
  751.       let suggestedDeps = suggestedDependencies;
    
  752. 
    
  753.       const problemCount =
    
  754.         duplicateDependencies.size +
    
  755.         missingDependencies.size +
    
  756.         unnecessaryDependencies.size;
    
  757. 
    
  758.       if (problemCount === 0) {
    
  759.         // If nothing else to report, check if some dependencies would
    
  760.         // invalidate on every render.
    
  761.         const constructions = scanForConstructions({
    
  762.           declaredDependencies,
    
  763.           declaredDependenciesNode,
    
  764.           componentScope,
    
  765.           scope,
    
  766.         });
    
  767.         constructions.forEach(
    
  768.           ({construction, isUsedOutsideOfHook, depType}) => {
    
  769.             const wrapperHook =
    
  770.               depType === 'function' ? 'useCallback' : 'useMemo';
    
  771. 
    
  772.             const constructionType =
    
  773.               depType === 'function' ? 'definition' : 'initialization';
    
  774. 
    
  775.             const defaultAdvice = `wrap the ${constructionType} of '${construction.name.name}' in its own ${wrapperHook}() Hook.`;
    
  776. 
    
  777.             const advice = isUsedOutsideOfHook
    
  778.               ? `To fix this, ${defaultAdvice}`
    
  779.               : `Move it inside the ${reactiveHookName} callback. Alternatively, ${defaultAdvice}`;
    
  780. 
    
  781.             const causation =
    
  782.               depType === 'conditional' || depType === 'logical expression'
    
  783.                 ? 'could make'
    
  784.                 : 'makes';
    
  785. 
    
  786.             const message =
    
  787.               `The '${construction.name.name}' ${depType} ${causation} the dependencies of ` +
    
  788.               `${reactiveHookName} Hook (at line ${declaredDependenciesNode.loc.start.line}) ` +
    
  789.               `change on every render. ${advice}`;
    
  790. 
    
  791.             let suggest;
    
  792.             // Only handle the simple case of variable assignments.
    
  793.             // Wrapping function declarations can mess up hoisting.
    
  794.             if (
    
  795.               isUsedOutsideOfHook &&
    
  796.               construction.type === 'Variable' &&
    
  797.               // Objects may be mutated after construction, which would make this
    
  798.               // fix unsafe. Functions _probably_ won't be mutated, so we'll
    
  799.               // allow this fix for them.
    
  800.               depType === 'function'
    
  801.             ) {
    
  802.               suggest = [
    
  803.                 {
    
  804.                   desc: `Wrap the ${constructionType} of '${construction.name.name}' in its own ${wrapperHook}() Hook.`,
    
  805.                   fix(fixer) {
    
  806.                     const [before, after] =
    
  807.                       wrapperHook === 'useMemo'
    
  808.                         ? [`useMemo(() => { return `, '; })']
    
  809.                         : ['useCallback(', ')'];
    
  810.                     return [
    
  811.                       // TODO: also add an import?
    
  812.                       fixer.insertTextBefore(construction.node.init, before),
    
  813.                       // TODO: ideally we'd gather deps here but it would require
    
  814.                       // restructuring the rule code. This will cause a new lint
    
  815.                       // error to appear immediately for useCallback. Note we're
    
  816.                       // not adding [] because would that changes semantics.
    
  817.                       fixer.insertTextAfter(construction.node.init, after),
    
  818.                     ];
    
  819.                   },
    
  820.                 },
    
  821.               ];
    
  822.             }
    
  823.             // TODO: What if the function needs to change on every render anyway?
    
  824.             // Should we suggest removing effect deps as an appropriate fix too?
    
  825.             reportProblem({
    
  826.               // TODO: Why not report this at the dependency site?
    
  827.               node: construction.node,
    
  828.               message,
    
  829.               suggest,
    
  830.             });
    
  831.           },
    
  832.         );
    
  833.         return;
    
  834.       }
    
  835. 
    
  836.       // If we're going to report a missing dependency,
    
  837.       // we might as well recalculate the list ignoring
    
  838.       // the currently specified deps. This can result
    
  839.       // in some extra deduplication. We can't do this
    
  840.       // for effects though because those have legit
    
  841.       // use cases for over-specifying deps.
    
  842.       if (!isEffect && missingDependencies.size > 0) {
    
  843.         suggestedDeps = collectRecommendations({
    
  844.           dependencies,
    
  845.           declaredDependencies: [], // Pretend we don't know
    
  846.           stableDependencies,
    
  847.           externalDependencies,
    
  848.           isEffect,
    
  849.         }).suggestedDependencies;
    
  850.       }
    
  851. 
    
  852.       // Alphabetize the suggestions, but only if deps were already alphabetized.
    
  853.       function areDeclaredDepsAlphabetized() {
    
  854.         if (declaredDependencies.length === 0) {
    
  855.           return true;
    
  856.         }
    
  857.         const declaredDepKeys = declaredDependencies.map(dep => dep.key);
    
  858.         const sortedDeclaredDepKeys = declaredDepKeys.slice().sort();
    
  859.         return declaredDepKeys.join(',') === sortedDeclaredDepKeys.join(',');
    
  860.       }
    
  861.       if (areDeclaredDepsAlphabetized()) {
    
  862.         suggestedDeps.sort();
    
  863.       }
    
  864. 
    
  865.       // Most of our algorithm deals with dependency paths with optional chaining stripped.
    
  866.       // This function is the last step before printing a dependency, so now is a good time to
    
  867.       // check whether any members in our path are always used as optional-only. In that case,
    
  868.       // we will use ?. instead of . to concatenate those parts of the path.
    
  869.       function formatDependency(path) {
    
  870.         const members = path.split('.');
    
  871.         let finalPath = '';
    
  872.         for (let i = 0; i < members.length; i++) {
    
  873.           if (i !== 0) {
    
  874.             const pathSoFar = members.slice(0, i + 1).join('.');
    
  875.             const isOptional = optionalChains.get(pathSoFar) === true;
    
  876.             finalPath += isOptional ? '?.' : '.';
    
  877.           }
    
  878.           finalPath += members[i];
    
  879.         }
    
  880.         return finalPath;
    
  881.       }
    
  882. 
    
  883.       function getWarningMessage(deps, singlePrefix, label, fixVerb) {
    
  884.         if (deps.size === 0) {
    
  885.           return null;
    
  886.         }
    
  887.         return (
    
  888.           (deps.size > 1 ? '' : singlePrefix + ' ') +
    
  889.           label +
    
  890.           ' ' +
    
  891.           (deps.size > 1 ? 'dependencies' : 'dependency') +
    
  892.           ': ' +
    
  893.           joinEnglish(
    
  894.             Array.from(deps)
    
  895.               .sort()
    
  896.               .map(name => "'" + formatDependency(name) + "'"),
    
  897.           ) +
    
  898.           `. Either ${fixVerb} ${
    
  899.             deps.size > 1 ? 'them' : 'it'
    
  900.           } or remove the dependency array.`
    
  901.         );
    
  902.       }
    
  903. 
    
  904.       let extraWarning = '';
    
  905.       if (unnecessaryDependencies.size > 0) {
    
  906.         let badRef = null;
    
  907.         Array.from(unnecessaryDependencies.keys()).forEach(key => {
    
  908.           if (badRef !== null) {
    
  909.             return;
    
  910.           }
    
  911.           if (key.endsWith('.current')) {
    
  912.             badRef = key;
    
  913.           }
    
  914.         });
    
  915.         if (badRef !== null) {
    
  916.           extraWarning =
    
  917.             ` Mutable values like '${badRef}' aren't valid dependencies ` +
    
  918.             "because mutating them doesn't re-render the component.";
    
  919.         } else if (externalDependencies.size > 0) {
    
  920.           const dep = Array.from(externalDependencies)[0];
    
  921.           // Don't show this warning for things that likely just got moved *inside* the callback
    
  922.           // because in that case they're clearly not referring to globals.
    
  923.           if (!scope.set.has(dep)) {
    
  924.             extraWarning =
    
  925.               ` Outer scope values like '${dep}' aren't valid dependencies ` +
    
  926.               `because mutating them doesn't re-render the component.`;
    
  927.           }
    
  928.         }
    
  929.       }
    
  930. 
    
  931.       // `props.foo()` marks `props` as a dependency because it has
    
  932.       // a `this` value. This warning can be confusing.
    
  933.       // So if we're going to show it, append a clarification.
    
  934.       if (!extraWarning && missingDependencies.has('props')) {
    
  935.         const propDep = dependencies.get('props');
    
  936.         if (propDep == null) {
    
  937.           return;
    
  938.         }
    
  939.         const refs = propDep.references;
    
  940.         if (!Array.isArray(refs)) {
    
  941.           return;
    
  942.         }
    
  943.         let isPropsOnlyUsedInMembers = true;
    
  944.         for (let i = 0; i < refs.length; i++) {
    
  945.           const ref = refs[i];
    
  946.           const id = fastFindReferenceWithParent(
    
  947.             componentScope.block,
    
  948.             ref.identifier,
    
  949.           );
    
  950.           if (!id) {
    
  951.             isPropsOnlyUsedInMembers = false;
    
  952.             break;
    
  953.           }
    
  954.           const parent = id.parent;
    
  955.           if (parent == null) {
    
  956.             isPropsOnlyUsedInMembers = false;
    
  957.             break;
    
  958.           }
    
  959.           if (
    
  960.             parent.type !== 'MemberExpression' &&
    
  961.             parent.type !== 'OptionalMemberExpression'
    
  962.           ) {
    
  963.             isPropsOnlyUsedInMembers = false;
    
  964.             break;
    
  965.           }
    
  966.         }
    
  967.         if (isPropsOnlyUsedInMembers) {
    
  968.           extraWarning =
    
  969.             ` However, 'props' will change when *any* prop changes, so the ` +
    
  970.             `preferred fix is to destructure the 'props' object outside of ` +
    
  971.             `the ${reactiveHookName} call and refer to those specific props ` +
    
  972.             `inside ${context.getSource(reactiveHook)}.`;
    
  973.         }
    
  974.       }
    
  975. 
    
  976.       if (!extraWarning && missingDependencies.size > 0) {
    
  977.         // See if the user is trying to avoid specifying a callable prop.
    
  978.         // This usually means they're unaware of useCallback.
    
  979.         let missingCallbackDep = null;
    
  980.         missingDependencies.forEach(missingDep => {
    
  981.           if (missingCallbackDep) {
    
  982.             return;
    
  983.           }
    
  984.           // Is this a variable from top scope?
    
  985.           const topScopeRef = componentScope.set.get(missingDep);
    
  986.           const usedDep = dependencies.get(missingDep);
    
  987.           if (usedDep.references[0].resolved !== topScopeRef) {
    
  988.             return;
    
  989.           }
    
  990.           // Is this a destructured prop?
    
  991.           const def = topScopeRef.defs[0];
    
  992.           if (def == null || def.name == null || def.type !== 'Parameter') {
    
  993.             return;
    
  994.           }
    
  995.           // Was it called in at least one case? Then it's a function.
    
  996.           let isFunctionCall = false;
    
  997.           let id;
    
  998.           for (let i = 0; i < usedDep.references.length; i++) {
    
  999.             id = usedDep.references[i].identifier;
    
  1000.             if (
    
  1001.               id != null &&
    
  1002.               id.parent != null &&
    
  1003.               (id.parent.type === 'CallExpression' ||
    
  1004.                 id.parent.type === 'OptionalCallExpression') &&
    
  1005.               id.parent.callee === id
    
  1006.             ) {
    
  1007.               isFunctionCall = true;
    
  1008.               break;
    
  1009.             }
    
  1010.           }
    
  1011.           if (!isFunctionCall) {
    
  1012.             return;
    
  1013.           }
    
  1014.           // If it's missing (i.e. in component scope) *and* it's a parameter
    
  1015.           // then it is definitely coming from props destructuring.
    
  1016.           // (It could also be props itself but we wouldn't be calling it then.)
    
  1017.           missingCallbackDep = missingDep;
    
  1018.         });
    
  1019.         if (missingCallbackDep !== null) {
    
  1020.           extraWarning =
    
  1021.             ` If '${missingCallbackDep}' changes too often, ` +
    
  1022.             `find the parent component that defines it ` +
    
  1023.             `and wrap that definition in useCallback.`;
    
  1024.         }
    
  1025.       }
    
  1026. 
    
  1027.       if (!extraWarning && missingDependencies.size > 0) {
    
  1028.         let setStateRecommendation = null;
    
  1029.         missingDependencies.forEach(missingDep => {
    
  1030.           if (setStateRecommendation !== null) {
    
  1031.             return;
    
  1032.           }
    
  1033.           const usedDep = dependencies.get(missingDep);
    
  1034.           const references = usedDep.references;
    
  1035.           let id;
    
  1036.           let maybeCall;
    
  1037.           for (let i = 0; i < references.length; i++) {
    
  1038.             id = references[i].identifier;
    
  1039.             maybeCall = id.parent;
    
  1040.             // Try to see if we have setState(someExpr(missingDep)).
    
  1041.             while (maybeCall != null && maybeCall !== componentScope.block) {
    
  1042.               if (maybeCall.type === 'CallExpression') {
    
  1043.                 const correspondingStateVariable = setStateCallSites.get(
    
  1044.                   maybeCall.callee,
    
  1045.                 );
    
  1046.                 if (correspondingStateVariable != null) {
    
  1047.                   if (correspondingStateVariable.name === missingDep) {
    
  1048.                     // setCount(count + 1)
    
  1049.                     setStateRecommendation = {
    
  1050.                       missingDep,
    
  1051.                       setter: maybeCall.callee.name,
    
  1052.                       form: 'updater',
    
  1053.                     };
    
  1054.                   } else if (stateVariables.has(id)) {
    
  1055.                     // setCount(count + increment)
    
  1056.                     setStateRecommendation = {
    
  1057.                       missingDep,
    
  1058.                       setter: maybeCall.callee.name,
    
  1059.                       form: 'reducer',
    
  1060.                     };
    
  1061.                   } else {
    
  1062.                     const resolved = references[i].resolved;
    
  1063.                     if (resolved != null) {
    
  1064.                       // If it's a parameter *and* a missing dep,
    
  1065.                       // it must be a prop or something inside a prop.
    
  1066.                       // Therefore, recommend an inline reducer.
    
  1067.                       const def = resolved.defs[0];
    
  1068.                       if (def != null && def.type === 'Parameter') {
    
  1069.                         setStateRecommendation = {
    
  1070.                           missingDep,
    
  1071.                           setter: maybeCall.callee.name,
    
  1072.                           form: 'inlineReducer',
    
  1073.                         };
    
  1074.                       }
    
  1075.                     }
    
  1076.                   }
    
  1077.                   break;
    
  1078.                 }
    
  1079.               }
    
  1080.               maybeCall = maybeCall.parent;
    
  1081.             }
    
  1082.             if (setStateRecommendation !== null) {
    
  1083.               break;
    
  1084.             }
    
  1085.           }
    
  1086.         });
    
  1087.         if (setStateRecommendation !== null) {
    
  1088.           switch (setStateRecommendation.form) {
    
  1089.             case 'reducer':
    
  1090.               extraWarning =
    
  1091.                 ` You can also replace multiple useState variables with useReducer ` +
    
  1092.                 `if '${setStateRecommendation.setter}' needs the ` +
    
  1093.                 `current value of '${setStateRecommendation.missingDep}'.`;
    
  1094.               break;
    
  1095.             case 'inlineReducer':
    
  1096.               extraWarning =
    
  1097.                 ` If '${setStateRecommendation.setter}' needs the ` +
    
  1098.                 `current value of '${setStateRecommendation.missingDep}', ` +
    
  1099.                 `you can also switch to useReducer instead of useState and ` +
    
  1100.                 `read '${setStateRecommendation.missingDep}' in the reducer.`;
    
  1101.               break;
    
  1102.             case 'updater':
    
  1103.               extraWarning =
    
  1104.                 ` You can also do a functional update '${
    
  1105.                   setStateRecommendation.setter
    
  1106.                 }(${setStateRecommendation.missingDep.slice(
    
  1107.                   0,
    
  1108.                   1,
    
  1109.                 )} => ...)' if you only need '${
    
  1110.                   setStateRecommendation.missingDep
    
  1111.                 }'` + ` in the '${setStateRecommendation.setter}' call.`;
    
  1112.               break;
    
  1113.             default:
    
  1114.               throw new Error('Unknown case.');
    
  1115.           }
    
  1116.         }
    
  1117.       }
    
  1118. 
    
  1119.       reportProblem({
    
  1120.         node: declaredDependenciesNode,
    
  1121.         message:
    
  1122.           `React Hook ${context.getSource(reactiveHook)} has ` +
    
  1123.           // To avoid a long message, show the next actionable item.
    
  1124.           (getWarningMessage(missingDependencies, 'a', 'missing', 'include') ||
    
  1125.             getWarningMessage(
    
  1126.               unnecessaryDependencies,
    
  1127.               'an',
    
  1128.               'unnecessary',
    
  1129.               'exclude',
    
  1130.             ) ||
    
  1131.             getWarningMessage(
    
  1132.               duplicateDependencies,
    
  1133.               'a',
    
  1134.               'duplicate',
    
  1135.               'omit',
    
  1136.             )) +
    
  1137.           extraWarning,
    
  1138.         suggest: [
    
  1139.           {
    
  1140.             desc: `Update the dependencies array to be: [${suggestedDeps
    
  1141.               .map(formatDependency)
    
  1142.               .join(', ')}]`,
    
  1143.             fix(fixer) {
    
  1144.               // TODO: consider preserving the comments or formatting?
    
  1145.               return fixer.replaceText(
    
  1146.                 declaredDependenciesNode,
    
  1147.                 `[${suggestedDeps.map(formatDependency).join(', ')}]`,
    
  1148.               );
    
  1149.             },
    
  1150.           },
    
  1151.         ],
    
  1152.       });
    
  1153.     }
    
  1154. 
    
  1155.     function visitCallExpression(node) {
    
  1156.       const callbackIndex = getReactiveHookCallbackIndex(node.callee, options);
    
  1157.       if (callbackIndex === -1) {
    
  1158.         // Not a React Hook call that needs deps.
    
  1159.         return;
    
  1160.       }
    
  1161.       const callback = node.arguments[callbackIndex];
    
  1162.       const reactiveHook = node.callee;
    
  1163.       const reactiveHookName = getNodeWithoutReactNamespace(reactiveHook).name;
    
  1164.       const maybeNode = node.arguments[callbackIndex + 1];
    
  1165.       const declaredDependenciesNode =
    
  1166.         maybeNode &&
    
  1167.         !(maybeNode.type === 'Identifier' && maybeNode.name === 'undefined')
    
  1168.           ? maybeNode
    
  1169.           : undefined;
    
  1170.       const isEffect = /Effect($|[^a-z])/g.test(reactiveHookName);
    
  1171. 
    
  1172.       // Check whether a callback is supplied. If there is no callback supplied
    
  1173.       // then the hook will not work and React will throw a TypeError.
    
  1174.       // So no need to check for dependency inclusion.
    
  1175.       if (!callback) {
    
  1176.         reportProblem({
    
  1177.           node: reactiveHook,
    
  1178.           message:
    
  1179.             `React Hook ${reactiveHookName} requires an effect callback. ` +
    
  1180.             `Did you forget to pass a callback to the hook?`,
    
  1181.         });
    
  1182.         return;
    
  1183.       }
    
  1184. 
    
  1185.       // Check the declared dependencies for this reactive hook. If there is no
    
  1186.       // second argument then the reactive callback will re-run on every render.
    
  1187.       // So no need to check for dependency inclusion.
    
  1188.       if (!declaredDependenciesNode && !isEffect) {
    
  1189.         // These are only used for optimization.
    
  1190.         if (
    
  1191.           reactiveHookName === 'useMemo' ||
    
  1192.           reactiveHookName === 'useCallback'
    
  1193.         ) {
    
  1194.           // TODO: Can this have a suggestion?
    
  1195.           reportProblem({
    
  1196.             node: reactiveHook,
    
  1197.             message:
    
  1198.               `React Hook ${reactiveHookName} does nothing when called with ` +
    
  1199.               `only one argument. Did you forget to pass an array of ` +
    
  1200.               `dependencies?`,
    
  1201.           });
    
  1202.         }
    
  1203.         return;
    
  1204.       }
    
  1205. 
    
  1206.       switch (callback.type) {
    
  1207.         case 'FunctionExpression':
    
  1208.         case 'ArrowFunctionExpression':
    
  1209.           visitFunctionWithDependencies(
    
  1210.             callback,
    
  1211.             declaredDependenciesNode,
    
  1212.             reactiveHook,
    
  1213.             reactiveHookName,
    
  1214.             isEffect,
    
  1215.           );
    
  1216.           return; // Handled
    
  1217.         case 'Identifier':
    
  1218.           if (!declaredDependenciesNode) {
    
  1219.             // No deps, no problems.
    
  1220.             return; // Handled
    
  1221.           }
    
  1222.           // The function passed as a callback is not written inline.
    
  1223.           // But perhaps it's in the dependencies array?
    
  1224.           if (
    
  1225.             declaredDependenciesNode.elements &&
    
  1226.             declaredDependenciesNode.elements.some(
    
  1227.               el => el && el.type === 'Identifier' && el.name === callback.name,
    
  1228.             )
    
  1229.           ) {
    
  1230.             // If it's already in the list of deps, we don't care because
    
  1231.             // this is valid regardless.
    
  1232.             return; // Handled
    
  1233.           }
    
  1234.           // We'll do our best effort to find it, complain otherwise.
    
  1235.           const variable = context.getScope().set.get(callback.name);
    
  1236.           if (variable == null || variable.defs == null) {
    
  1237.             // If it's not in scope, we don't care.
    
  1238.             return; // Handled
    
  1239.           }
    
  1240.           // The function passed as a callback is not written inline.
    
  1241.           // But it's defined somewhere in the render scope.
    
  1242.           // We'll do our best effort to find and check it, complain otherwise.
    
  1243.           const def = variable.defs[0];
    
  1244.           if (!def || !def.node) {
    
  1245.             break; // Unhandled
    
  1246.           }
    
  1247.           if (def.type !== 'Variable' && def.type !== 'FunctionName') {
    
  1248.             // Parameter or an unusual pattern. Bail out.
    
  1249.             break; // Unhandled
    
  1250.           }
    
  1251.           switch (def.node.type) {
    
  1252.             case 'FunctionDeclaration':
    
  1253.               // useEffect(() => { ... }, []);
    
  1254.               visitFunctionWithDependencies(
    
  1255.                 def.node,
    
  1256.                 declaredDependenciesNode,
    
  1257.                 reactiveHook,
    
  1258.                 reactiveHookName,
    
  1259.                 isEffect,
    
  1260.               );
    
  1261.               return; // Handled
    
  1262.             case 'VariableDeclarator':
    
  1263.               const init = def.node.init;
    
  1264.               if (!init) {
    
  1265.                 break; // Unhandled
    
  1266.               }
    
  1267.               switch (init.type) {
    
  1268.                 // const effectBody = () => {...};
    
  1269.                 // useEffect(effectBody, []);
    
  1270.                 case 'ArrowFunctionExpression':
    
  1271.                 case 'FunctionExpression':
    
  1272.                   // We can inspect this function as if it were inline.
    
  1273.                   visitFunctionWithDependencies(
    
  1274.                     init,
    
  1275.                     declaredDependenciesNode,
    
  1276.                     reactiveHook,
    
  1277.                     reactiveHookName,
    
  1278.                     isEffect,
    
  1279.                   );
    
  1280.                   return; // Handled
    
  1281.               }
    
  1282.               break; // Unhandled
    
  1283.           }
    
  1284.           break; // Unhandled
    
  1285.         default:
    
  1286.           // useEffect(generateEffectBody(), []);
    
  1287.           reportProblem({
    
  1288.             node: reactiveHook,
    
  1289.             message:
    
  1290.               `React Hook ${reactiveHookName} received a function whose dependencies ` +
    
  1291.               `are unknown. Pass an inline function instead.`,
    
  1292.           });
    
  1293.           return; // Handled
    
  1294.       }
    
  1295. 
    
  1296.       // Something unusual. Fall back to suggesting to add the body itself as a dep.
    
  1297.       reportProblem({
    
  1298.         node: reactiveHook,
    
  1299.         message:
    
  1300.           `React Hook ${reactiveHookName} has a missing dependency: '${callback.name}'. ` +
    
  1301.           `Either include it or remove the dependency array.`,
    
  1302.         suggest: [
    
  1303.           {
    
  1304.             desc: `Update the dependencies array to be: [${callback.name}]`,
    
  1305.             fix(fixer) {
    
  1306.               return fixer.replaceText(
    
  1307.                 declaredDependenciesNode,
    
  1308.                 `[${callback.name}]`,
    
  1309.               );
    
  1310.             },
    
  1311.           },
    
  1312.         ],
    
  1313.       });
    
  1314.     }
    
  1315. 
    
  1316.     return {
    
  1317.       CallExpression: visitCallExpression,
    
  1318.     };
    
  1319.   },
    
  1320. };
    
  1321. 
    
  1322. // The meat of the logic.
    
  1323. function collectRecommendations({
    
  1324.   dependencies,
    
  1325.   declaredDependencies,
    
  1326.   stableDependencies,
    
  1327.   externalDependencies,
    
  1328.   isEffect,
    
  1329. }) {
    
  1330.   // Our primary data structure.
    
  1331.   // It is a logical representation of property chains:
    
  1332.   // `props` -> `props.foo` -> `props.foo.bar` -> `props.foo.bar.baz`
    
  1333.   //         -> `props.lol`
    
  1334.   //         -> `props.huh` -> `props.huh.okay`
    
  1335.   //         -> `props.wow`
    
  1336.   // We'll use it to mark nodes that are *used* by the programmer,
    
  1337.   // and the nodes that were *declared* as deps. Then we will
    
  1338.   // traverse it to learn which deps are missing or unnecessary.
    
  1339.   const depTree = createDepTree();
    
  1340.   function createDepTree() {
    
  1341.     return {
    
  1342.       isUsed: false, // True if used in code
    
  1343.       isSatisfiedRecursively: false, // True if specified in deps
    
  1344.       isSubtreeUsed: false, // True if something deeper is used by code
    
  1345.       children: new Map(), // Nodes for properties
    
  1346.     };
    
  1347.   }
    
  1348. 
    
  1349.   // Mark all required nodes first.
    
  1350.   // Imagine exclamation marks next to each used deep property.
    
  1351.   dependencies.forEach((_, key) => {
    
  1352.     const node = getOrCreateNodeByPath(depTree, key);
    
  1353.     node.isUsed = true;
    
  1354.     markAllParentsByPath(depTree, key, parent => {
    
  1355.       parent.isSubtreeUsed = true;
    
  1356.     });
    
  1357.   });
    
  1358. 
    
  1359.   // Mark all satisfied nodes.
    
  1360.   // Imagine checkmarks next to each declared dependency.
    
  1361.   declaredDependencies.forEach(({key}) => {
    
  1362.     const node = getOrCreateNodeByPath(depTree, key);
    
  1363.     node.isSatisfiedRecursively = true;
    
  1364.   });
    
  1365.   stableDependencies.forEach(key => {
    
  1366.     const node = getOrCreateNodeByPath(depTree, key);
    
  1367.     node.isSatisfiedRecursively = true;
    
  1368.   });
    
  1369. 
    
  1370.   // Tree manipulation helpers.
    
  1371.   function getOrCreateNodeByPath(rootNode, path) {
    
  1372.     const keys = path.split('.');
    
  1373.     let node = rootNode;
    
  1374.     for (const key of keys) {
    
  1375.       let child = node.children.get(key);
    
  1376.       if (!child) {
    
  1377.         child = createDepTree();
    
  1378.         node.children.set(key, child);
    
  1379.       }
    
  1380.       node = child;
    
  1381.     }
    
  1382.     return node;
    
  1383.   }
    
  1384.   function markAllParentsByPath(rootNode, path, fn) {
    
  1385.     const keys = path.split('.');
    
  1386.     let node = rootNode;
    
  1387.     for (const key of keys) {
    
  1388.       const child = node.children.get(key);
    
  1389.       if (!child) {
    
  1390.         return;
    
  1391.       }
    
  1392.       fn(child);
    
  1393.       node = child;
    
  1394.     }
    
  1395.   }
    
  1396. 
    
  1397.   // Now we can learn which dependencies are missing or necessary.
    
  1398.   const missingDependencies = new Set();
    
  1399.   const satisfyingDependencies = new Set();
    
  1400.   scanTreeRecursively(
    
  1401.     depTree,
    
  1402.     missingDependencies,
    
  1403.     satisfyingDependencies,
    
  1404.     key => key,
    
  1405.   );
    
  1406.   function scanTreeRecursively(node, missingPaths, satisfyingPaths, keyToPath) {
    
  1407.     node.children.forEach((child, key) => {
    
  1408.       const path = keyToPath(key);
    
  1409.       if (child.isSatisfiedRecursively) {
    
  1410.         if (child.isSubtreeUsed) {
    
  1411.           // Remember this dep actually satisfied something.
    
  1412.           satisfyingPaths.add(path);
    
  1413.         }
    
  1414.         // It doesn't matter if there's something deeper.
    
  1415.         // It would be transitively satisfied since we assume immutability.
    
  1416.         // `props.foo` is enough if you read `props.foo.id`.
    
  1417.         return;
    
  1418.       }
    
  1419.       if (child.isUsed) {
    
  1420.         // Remember that no declared deps satisfied this node.
    
  1421.         missingPaths.add(path);
    
  1422.         // If we got here, nothing in its subtree was satisfied.
    
  1423.         // No need to search further.
    
  1424.         return;
    
  1425.       }
    
  1426.       scanTreeRecursively(
    
  1427.         child,
    
  1428.         missingPaths,
    
  1429.         satisfyingPaths,
    
  1430.         childKey => path + '.' + childKey,
    
  1431.       );
    
  1432.     });
    
  1433.   }
    
  1434. 
    
  1435.   // Collect suggestions in the order they were originally specified.
    
  1436.   const suggestedDependencies = [];
    
  1437.   const unnecessaryDependencies = new Set();
    
  1438.   const duplicateDependencies = new Set();
    
  1439.   declaredDependencies.forEach(({key}) => {
    
  1440.     // Does this declared dep satisfy a real need?
    
  1441.     if (satisfyingDependencies.has(key)) {
    
  1442.       if (suggestedDependencies.indexOf(key) === -1) {
    
  1443.         // Good one.
    
  1444.         suggestedDependencies.push(key);
    
  1445.       } else {
    
  1446.         // Duplicate.
    
  1447.         duplicateDependencies.add(key);
    
  1448.       }
    
  1449.     } else {
    
  1450.       if (
    
  1451.         isEffect &&
    
  1452.         !key.endsWith('.current') &&
    
  1453.         !externalDependencies.has(key)
    
  1454.       ) {
    
  1455.         // Effects are allowed extra "unnecessary" deps.
    
  1456.         // Such as resetting scroll when ID changes.
    
  1457.         // Consider them legit.
    
  1458.         // The exception is ref.current which is always wrong.
    
  1459.         if (suggestedDependencies.indexOf(key) === -1) {
    
  1460.           suggestedDependencies.push(key);
    
  1461.         }
    
  1462.       } else {
    
  1463.         // It's definitely not needed.
    
  1464.         unnecessaryDependencies.add(key);
    
  1465.       }
    
  1466.     }
    
  1467.   });
    
  1468. 
    
  1469.   // Then add the missing ones at the end.
    
  1470.   missingDependencies.forEach(key => {
    
  1471.     suggestedDependencies.push(key);
    
  1472.   });
    
  1473. 
    
  1474.   return {
    
  1475.     suggestedDependencies,
    
  1476.     unnecessaryDependencies,
    
  1477.     duplicateDependencies,
    
  1478.     missingDependencies,
    
  1479.   };
    
  1480. }
    
  1481. 
    
  1482. // If the node will result in constructing a referentially unique value, return
    
  1483. // its human readable type name, else return null.
    
  1484. function getConstructionExpressionType(node) {
    
  1485.   switch (node.type) {
    
  1486.     case 'ObjectExpression':
    
  1487.       return 'object';
    
  1488.     case 'ArrayExpression':
    
  1489.       return 'array';
    
  1490.     case 'ArrowFunctionExpression':
    
  1491.     case 'FunctionExpression':
    
  1492.       return 'function';
    
  1493.     case 'ClassExpression':
    
  1494.       return 'class';
    
  1495.     case 'ConditionalExpression':
    
  1496.       if (
    
  1497.         getConstructionExpressionType(node.consequent) != null ||
    
  1498.         getConstructionExpressionType(node.alternate) != null
    
  1499.       ) {
    
  1500.         return 'conditional';
    
  1501.       }
    
  1502.       return null;
    
  1503.     case 'LogicalExpression':
    
  1504.       if (
    
  1505.         getConstructionExpressionType(node.left) != null ||
    
  1506.         getConstructionExpressionType(node.right) != null
    
  1507.       ) {
    
  1508.         return 'logical expression';
    
  1509.       }
    
  1510.       return null;
    
  1511.     case 'JSXFragment':
    
  1512.       return 'JSX fragment';
    
  1513.     case 'JSXElement':
    
  1514.       return 'JSX element';
    
  1515.     case 'AssignmentExpression':
    
  1516.       if (getConstructionExpressionType(node.right) != null) {
    
  1517.         return 'assignment expression';
    
  1518.       }
    
  1519.       return null;
    
  1520.     case 'NewExpression':
    
  1521.       return 'object construction';
    
  1522.     case 'Literal':
    
  1523.       if (node.value instanceof RegExp) {
    
  1524.         return 'regular expression';
    
  1525.       }
    
  1526.       return null;
    
  1527.     case 'TypeCastExpression':
    
  1528.     case 'AsExpression':
    
  1529.     case 'TSAsExpression':
    
  1530.       return getConstructionExpressionType(node.expression);
    
  1531.   }
    
  1532.   return null;
    
  1533. }
    
  1534. 
    
  1535. // Finds variables declared as dependencies
    
  1536. // that would invalidate on every render.
    
  1537. function scanForConstructions({
    
  1538.   declaredDependencies,
    
  1539.   declaredDependenciesNode,
    
  1540.   componentScope,
    
  1541.   scope,
    
  1542. }) {
    
  1543.   const constructions = declaredDependencies
    
  1544.     .map(({key}) => {
    
  1545.       const ref = componentScope.variables.find(v => v.name === key);
    
  1546.       if (ref == null) {
    
  1547.         return null;
    
  1548.       }
    
  1549. 
    
  1550.       const node = ref.defs[0];
    
  1551.       if (node == null) {
    
  1552.         return null;
    
  1553.       }
    
  1554.       // const handleChange = function () {}
    
  1555.       // const handleChange = () => {}
    
  1556.       // const foo = {}
    
  1557.       // const foo = []
    
  1558.       // etc.
    
  1559.       if (
    
  1560.         node.type === 'Variable' &&
    
  1561.         node.node.type === 'VariableDeclarator' &&
    
  1562.         node.node.id.type === 'Identifier' && // Ensure this is not destructed assignment
    
  1563.         node.node.init != null
    
  1564.       ) {
    
  1565.         const constantExpressionType = getConstructionExpressionType(
    
  1566.           node.node.init,
    
  1567.         );
    
  1568.         if (constantExpressionType != null) {
    
  1569.           return [ref, constantExpressionType];
    
  1570.         }
    
  1571.       }
    
  1572.       // function handleChange() {}
    
  1573.       if (
    
  1574.         node.type === 'FunctionName' &&
    
  1575.         node.node.type === 'FunctionDeclaration'
    
  1576.       ) {
    
  1577.         return [ref, 'function'];
    
  1578.       }
    
  1579. 
    
  1580.       // class Foo {}
    
  1581.       if (node.type === 'ClassName' && node.node.type === 'ClassDeclaration') {
    
  1582.         return [ref, 'class'];
    
  1583.       }
    
  1584.       return null;
    
  1585.     })
    
  1586.     .filter(Boolean);
    
  1587. 
    
  1588.   function isUsedOutsideOfHook(ref) {
    
  1589.     let foundWriteExpr = false;
    
  1590.     for (let i = 0; i < ref.references.length; i++) {
    
  1591.       const reference = ref.references[i];
    
  1592.       if (reference.writeExpr) {
    
  1593.         if (foundWriteExpr) {
    
  1594.           // Two writes to the same function.
    
  1595.           return true;
    
  1596.         } else {
    
  1597.           // Ignore first write as it's not usage.
    
  1598.           foundWriteExpr = true;
    
  1599.           continue;
    
  1600.         }
    
  1601.       }
    
  1602.       let currentScope = reference.from;
    
  1603.       while (currentScope !== scope && currentScope != null) {
    
  1604.         currentScope = currentScope.upper;
    
  1605.       }
    
  1606.       if (currentScope !== scope) {
    
  1607.         // This reference is outside the Hook callback.
    
  1608.         // It can only be legit if it's the deps array.
    
  1609.         if (!isAncestorNodeOf(declaredDependenciesNode, reference.identifier)) {
    
  1610.           return true;
    
  1611.         }
    
  1612.       }
    
  1613.     }
    
  1614.     return false;
    
  1615.   }
    
  1616. 
    
  1617.   return constructions.map(([ref, depType]) => ({
    
  1618.     construction: ref.defs[0],
    
  1619.     depType,
    
  1620.     isUsedOutsideOfHook: isUsedOutsideOfHook(ref),
    
  1621.   }));
    
  1622. }
    
  1623. 
    
  1624. /**
    
  1625.  * Assuming () means the passed/returned node:
    
  1626.  * (props) => (props)
    
  1627.  * props.(foo) => (props.foo)
    
  1628.  * props.foo.(bar) => (props).foo.bar
    
  1629.  * props.foo.bar.(baz) => (props).foo.bar.baz
    
  1630.  */
    
  1631. function getDependency(node) {
    
  1632.   if (
    
  1633.     (node.parent.type === 'MemberExpression' ||
    
  1634.       node.parent.type === 'OptionalMemberExpression') &&
    
  1635.     node.parent.object === node &&
    
  1636.     node.parent.property.name !== 'current' &&
    
  1637.     !node.parent.computed &&
    
  1638.     !(
    
  1639.       node.parent.parent != null &&
    
  1640.       (node.parent.parent.type === 'CallExpression' ||
    
  1641.         node.parent.parent.type === 'OptionalCallExpression') &&
    
  1642.       node.parent.parent.callee === node.parent
    
  1643.     )
    
  1644.   ) {
    
  1645.     return getDependency(node.parent);
    
  1646.   } else if (
    
  1647.     // Note: we don't check OptionalMemberExpression because it can't be LHS.
    
  1648.     node.type === 'MemberExpression' &&
    
  1649.     node.parent &&
    
  1650.     node.parent.type === 'AssignmentExpression' &&
    
  1651.     node.parent.left === node
    
  1652.   ) {
    
  1653.     return node.object;
    
  1654.   } else {
    
  1655.     return node;
    
  1656.   }
    
  1657. }
    
  1658. 
    
  1659. /**
    
  1660.  * Mark a node as either optional or required.
    
  1661.  * Note: If the node argument is an OptionalMemberExpression, it doesn't necessarily mean it is optional.
    
  1662.  * It just means there is an optional member somewhere inside.
    
  1663.  * This particular node might still represent a required member, so check .optional field.
    
  1664.  */
    
  1665. function markNode(node, optionalChains, result) {
    
  1666.   if (optionalChains) {
    
  1667.     if (node.optional) {
    
  1668.       // We only want to consider it optional if *all* usages were optional.
    
  1669.       if (!optionalChains.has(result)) {
    
  1670.         // Mark as (maybe) optional. If there's a required usage, this will be overridden.
    
  1671.         optionalChains.set(result, true);
    
  1672.       }
    
  1673.     } else {
    
  1674.       // Mark as required.
    
  1675.       optionalChains.set(result, false);
    
  1676.     }
    
  1677.   }
    
  1678. }
    
  1679. 
    
  1680. /**
    
  1681.  * Assuming () means the passed node.
    
  1682.  * (foo) -> 'foo'
    
  1683.  * foo(.)bar -> 'foo.bar'
    
  1684.  * foo.bar(.)baz -> 'foo.bar.baz'
    
  1685.  * Otherwise throw.
    
  1686.  */
    
  1687. function analyzePropertyChain(node, optionalChains) {
    
  1688.   if (node.type === 'Identifier' || node.type === 'JSXIdentifier') {
    
  1689.     const result = node.name;
    
  1690.     if (optionalChains) {
    
  1691.       // Mark as required.
    
  1692.       optionalChains.set(result, false);
    
  1693.     }
    
  1694.     return result;
    
  1695.   } else if (node.type === 'MemberExpression' && !node.computed) {
    
  1696.     const object = analyzePropertyChain(node.object, optionalChains);
    
  1697.     const property = analyzePropertyChain(node.property, null);
    
  1698.     const result = `${object}.${property}`;
    
  1699.     markNode(node, optionalChains, result);
    
  1700.     return result;
    
  1701.   } else if (node.type === 'OptionalMemberExpression' && !node.computed) {
    
  1702.     const object = analyzePropertyChain(node.object, optionalChains);
    
  1703.     const property = analyzePropertyChain(node.property, null);
    
  1704.     const result = `${object}.${property}`;
    
  1705.     markNode(node, optionalChains, result);
    
  1706.     return result;
    
  1707.   } else if (node.type === 'ChainExpression' && !node.computed) {
    
  1708.     const expression = node.expression;
    
  1709. 
    
  1710.     if (expression.type === 'CallExpression') {
    
  1711.       throw new Error(`Unsupported node type: ${expression.type}`);
    
  1712.     }
    
  1713. 
    
  1714.     const object = analyzePropertyChain(expression.object, optionalChains);
    
  1715.     const property = analyzePropertyChain(expression.property, null);
    
  1716.     const result = `${object}.${property}`;
    
  1717.     markNode(expression, optionalChains, result);
    
  1718.     return result;
    
  1719.   } else {
    
  1720.     throw new Error(`Unsupported node type: ${node.type}`);
    
  1721.   }
    
  1722. }
    
  1723. 
    
  1724. function getNodeWithoutReactNamespace(node, options) {
    
  1725.   if (
    
  1726.     node.type === 'MemberExpression' &&
    
  1727.     node.object.type === 'Identifier' &&
    
  1728.     node.object.name === 'React' &&
    
  1729.     node.property.type === 'Identifier' &&
    
  1730.     !node.computed
    
  1731.   ) {
    
  1732.     return node.property;
    
  1733.   }
    
  1734.   return node;
    
  1735. }
    
  1736. 
    
  1737. // What's the index of callback that needs to be analyzed for a given Hook?
    
  1738. // -1 if it's not a Hook we care about (e.g. useState).
    
  1739. // 0 for useEffect/useMemo/useCallback(fn).
    
  1740. // 1 for useImperativeHandle(ref, fn).
    
  1741. // For additionally configured Hooks, assume that they're like useEffect (0).
    
  1742. function getReactiveHookCallbackIndex(calleeNode, options) {
    
  1743.   const node = getNodeWithoutReactNamespace(calleeNode);
    
  1744.   if (node.type !== 'Identifier') {
    
  1745.     return -1;
    
  1746.   }
    
  1747.   switch (node.name) {
    
  1748.     case 'useEffect':
    
  1749.     case 'useLayoutEffect':
    
  1750.     case 'useCallback':
    
  1751.     case 'useMemo':
    
  1752.       // useEffect(fn)
    
  1753.       return 0;
    
  1754.     case 'useImperativeHandle':
    
  1755.       // useImperativeHandle(ref, fn)
    
  1756.       return 1;
    
  1757.     default:
    
  1758.       if (node === calleeNode && options && options.additionalHooks) {
    
  1759.         // Allow the user to provide a regular expression which enables the lint to
    
  1760.         // target custom reactive hooks.
    
  1761.         let name;
    
  1762.         try {
    
  1763.           name = analyzePropertyChain(node, null);
    
  1764.         } catch (error) {
    
  1765.           if (/Unsupported node type/.test(error.message)) {
    
  1766.             return 0;
    
  1767.           } else {
    
  1768.             throw error;
    
  1769.           }
    
  1770.         }
    
  1771.         return options.additionalHooks.test(name) ? 0 : -1;
    
  1772.       } else {
    
  1773.         return -1;
    
  1774.       }
    
  1775.   }
    
  1776. }
    
  1777. 
    
  1778. /**
    
  1779.  * ESLint won't assign node.parent to references from context.getScope()
    
  1780.  *
    
  1781.  * So instead we search for the node from an ancestor assigning node.parent
    
  1782.  * as we go. This mutates the AST.
    
  1783.  *
    
  1784.  * This traversal is:
    
  1785.  * - optimized by only searching nodes with a range surrounding our target node
    
  1786.  * - agnostic to AST node types, it looks for `{ type: string, ... }`
    
  1787.  */
    
  1788. function fastFindReferenceWithParent(start, target) {
    
  1789.   const queue = [start];
    
  1790.   let item = null;
    
  1791. 
    
  1792.   while (queue.length) {
    
  1793.     item = queue.shift();
    
  1794. 
    
  1795.     if (isSameIdentifier(item, target)) {
    
  1796.       return item;
    
  1797.     }
    
  1798. 
    
  1799.     if (!isAncestorNodeOf(item, target)) {
    
  1800.       continue;
    
  1801.     }
    
  1802. 
    
  1803.     for (const [key, value] of Object.entries(item)) {
    
  1804.       if (key === 'parent') {
    
  1805.         continue;
    
  1806.       }
    
  1807.       if (isNodeLike(value)) {
    
  1808.         value.parent = item;
    
  1809.         queue.push(value);
    
  1810.       } else if (Array.isArray(value)) {
    
  1811.         value.forEach(val => {
    
  1812.           if (isNodeLike(val)) {
    
  1813.             val.parent = item;
    
  1814.             queue.push(val);
    
  1815.           }
    
  1816.         });
    
  1817.       }
    
  1818.     }
    
  1819.   }
    
  1820. 
    
  1821.   return null;
    
  1822. }
    
  1823. 
    
  1824. function joinEnglish(arr) {
    
  1825.   let s = '';
    
  1826.   for (let i = 0; i < arr.length; i++) {
    
  1827.     s += arr[i];
    
  1828.     if (i === 0 && arr.length === 2) {
    
  1829.       s += ' and ';
    
  1830.     } else if (i === arr.length - 2 && arr.length > 2) {
    
  1831.       s += ', and ';
    
  1832.     } else if (i < arr.length - 1) {
    
  1833.       s += ', ';
    
  1834.     }
    
  1835.   }
    
  1836.   return s;
    
  1837. }
    
  1838. 
    
  1839. function isNodeLike(val) {
    
  1840.   return (
    
  1841.     typeof val === 'object' &&
    
  1842.     val !== null &&
    
  1843.     !Array.isArray(val) &&
    
  1844.     typeof val.type === 'string'
    
  1845.   );
    
  1846. }
    
  1847. 
    
  1848. function isSameIdentifier(a, b) {
    
  1849.   return (
    
  1850.     (a.type === 'Identifier' || a.type === 'JSXIdentifier') &&
    
  1851.     a.type === b.type &&
    
  1852.     a.name === b.name &&
    
  1853.     a.range[0] === b.range[0] &&
    
  1854.     a.range[1] === b.range[1]
    
  1855.   );
    
  1856. }
    
  1857. 
    
  1858. function isAncestorNodeOf(a, b) {
    
  1859.   return a.range[0] <= b.range[0] && a.range[1] >= b.range[1];
    
  1860. }
    
  1861. 
    
  1862. function isUseEffectEventIdentifier(node) {
    
  1863.   if (__EXPERIMENTAL__) {
    
  1864.     return node.type === 'Identifier' && node.name === 'useEffectEvent';
    
  1865.   }
    
  1866.   return false;
    
  1867. }