packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts TYPESCRIPT 935 lines View on github.com → Search inside
1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 */7/* eslint-disable no-for-of-loops/no-for-of-loops */89import type {Rule, Scope} from 'eslint';10import type {11  CallExpression,12  CatchClause,13  DoWhileStatement,14  Expression,15  Identifier,16  Node,17  Super,18  TryStatement,19} from 'estree';2021// @ts-expect-error untyped module22import CodePathAnalyzer from '../code-path-analysis/code-path-analyzer';23import {getAdditionalEffectHooksFromSettings} from '../shared/Utils';2425/**26 * Catch all identifiers that begin with "use" followed by an uppercase Latin27 * character to exclude identifiers like "user".28 */29function isHookName(s: string): boolean {30  return s === 'use' || /^use[A-Z0-9]/.test(s);31}3233/**34 * We consider hooks to be a hook name identifier or a member expression35 * containing a hook name.36 */37function isHook(node: Node): boolean {38  if (node.type === 'Identifier') {39    return isHookName(node.name);40  } else if (41    node.type === 'MemberExpression' &&42    !node.computed &&43    isHook(node.property)44  ) {45    const obj = node.object;46    const isPascalCaseNameSpace = /^[A-Z].*/;47    return obj.type === 'Identifier' && isPascalCaseNameSpace.test(obj.name);48  } else {49    return false;50  }51}5253/**54 * Checks if the node is a React component name. React component names must55 * always start with an uppercase letter.56 */57function isComponentName(node: Node): boolean {58  return node.type === 'Identifier' && /^[A-Z]/.test(node.name);59}6061function isReactFunction(node: Node, functionName: string): boolean {62  return (63    ('name' in node && node.name === functionName) ||64    (node.type === 'MemberExpression' &&65      'name' in node.object &&66      node.object.name === 'React' &&67      'name' in node.property &&68      node.property.name === functionName)69  );70}7172/**73 * Checks if the node is a callback argument of forwardRef. This render function74 * should follow the rules of hooks.75 */76function isForwardRefCallback(node: Node): boolean {77  return !!(78    node.parent &&79    'callee' in node.parent &&80    node.parent.callee &&81    isReactFunction(node.parent.callee, 'forwardRef')82  );83}8485/**86 * Checks if the node is a callback argument of React.memo. This anonymous87 * functional component should follow the rules of hooks.88 */89function isMemoCallback(node: Node): boolean {90  return !!(91    node.parent &&92    'callee' in node.parent &&93    node.parent.callee &&94    isReactFunction(node.parent.callee, 'memo')95  );96}9798function isInsideComponentOrHook(node: Node | undefined): boolean {99  while (node) {100    const functionName = getFunctionName(node);101    if (functionName) {102      if (isComponentName(functionName) || isHook(functionName)) {103        return true;104      }105    }106    if (isForwardRefCallback(node) || isMemoCallback(node)) {107      return true;108    }109    node = node.parent;110  }111  return false;112}113114function isInsideDoWhileLoop(node: Node | undefined): node is DoWhileStatement {115  while (node) {116    if (node.type === 'DoWhileStatement') {117      return true;118    }119    node = node.parent;120  }121  return false;122}123124function isInsideTryCatch(125  node: Node | undefined,126): node is TryStatement | CatchClause {127  while (node) {128    if (node.type === 'TryStatement' || node.type === 'CatchClause') {129      return true;130    }131    node = node.parent;132  }133  return false;134}135136function getNodeWithoutReactNamespace(137  node: Expression | Super,138): Expression | Identifier | Super {139  if (140    node.type === 'MemberExpression' &&141    node.object.type === 'Identifier' &&142    node.object.name === 'React' &&143    node.property.type === 'Identifier' &&144    !node.computed145  ) {146    return node.property;147  }148  return node;149}150151function isEffectIdentifier(node: Node, additionalHooks?: RegExp): boolean {152  const isBuiltInEffect =153    node.type === 'Identifier' &&154    (node.name === 'useEffect' ||155      node.name === 'useLayoutEffect' ||156      node.name === 'useInsertionEffect');157158  if (isBuiltInEffect) {159    return true;160  }161162  // Check if this matches additional hooks configured by the user163  if (additionalHooks && node.type === 'Identifier') {164    return additionalHooks.test(node.name);165  }166167  return false;168}169170function isUseEffectEventIdentifier(node: Node): boolean {171  return node.type === 'Identifier' && node.name === 'useEffectEvent';172}173174function useEffectEventError(fn: string | null, called: boolean): string {175  // no function identifier, i.e. it is not assigned to a variable176  if (fn === null) {177    return (178      `React Hook "useEffectEvent" can only be called at the top level of your component.` +179      ` It cannot be passed down.`180    );181  }182183  return (184    `\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` +185    'Effects and Effect Events in the same component.' +186    (called ? '' : ' It cannot be assigned to a variable or passed down.')187  );188}189190function isUseIdentifier(node: Node): boolean {191  return isReactFunction(node, 'use');192}193194const rule = {195  meta: {196    type: 'problem',197    docs: {198      description: 'enforces the Rules of Hooks',199      recommended: true,200      url: 'https://react.dev/reference/rules/rules-of-hooks',201    },202    schema: [203      {204        type: 'object',205        additionalProperties: false,206        properties: {207          additionalHooks: {208            type: 'string',209          },210        },211      },212    ],213  },214  create(context: Rule.RuleContext) {215    const settings = context.settings || {};216217    const additionalEffectHooks =218      getAdditionalEffectHooksFromSettings(settings);219220    let lastEffect: CallExpression | null = null;221    const codePathReactHooksMapStack: Array<222      Map<Rule.CodePathSegment, Array<Node>>223    > = [];224    const codePathSegmentStack: Array<Rule.CodePathSegment> = [];225    const useEffectEventFunctions = new WeakSet();226227    // For a given scope, iterate through the references and add all useEffectEvent definitions. We can228    // do this in non-Program nodes because we can rely on the assumption that useEffectEvent functions229    // can only be declared within a component or hook at its top level.230    function recordAllUseEffectEventFunctions(scope: Scope.Scope): void {231      for (const reference of scope.references) {232        const parent = reference.identifier.parent;233        if (234          parent?.type === 'VariableDeclarator' &&235          parent.init &&236          parent.init.type === 'CallExpression' &&237          parent.init.callee &&238          isUseEffectEventIdentifier(parent.init.callee)239        ) {240          if (reference.resolved === null) {241            throw new Error('Unexpected null reference.resolved');242          }243          for (const ref of reference.resolved.references) {244            if (ref !== reference) {245              useEffectEventFunctions.add(ref.identifier);246            }247          }248        }249      }250    }251252    /**253     * SourceCode that also works down to ESLint 3.0.0254     */255    const getSourceCode =256      typeof context.getSourceCode === 'function'257        ? () => {258            return context.getSourceCode();259          }260        : () => {261            return context.sourceCode;262          };263    /**264     * SourceCode#getScope that also works down to ESLint 3.0.0265     */266    const getScope =267      typeof context.getScope === 'function'268        ? (): Scope.Scope => {269            return context.getScope();270          }271        : (node: Node): Scope.Scope => {272            return getSourceCode().getScope(node);273          };274275    function hasFlowSuppression(node: Node, suppression: string) {276      const sourceCode = getSourceCode();277      const comments = sourceCode.getAllComments();278      const flowSuppressionRegex = new RegExp(279        '\\$FlowFixMe\\[' + suppression + '\\]',280      );281      return comments.some(282        commentNode =>283          flowSuppressionRegex.test(commentNode.value) &&284          commentNode.loc != null &&285          node.loc != null &&286          commentNode.loc.end.line === node.loc.start.line - 1,287      );288    }289290    const analyzer = new CodePathAnalyzer({291      // Maintain code segment path stack as we traverse.292      onCodePathSegmentStart: (segment: Rule.CodePathSegment) =>293        codePathSegmentStack.push(segment),294      onCodePathSegmentEnd: () => codePathSegmentStack.pop(),295296      // Maintain code path stack as we traverse.297      onCodePathStart: () =>298        codePathReactHooksMapStack.push(299          new Map<Rule.CodePathSegment, Array<Node>>(),300        ),301302      // Process our code path.303      //304      // Everything is ok if all React Hooks are both reachable from the initial305      // segment and reachable from every final segment.306      onCodePathEnd(codePath: any, codePathNode: Node) {307        const reactHooksMap = codePathReactHooksMapStack.pop();308        if (reactHooksMap?.size === 0) {309          return;310        } else if (typeof reactHooksMap === 'undefined') {311          throw new Error('Unexpected undefined reactHooksMap');312        }313314        // All of the segments which are cyclic are recorded in this set.315        const cyclic = new Set();316317        /**318         * Count the number of code paths from the start of the function to this319         * segment. For example:320         *321         * ```js322         * function MyComponent() {323         *   if (condition) {324         *     // Segment 1325         *   } else {326         *     // Segment 2327         *   }328         *   // Segment 3329         * }330         * ```331         *332         * Segments 1 and 2 have one path to the beginning of `MyComponent` and333         * segment 3 has two paths to the beginning of `MyComponent` since we334         * could have either taken the path of segment 1 or segment 2.335         *336         * Populates `cyclic` with cyclic segments.337         */338        function countPathsFromStart(339          segment: Rule.CodePathSegment,340          pathHistory?: Set<string>,341        ): bigint {342          const {cache} = countPathsFromStart;343          let paths = cache.get(segment.id);344          const pathList = new Set<string>(pathHistory);345346          // If `pathList` includes the current segment then we've found a cycle!347          // We need to fill `cyclic` with all segments inside cycle348          if (pathList.has(segment.id)) {349            const pathArray = [...pathList];350            const cyclicSegments = pathArray.slice(351              pathArray.indexOf(segment.id) + 1,352            );353            for (const cyclicSegment of cyclicSegments) {354              cyclic.add(cyclicSegment);355            }356357            return BigInt('0');358          }359360          // add the current segment to pathList361          pathList.add(segment.id);362363          // We have a cached `paths`. Return it.364          if (paths !== undefined) {365            return paths;366          }367368          if (codePath.thrownSegments.includes(segment)) {369            paths = BigInt('0');370          } else if (segment.prevSegments.length === 0) {371            paths = BigInt('1');372          } else {373            paths = BigInt('0');374            for (const prevSegment of segment.prevSegments) {375              paths += countPathsFromStart(prevSegment, pathList);376            }377          }378379          // If our segment is reachable then there should be at least one path380          // to it from the start of our code path.381          if (segment.reachable && paths === BigInt('0')) {382            cache.delete(segment.id);383          } else {384            cache.set(segment.id, paths);385          }386387          return paths;388        }389390        /**391         * Count the number of code paths from this segment to the end of the392         * function. For example:393         *394         * ```js395         * function MyComponent() {396         *   // Segment 1397         *   if (condition) {398         *     // Segment 2399         *   } else {400         *     // Segment 3401         *   }402         * }403         * ```404         *405         * Segments 2 and 3 have one path to the end of `MyComponent` and406         * segment 1 has two paths to the end of `MyComponent` since we could407         * either take the path of segment 1 or segment 2.408         *409         * Populates `cyclic` with cyclic segments.410         */411412        function countPathsToEnd(413          segment: Rule.CodePathSegment,414          pathHistory?: Set<string>,415        ): bigint {416          const {cache} = countPathsToEnd;417          let paths = cache.get(segment.id);418          const pathList = new Set(pathHistory);419420          // If `pathList` includes the current segment then we've found a cycle!421          // We need to fill `cyclic` with all segments inside cycle422          if (pathList.has(segment.id)) {423            const pathArray = Array.from(pathList);424            const cyclicSegments = pathArray.slice(425              pathArray.indexOf(segment.id) + 1,426            );427            for (const cyclicSegment of cyclicSegments) {428              cyclic.add(cyclicSegment);429            }430431            return BigInt('0');432          }433434          // add the current segment to pathList435          pathList.add(segment.id);436437          // We have a cached `paths`. Return it.438          if (paths !== undefined) {439            return paths;440          }441442          if (codePath.thrownSegments.includes(segment)) {443            paths = BigInt('0');444          } else if (segment.nextSegments.length === 0) {445            paths = BigInt('1');446          } else {447            paths = BigInt('0');448            for (const nextSegment of segment.nextSegments) {449              paths += countPathsToEnd(nextSegment, pathList);450            }451          }452453          cache.set(segment.id, paths);454          return paths;455        }456457        /**458         * Gets the shortest path length to the start of a code path.459         * For example:460         *461         * ```js462         * function MyComponent() {463         *   if (condition) {464         *     // Segment 1465         *   }466         *   // Segment 2467         * }468         * ```469         *470         * There is only one path from segment 1 to the code path start. Its471         * length is one so that is the shortest path.472         *473         * There are two paths from segment 2 to the code path start. One474         * through segment 1 with a length of two and another directly to the475         * start with a length of one. The shortest path has a length of one476         * so we would return that.477         */478479        function shortestPathLengthToStart(480          segment: Rule.CodePathSegment,481        ): number {482          const {cache} = shortestPathLengthToStart;483          let length = cache.get(segment.id);484485          // If `length` is null then we found a cycle! Return infinity since486          // the shortest path is definitely not the one where we looped.487          if (length === null) {488            return Infinity;489          }490491          // We have a cached `length`. Return it.492          if (length !== undefined) {493            return length;494          }495496          // Compute `length` and cache it. Guarding against cycles.497          cache.set(segment.id, null);498          if (segment.prevSegments.length === 0) {499            length = 1;500          } else {501            length = Infinity;502            for (const prevSegment of segment.prevSegments) {503              const prevLength = shortestPathLengthToStart(prevSegment);504              if (prevLength < length) {505                length = prevLength;506              }507            }508            length += 1;509          }510          cache.set(segment.id, length);511          return length;512        }513514        countPathsFromStart.cache = new Map<string, bigint>();515        countPathsToEnd.cache = new Map<string, bigint>();516        shortestPathLengthToStart.cache = new Map<string, number | null>();517518        // Count all code paths to the end of our component/hook. Also primes519        // the `countPathsToEnd` cache.520        const allPathsFromStartToEnd = countPathsToEnd(codePath.initialSegment);521522        // Gets the function name for our code path. If the function name is523        // `undefined` then we know either that we have an anonymous function524        // expression or our code path is not in a function. In both cases we525        // will want to error since neither are React function components or526        // hook functions - unless it is an anonymous function argument to527        // forwardRef or memo.528        const codePathFunctionName = getFunctionName(codePathNode);529530        // This is a valid code path for React hooks if we are directly in a React531        // function component or we are in a hook function.532        const isSomewhereInsideComponentOrHook =533          isInsideComponentOrHook(codePathNode);534        const isDirectlyInsideComponentOrHook = codePathFunctionName535          ? isComponentName(codePathFunctionName) ||536            isHook(codePathFunctionName)537          : isForwardRefCallback(codePathNode) || isMemoCallback(codePathNode);538539        // Compute the earliest finalizer level using information from the540        // cache. We expect all reachable final segments to have a cache entry541        // after calling `visitSegment()`.542        let shortestFinalPathLength = Infinity;543        for (const finalSegment of codePath.finalSegments) {544          if (!finalSegment.reachable) {545            continue;546          }547          const length = shortestPathLengthToStart(finalSegment);548          if (length < shortestFinalPathLength) {549            shortestFinalPathLength = length;550          }551        }552553        // Make sure all React Hooks pass our lint invariants. Log warnings554        // if not.555        for (const [segment, reactHooks] of reactHooksMap) {556          // NOTE: We could report here that the hook is not reachable, but557          // that would be redundant with more general "no unreachable"558          // lint rules.559          if (!segment.reachable) {560            continue;561          }562563          // If there are any final segments with a shorter path to start then564          // we possibly have an early return.565          //566          // If our segment is a final segment itself then siblings could567          // possibly be early returns.568          const possiblyHasEarlyReturn =569            segment.nextSegments.length === 0570              ? shortestFinalPathLength <= shortestPathLengthToStart(segment)571              : shortestFinalPathLength < shortestPathLengthToStart(segment);572573          // Count all the paths from the start of our code path to the end of574          // our code path that go _through_ this segment. The critical piece575          // of this is _through_. If we just call `countPathsToEnd(segment)`576          // then we neglect that we may have gone through multiple paths to get577          // to this point! Consider:578          //579          // ```js580          // function MyComponent() {581          //   if (a) {582          //     // Segment 1583          //   } else {584          //     // Segment 2585          //   }586          //   // Segment 3587          //   if (b) {588          //     // Segment 4589          //   } else {590          //     // Segment 5591          //   }592          // }593          // ```594          //595          // In this component we have four code paths:596          //597          // 1. `a = true; b = true`598          // 2. `a = true; b = false`599          // 3. `a = false; b = true`600          // 4. `a = false; b = false`601          //602          // From segment 3 there are two code paths to the end through segment603          // 4 and segment 5. However, we took two paths to get here through604          // segment 1 and segment 2.605          //606          // If we multiply the paths from start (two) by the paths to end (two)607          // for segment 3 we get four. Which is our desired count.608          const pathsFromStartToEnd =609            countPathsFromStart(segment) * countPathsToEnd(segment);610611          // Is this hook a part of a cyclic segment?612          const cycled = cyclic.has(segment.id);613614          for (const hook of reactHooks) {615            // Skip reporting if this hook already has a relevant flow suppression.616            if (hasFlowSuppression(hook, 'react-rule-hook')) {617              continue;618            }619620            // Report an error if use() is called inside try/catch.621            if (isUseIdentifier(hook) && isInsideTryCatch(hook)) {622              context.report({623                node: hook,624                message: `React Hook "${getSourceCode().getText(625                  hook,626                )}" cannot be called in a try/catch block.`,627              });628            }629630            // Report an error if a hook may be called more then once.631            // `use(...)` can be called in loops.632            if (633              (cycled || isInsideDoWhileLoop(hook)) &&634              !isUseIdentifier(hook)635            ) {636              context.report({637                node: hook,638                message:639                  `React Hook "${getSourceCode().getText(640                    hook,641                  )}" may be executed ` +642                  'more than once. Possibly because it is called in a loop. ' +643                  'React Hooks must be called in the exact same order in ' +644                  'every component render.',645              });646            }647648            // If this is not a valid code path for React hooks then we need to649            // log a warning for every hook in this code path.650            //651            // Pick a special message depending on the scope this hook was652            // called in.653            if (isDirectlyInsideComponentOrHook) {654              // Report an error if the hook is called inside an async function.655              // @ts-expect-error the above check hasn't properly type-narrowed `codePathNode` (async doesn't exist on Node)656              const isAsyncFunction = codePathNode.async;657              if (isAsyncFunction) {658                context.report({659                  node: hook,660                  message:661                    `React Hook "${getSourceCode().getText(hook)}" cannot be ` +662                    'called in an async function.',663                });664              }665666              // Report an error if a hook does not reach all finalizing code667              // path segments.668              //669              // Special case when we think there might be an early return.670              if (671                !cycled &&672                pathsFromStartToEnd !== allPathsFromStartToEnd &&673                !isUseIdentifier(hook) && // `use(...)` can be called conditionally.674                !isInsideDoWhileLoop(hook) // wrapping do/while loops are checked separately.675              ) {676                const message =677                  `React Hook "${getSourceCode().getText(hook)}" is called ` +678                  'conditionally. React Hooks must be called in the exact ' +679                  'same order in every component render.' +680                  (possiblyHasEarlyReturn681                    ? ' Did you accidentally call a React Hook after an' +682                      ' early return?'683                    : '');684                context.report({node: hook, message});685              }686            } else if (687              codePathNode.parent != null &&688              (codePathNode.parent.type === 'MethodDefinition' ||689                // @ts-expect-error `ClassProperty` was removed from typescript-estree in https://github.com/typescript-eslint/typescript-eslint/pull/3806690                codePathNode.parent.type === 'ClassProperty' ||691                codePathNode.parent.type === 'PropertyDefinition') &&692              codePathNode.parent.value === codePathNode693            ) {694              // Custom message for hooks inside a class695              const message =696                `React Hook "${getSourceCode().getText(697                  hook,698                )}" cannot be called ` +699                'in a class component. React Hooks must be called in a ' +700                'React function component or a custom React Hook function.';701              context.report({node: hook, message});702            } else if (codePathFunctionName) {703              // Custom message if we found an invalid function name.704              const message =705                `React Hook "${getSourceCode().getText(hook)}" is called in ` +706                `function "${getSourceCode().getText(codePathFunctionName)}" ` +707                'that is neither a React function component nor a custom ' +708                'React Hook function.' +709                ' React component names must start with an uppercase letter.' +710                ' React Hook names must start with the word "use".';711              context.report({node: hook, message});712            } else if (codePathNode.type === 'Program') {713              // These are dangerous if you have inline requires enabled.714              const message =715                `React Hook "${getSourceCode().getText(716                  hook,717                )}" cannot be called ` +718                'at the top level. React Hooks must be called in a ' +719                'React function component or a custom React Hook function.';720              context.report({node: hook, message});721            } else {722              // Assume in all other cases the user called a hook in some723              // random function callback. This should usually be true for724              // anonymous function expressions. Hopefully this is clarifying725              // enough in the common case that the incorrect message in726              // uncommon cases doesn't matter.727              // `use(...)` can be called in callbacks.728              if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) {729                const message =730                  `React Hook "${getSourceCode().getText(731                    hook,732                  )}" cannot be called ` +733                  'inside a callback. React Hooks must be called in a ' +734                  'React function component or a custom React Hook function.';735                context.report({node: hook, message});736              }737            }738          }739        }740      },741    });742743    return {744      '*'(node: any) {745        analyzer.enterNode(node);746      },747748      '*:exit'(node: any) {749        analyzer.leaveNode(node);750      },751752      // Missed opportunity...We could visit all `Identifier`s instead of all753      // `CallExpression`s and check that _every use_ of a hook name is valid.754      // But that gets complicated and enters type-system territory, so we're755      // only being strict about hook calls for now.756      CallExpression(node) {757        if (isHook(node.callee)) {758          // Add the hook node to a map keyed by the code path segment. We will759          // do full code path analysis at the end of our code path.760          const reactHooksMap = last(codePathReactHooksMapStack);761          const codePathSegment = last(codePathSegmentStack);762          let reactHooks = reactHooksMap.get(codePathSegment);763          if (!reactHooks) {764            reactHooks = [];765            reactHooksMap.set(codePathSegment, reactHooks);766          }767          reactHooks.push(node.callee);768        }769770        // useEffectEvent: useEffectEvent functions can be passed by reference within useEffect as well as in771        // another useEffectEvent772        // Check all `useEffect` and `React.useEffect`, `useEffectEvent`, and `React.useEffectEvent`773        const nodeWithoutNamespace = getNodeWithoutReactNamespace(node.callee);774        if (775          (isEffectIdentifier(nodeWithoutNamespace, additionalEffectHooks) ||776            isUseEffectEventIdentifier(nodeWithoutNamespace)) &&777          node.arguments.length > 0778        ) {779          // Denote that we have traversed into a useEffect call, and stash the CallExpr for780          // comparison later when we exit781          lastEffect = node;782        }783784        // Specifically disallow <Child onClick={useEffectEvent(...)} /> because this785        // case can't be caught by `recordAllUseEffectEventFunctions` as it isn't assigned to a variable786        if (787          isUseEffectEventIdentifier(nodeWithoutNamespace) &&788          node.parent?.type !== 'VariableDeclarator' &&789          // like in other hooks, calling useEffectEvent at component's top level without assignment is valid790          node.parent?.type !== 'ExpressionStatement'791        ) {792          const message = useEffectEventError(null, false);793794          context.report({795            node,796            message,797          });798        }799      },800801      Identifier(node) {802        // This identifier resolves to a useEffectEvent function, but isn't being referenced in an803        // effect or another event function. It isn't being called either.804        if (lastEffect == null && useEffectEventFunctions.has(node)) {805          const message = useEffectEventError(806            getSourceCode().getText(node),807            node.parent.type === 'CallExpression',808          );809810          context.report({811            node,812            message,813          });814        }815      },816817      'CallExpression:exit'(node) {818        if (node === lastEffect) {819          lastEffect = null;820        }821      },822823      FunctionDeclaration(node) {824        // function MyComponent() { const onClick = useEffectEvent(...) }825        if (isInsideComponentOrHook(node)) {826          recordAllUseEffectEventFunctions(getScope(node));827        }828      },829830      ArrowFunctionExpression(node) {831        // const MyComponent = () => { const onClick = useEffectEvent(...) }832        if (isInsideComponentOrHook(node)) {833          recordAllUseEffectEventFunctions(getScope(node));834        }835      },836837      // @ts-expect-error parser-hermes produces these node types838      ComponentDeclaration(node) {839        // component MyComponent() { const onClick = useEffectEvent(...) }840        recordAllUseEffectEventFunctions(getScope(node));841      },842843      // @ts-expect-error parser-hermes produces these node types844      HookDeclaration(node) {845        // hook useMyHook() { const onClick = useEffectEvent(...) }846        recordAllUseEffectEventFunctions(getScope(node));847      },848    };849  },850} satisfies Rule.RuleModule;851852/**853 * Gets the static name of a function AST node. For function declarations it is854 * easy. For anonymous function expressions it is much harder. If you search for855 * `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places856 * where JS gives anonymous function expressions names. We roughly detect the857 * same AST nodes with some exceptions to better fit our use case.858 */859860function getFunctionName(node: Node) {861  if (862    // @ts-expect-error parser-hermes produces these node types863    node.type === 'ComponentDeclaration' ||864    // @ts-expect-error parser-hermes produces these node types865    node.type === 'HookDeclaration' ||866    node.type === 'FunctionDeclaration' ||867    (node.type === 'FunctionExpression' && node.id)868  ) {869    // function useHook() {}870    // const whatever = function useHook() {};871    //872    // Function declaration or function expression names win over any873    // assignment statements or other renames.874    return node.id;875  } else if (876    node.type === 'FunctionExpression' ||877    node.type === 'ArrowFunctionExpression'878  ) {879    if (880      node.parent?.type === 'VariableDeclarator' &&881      node.parent.init === node882    ) {883      // const useHook = () => {};884      return node.parent.id;885    } else if (886      node.parent?.type === 'AssignmentExpression' &&887      node.parent.right === node &&888      node.parent.operator === '='889    ) {890      // useHook = () => {};891      return node.parent.left;892    } else if (893      node.parent?.type === 'Property' &&894      node.parent.value === node &&895      !node.parent.computed896    ) {897      // {useHook: () => {}}898      // {useHook() {}}899      return node.parent.key;900901      // NOTE: We could also support `ClassProperty` and `MethodDefinition`902      // here to be pedantic. However, hooks in a class are an anti-pattern. So903      // we don't allow it to error early.904      //905      // class {useHook = () => {}}906      // class {useHook() {}}907    } else if (908      node.parent?.type === 'AssignmentPattern' &&909      node.parent.right === node &&910      // @ts-expect-error Property computed does not exist on type `AssignmentPattern`.911      !node.parent.computed912    ) {913      // const {useHook = () => {}} = {};914      // ({useHook = () => {}} = {});915      //916      // Kinda clowny, but we'd said we'd follow spec convention for917      // `IsAnonymousFunctionDefinition()` usage.918      return node.parent.left;919    } else {920      return undefined;921    }922  } else {923    return undefined;924  }925}926927/**928 * Convenience function for peeking the last item in a stack.929 */930function last<T>(array: Array<T>): T {931  return array[array.length - 1] as T;932}933934export default rule;

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.