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.