UNPKG

eslint-plugin-react-hooks

Version:

ESLint rules for React Hooks

1,026 lines (1,015 loc) • 135 kB
/** * @license React * eslint-plugin-react-hooks.development.js * * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; if (process.env.NODE_ENV !== "production") { (function() { 'use strict'; /*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise */ function __values(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); } function __read(o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; } function __spreadArray(to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || from); } /* eslint-disable no-for-of-loops/no-for-of-loops */ /** * Catch all identifiers that begin with "use" followed by an uppercase Latin * character to exclude identifiers like "user". */ function isHookName(s) { return s === 'use' || /^use[A-Z0-9]/.test(s); } /** * We consider hooks to be a hook name identifier or a member expression * containing a hook name. */ function isHook(node) { if (node.type === 'Identifier') { return isHookName(node.name); } else if (node.type === 'MemberExpression' && !node.computed && isHook(node.property)) { var obj = node.object; var isPascalCaseNameSpace = /^[A-Z].*/; return obj.type === 'Identifier' && isPascalCaseNameSpace.test(obj.name); } else { return false; } } /** * Checks if the node is a React component name. React component names must * always start with an uppercase letter. */ function isComponentName(node) { return node.type === 'Identifier' && /^[A-Z]/.test(node.name); } function isReactFunction(node, functionName) { return (('name' in node && node.name === functionName) || (node.type === 'MemberExpression' && 'name' in node.object && node.object.name === 'React' && 'name' in node.property && node.property.name === functionName)); } /** * Checks if the node is a callback argument of forwardRef. This render function * should follow the rules of hooks. */ function isForwardRefCallback(node) { return !!(node.parent && 'callee' in node.parent && node.parent.callee && isReactFunction(node.parent.callee, 'forwardRef')); } /** * Checks if the node is a callback argument of React.memo. This anonymous * functional component should follow the rules of hooks. */ function isMemoCallback(node) { return !!(node.parent && 'callee' in node.parent && node.parent.callee && isReactFunction(node.parent.callee, 'memo')); } function isInsideComponentOrHook(node) { while (node) { var functionName = getFunctionName(node); if (functionName) { if (isComponentName(functionName) || isHook(functionName)) { return true; } } if (isForwardRefCallback(node) || isMemoCallback(node)) { return true; } node = node.parent; } return false; } function isInsideDoWhileLoop(node) { while (node) { if (node.type === 'DoWhileStatement') { return true; } node = node.parent; } return false; } function isUseEffectEventIdentifier$1(node) { return false; } function isUseIdentifier(node) { return isReactFunction(node, 'use'); } var rule$1 = { meta: { type: 'problem', docs: { description: 'enforces the Rules of Hooks', recommended: true, url: 'https://reactjs.org/docs/hooks-rules.html', }, }, create: function (context) { var lastEffect = null; var codePathReactHooksMapStack = []; var codePathSegmentStack = []; var useEffectEventFunctions = new WeakSet(); // For a given scope, iterate through the references and add all useEffectEvent definitions. We can // do this in non-Program nodes because we can rely on the assumption that useEffectEvent functions // can only be declared within a component or hook at its top level. function recordAllUseEffectEventFunctions(scope) { var e_1, _a, e_2, _b; try { for (var _c = __values(scope.references), _d = _c.next(); !_d.done; _d = _c.next()) { var reference = _d.value; var parent = reference.identifier.parent; if ((parent === null || parent === void 0 ? void 0 : parent.type) === 'VariableDeclarator' && parent.init && parent.init.type === 'CallExpression' && parent.init.callee && isUseEffectEventIdentifier$1(parent.init.callee)) { if (reference.resolved === null) { throw new Error('Unexpected null reference.resolved'); } try { for (var _e = (e_2 = void 0, __values(reference.resolved.references)), _f = _e.next(); !_f.done; _f = _e.next()) { var ref = _f.value; if (ref !== reference) { useEffectEventFunctions.add(ref.identifier); } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_f && !_f.done && (_b = _e.return)) _b.call(_e); } finally { if (e_2) throw e_2.error; } } } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_d && !_d.done && (_a = _c.return)) _a.call(_c); } finally { if (e_1) throw e_1.error; } } } /** * SourceCode that also works down to ESLint 3.0.0 */ var getSourceCode = typeof context.getSourceCode === 'function' ? function () { return context.getSourceCode(); } : function () { return context.sourceCode; }; /** * SourceCode#getScope that also works down to ESLint 3.0.0 */ var getScope = typeof context.getScope === 'function' ? function () { return context.getScope(); } : function (node) { return getSourceCode().getScope(node); }; return { // Maintain code segment path stack as we traverse. onCodePathSegmentStart: function (segment) { return codePathSegmentStack.push(segment); }, onCodePathSegmentEnd: function () { return codePathSegmentStack.pop(); }, // Maintain code path stack as we traverse. onCodePathStart: function () { return codePathReactHooksMapStack.push(new Map()); }, // Process our code path. // // Everything is ok if all React Hooks are both reachable from the initial // segment and reachable from every final segment. onCodePathEnd: function (codePath, codePathNode) { var e_3, _a, e_4, _b, e_5, _c; var reactHooksMap = codePathReactHooksMapStack.pop(); if ((reactHooksMap === null || reactHooksMap === void 0 ? void 0 : reactHooksMap.size) === 0) { return; } else if (typeof reactHooksMap === 'undefined') { throw new Error('Unexpected undefined reactHooksMap'); } // All of the segments which are cyclic are recorded in this set. var cyclic = new Set(); /** * Count the number of code paths from the start of the function to this * segment. For example: * * ```js * function MyComponent() { * if (condition) { * // Segment 1 * } else { * // Segment 2 * } * // Segment 3 * } * ``` * * Segments 1 and 2 have one path to the beginning of `MyComponent` and * segment 3 has two paths to the beginning of `MyComponent` since we * could have either taken the path of segment 1 or segment 2. * * Populates `cyclic` with cyclic segments. */ function countPathsFromStart(segment, pathHistory) { var e_6, _a, e_7, _b; var cache = countPathsFromStart.cache; var paths = cache.get(segment.id); var pathList = new Set(pathHistory); // If `pathList` includes the current segment then we've found a cycle! // We need to fill `cyclic` with all segments inside cycle if (pathList.has(segment.id)) { var pathArray = __spreadArray([], __read(pathList), false); var cyclicSegments = pathArray.slice(pathArray.indexOf(segment.id) + 1); try { for (var cyclicSegments_1 = __values(cyclicSegments), cyclicSegments_1_1 = cyclicSegments_1.next(); !cyclicSegments_1_1.done; cyclicSegments_1_1 = cyclicSegments_1.next()) { var cyclicSegment = cyclicSegments_1_1.value; cyclic.add(cyclicSegment); } } catch (e_6_1) { e_6 = { error: e_6_1 }; } finally { try { if (cyclicSegments_1_1 && !cyclicSegments_1_1.done && (_a = cyclicSegments_1.return)) _a.call(cyclicSegments_1); } finally { if (e_6) throw e_6.error; } } return BigInt('0'); } // add the current segment to pathList pathList.add(segment.id); // We have a cached `paths`. Return it. if (paths !== undefined) { return paths; } if (codePath.thrownSegments.includes(segment)) { paths = BigInt('0'); } else if (segment.prevSegments.length === 0) { paths = BigInt('1'); } else { paths = BigInt('0'); try { for (var _c = __values(segment.prevSegments), _d = _c.next(); !_d.done; _d = _c.next()) { var prevSegment = _d.value; paths += countPathsFromStart(prevSegment, pathList); } } catch (e_7_1) { e_7 = { error: e_7_1 }; } finally { try { if (_d && !_d.done && (_b = _c.return)) _b.call(_c); } finally { if (e_7) throw e_7.error; } } } // If our segment is reachable then there should be at least one path // to it from the start of our code path. if (segment.reachable && paths === BigInt('0')) { cache.delete(segment.id); } else { cache.set(segment.id, paths); } return paths; } /** * Count the number of code paths from this segment to the end of the * function. For example: * * ```js * function MyComponent() { * // Segment 1 * if (condition) { * // Segment 2 * } else { * // Segment 3 * } * } * ``` * * Segments 2 and 3 have one path to the end of `MyComponent` and * segment 1 has two paths to the end of `MyComponent` since we could * either take the path of segment 1 or segment 2. * * Populates `cyclic` with cyclic segments. */ function countPathsToEnd(segment, pathHistory) { var e_8, _a, e_9, _b; var cache = countPathsToEnd.cache; var paths = cache.get(segment.id); var pathList = new Set(pathHistory); // If `pathList` includes the current segment then we've found a cycle! // We need to fill `cyclic` with all segments inside cycle if (pathList.has(segment.id)) { var pathArray = Array.from(pathList); var cyclicSegments = pathArray.slice(pathArray.indexOf(segment.id) + 1); try { for (var cyclicSegments_2 = __values(cyclicSegments), cyclicSegments_2_1 = cyclicSegments_2.next(); !cyclicSegments_2_1.done; cyclicSegments_2_1 = cyclicSegments_2.next()) { var cyclicSegment = cyclicSegments_2_1.value; cyclic.add(cyclicSegment); } } catch (e_8_1) { e_8 = { error: e_8_1 }; } finally { try { if (cyclicSegments_2_1 && !cyclicSegments_2_1.done && (_a = cyclicSegments_2.return)) _a.call(cyclicSegments_2); } finally { if (e_8) throw e_8.error; } } return BigInt('0'); } // add the current segment to pathList pathList.add(segment.id); // We have a cached `paths`. Return it. if (paths !== undefined) { return paths; } if (codePath.thrownSegments.includes(segment)) { paths = BigInt('0'); } else if (segment.nextSegments.length === 0) { paths = BigInt('1'); } else { paths = BigInt('0'); try { for (var _c = __values(segment.nextSegments), _d = _c.next(); !_d.done; _d = _c.next()) { var nextSegment = _d.value; paths += countPathsToEnd(nextSegment, pathList); } } catch (e_9_1) { e_9 = { error: e_9_1 }; } finally { try { if (_d && !_d.done && (_b = _c.return)) _b.call(_c); } finally { if (e_9) throw e_9.error; } } } cache.set(segment.id, paths); return paths; } /** * Gets the shortest path length to the start of a code path. * For example: * * ```js * function MyComponent() { * if (condition) { * // Segment 1 * } * // Segment 2 * } * ``` * * There is only one path from segment 1 to the code path start. Its * length is one so that is the shortest path. * * There are two paths from segment 2 to the code path start. One * through segment 1 with a length of two and another directly to the * start with a length of one. The shortest path has a length of one * so we would return that. */ function shortestPathLengthToStart(segment) { var e_10, _a; var cache = shortestPathLengthToStart.cache; var length = cache.get(segment.id); // If `length` is null then we found a cycle! Return infinity since // the shortest path is definitely not the one where we looped. if (length === null) { return Infinity; } // We have a cached `length`. Return it. if (length !== undefined) { return length; } // Compute `length` and cache it. Guarding against cycles. cache.set(segment.id, null); if (segment.prevSegments.length === 0) { length = 1; } else { length = Infinity; try { for (var _b = __values(segment.prevSegments), _c = _b.next(); !_c.done; _c = _b.next()) { var prevSegment = _c.value; var prevLength = shortestPathLengthToStart(prevSegment); if (prevLength < length) { length = prevLength; } } } catch (e_10_1) { e_10 = { error: e_10_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_10) throw e_10.error; } } length += 1; } cache.set(segment.id, length); return length; } countPathsFromStart.cache = new Map(); countPathsToEnd.cache = new Map(); shortestPathLengthToStart.cache = new Map(); // Count all code paths to the end of our component/hook. Also primes // the `countPathsToEnd` cache. var allPathsFromStartToEnd = countPathsToEnd(codePath.initialSegment); // Gets the function name for our code path. If the function name is // `undefined` then we know either that we have an anonymous function // expression or our code path is not in a function. In both cases we // will want to error since neither are React function components or // hook functions - unless it is an anonymous function argument to // forwardRef or memo. var codePathFunctionName = getFunctionName(codePathNode); // This is a valid code path for React hooks if we are directly in a React // function component or we are in a hook function. var isSomewhereInsideComponentOrHook = isInsideComponentOrHook(codePathNode); var isDirectlyInsideComponentOrHook = codePathFunctionName ? isComponentName(codePathFunctionName) || isHook(codePathFunctionName) : isForwardRefCallback(codePathNode) || isMemoCallback(codePathNode); // Compute the earliest finalizer level using information from the // cache. We expect all reachable final segments to have a cache entry // after calling `visitSegment()`. var shortestFinalPathLength = Infinity; try { for (var _d = __values(codePath.finalSegments), _e = _d.next(); !_e.done; _e = _d.next()) { var finalSegment = _e.value; if (!finalSegment.reachable) { continue; } var length = shortestPathLengthToStart(finalSegment); if (length < shortestFinalPathLength) { shortestFinalPathLength = length; } } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (_e && !_e.done && (_a = _d.return)) _a.call(_d); } finally { if (e_3) throw e_3.error; } } try { // Make sure all React Hooks pass our lint invariants. Log warnings // if not. for (var reactHooksMap_1 = __values(reactHooksMap), reactHooksMap_1_1 = reactHooksMap_1.next(); !reactHooksMap_1_1.done; reactHooksMap_1_1 = reactHooksMap_1.next()) { var _f = __read(reactHooksMap_1_1.value, 2), segment = _f[0], reactHooks = _f[1]; // NOTE: We could report here that the hook is not reachable, but // that would be redundant with more general "no unreachable" // lint rules. if (!segment.reachable) { continue; } // If there are any final segments with a shorter path to start then // we possibly have an early return. // // If our segment is a final segment itself then siblings could // possibly be early returns. var possiblyHasEarlyReturn = segment.nextSegments.length === 0 ? shortestFinalPathLength <= shortestPathLengthToStart(segment) : shortestFinalPathLength < shortestPathLengthToStart(segment); // Count all the paths from the start of our code path to the end of // our code path that go _through_ this segment. The critical piece // of this is _through_. If we just call `countPathsToEnd(segment)` // then we neglect that we may have gone through multiple paths to get // to this point! Consider: // // ```js // function MyComponent() { // if (a) { // // Segment 1 // } else { // // Segment 2 // } // // Segment 3 // if (b) { // // Segment 4 // } else { // // Segment 5 // } // } // ``` // // In this component we have four code paths: // // 1. `a = true; b = true` // 2. `a = true; b = false` // 3. `a = false; b = true` // 4. `a = false; b = false` // // From segment 3 there are two code paths to the end through segment // 4 and segment 5. However, we took two paths to get here through // segment 1 and segment 2. // // If we multiply the paths from start (two) by the paths to end (two) // for segment 3 we get four. Which is our desired count. var pathsFromStartToEnd = countPathsFromStart(segment) * countPathsToEnd(segment); // Is this hook a part of a cyclic segment? var cycled = cyclic.has(segment.id); try { for (var reactHooks_1 = (e_5 = void 0, __values(reactHooks)), reactHooks_1_1 = reactHooks_1.next(); !reactHooks_1_1.done; reactHooks_1_1 = reactHooks_1.next()) { var hook = reactHooks_1_1.value; // Report an error if a hook may be called more then once. // `use(...)` can be called in loops. if ((cycled || isInsideDoWhileLoop(hook)) && !isUseIdentifier(hook)) { context.report({ node: hook, message: "React Hook \"".concat(getSourceCode().getText(hook), "\" may be executed ") + 'more than once. Possibly because it is called in a loop. ' + 'React Hooks must be called in the exact same order in ' + 'every component render.', }); } // If this is not a valid code path for React hooks then we need to // log a warning for every hook in this code path. // // Pick a special message depending on the scope this hook was // called in. if (isDirectlyInsideComponentOrHook) { // Report an error if the hook is called inside an async function. // @ts-expect-error the above check hasn't properly type-narrowed `codePathNode` (async doesn't exist on Node) var isAsyncFunction = codePathNode.async; if (isAsyncFunction) { context.report({ node: hook, message: "React Hook \"".concat(getSourceCode().getText(hook), "\" cannot be ") + 'called in an async function.', }); } // Report an error if a hook does not reach all finalizing code // path segments. // // Special case when we think there might be an early return. if (!cycled && pathsFromStartToEnd !== allPathsFromStartToEnd && !isUseIdentifier(hook) && // `use(...)` can be called conditionally. !isInsideDoWhileLoop(hook) // wrapping do/while loops are checked separately. ) { var message = "React Hook \"".concat(getSourceCode().getText(hook), "\" is called ") + 'conditionally. React Hooks must be called in the exact ' + 'same order in every component render.' + (possiblyHasEarlyReturn ? ' Did you accidentally call a React Hook after an' + ' early return?' : ''); context.report({ node: hook, message: message }); } } else if (codePathNode.parent != null && (codePathNode.parent.type === 'MethodDefinition' || // @ts-expect-error `ClassProperty` was removed from typescript-estree in https://github.com/typescript-eslint/typescript-eslint/pull/3806 codePathNode.parent.type === 'ClassProperty' || codePathNode.parent.type === 'PropertyDefinition') && codePathNode.parent.value === codePathNode) { // Custom message for hooks inside a class var message = "React Hook \"".concat(getSourceCode().getText(hook), "\" cannot be called ") + 'in a class component. React Hooks must be called in a ' + 'React function component or a custom React Hook function.'; context.report({ node: hook, message: message }); } else if (codePathFunctionName) { // Custom message if we found an invalid function name. var message = "React Hook \"".concat(getSourceCode().getText(hook), "\" is called in ") + "function \"".concat(getSourceCode().getText(codePathFunctionName), "\" ") + 'that is neither a React function component nor a custom ' + 'React Hook function.' + ' React component names must start with an uppercase letter.' + ' React Hook names must start with the word "use".'; context.report({ node: hook, message: message }); } else if (codePathNode.type === 'Program') { // These are dangerous if you have inline requires enabled. var message = "React Hook \"".concat(getSourceCode().getText(hook), "\" cannot be called ") + 'at the top level. React Hooks must be called in a ' + 'React function component or a custom React Hook function.'; context.report({ node: hook, message: message }); } else { // Assume in all other cases the user called a hook in some // random function callback. This should usually be true for // anonymous function expressions. Hopefully this is clarifying // enough in the common case that the incorrect message in // uncommon cases doesn't matter. // `use(...)` can be called in callbacks. if (isSomewhereInsideComponentOrHook && !isUseIdentifier(hook)) { var message = "React Hook \"".concat(getSourceCode().getText(hook), "\" cannot be called ") + 'inside a callback. React Hooks must be called in a ' + 'React function component or a custom React Hook function.'; context.report({ node: hook, message: message }); } } } } catch (e_5_1) { e_5 = { error: e_5_1 }; } finally { try { if (reactHooks_1_1 && !reactHooks_1_1.done && (_c = reactHooks_1.return)) _c.call(reactHooks_1); } finally { if (e_5) throw e_5.error; } } } } catch (e_4_1) { e_4 = { error: e_4_1 }; } finally { try { if (reactHooksMap_1_1 && !reactHooksMap_1_1.done && (_b = reactHooksMap_1.return)) _b.call(reactHooksMap_1); } finally { if (e_4) throw e_4.error; } } }, // Missed opportunity...We could visit all `Identifier`s instead of all // `CallExpression`s and check that _every use_ of a hook name is valid. // But that gets complicated and enters type-system territory, so we're // only being strict about hook calls for now. CallExpression: function (node) { if (isHook(node.callee)) { // Add the hook node to a map keyed by the code path segment. We will // do full code path analysis at the end of our code path. var reactHooksMap = last(codePathReactHooksMapStack); var codePathSegment = last(codePathSegmentStack); var reactHooks = reactHooksMap.get(codePathSegment); if (!reactHooks) { reactHooks = []; reactHooksMap.set(codePathSegment, reactHooks); } reactHooks.push(node.callee); } // useEffectEvent: useEffectEvent functions can be passed by reference within useEffect as well as in // another useEffectEvent if (node.callee.type === 'Identifier' && (node.callee.name === 'useEffect' || isUseEffectEventIdentifier$1()) && node.arguments.length > 0) { // Denote that we have traversed into a useEffect call, and stash the CallExpr for // comparison later when we exit lastEffect = node; } }, Identifier: function (node) { // This identifier resolves to a useEffectEvent function, but isn't being referenced in an // effect or another event function. It isn't being called either. if (lastEffect == null && useEffectEventFunctions.has(node) && node.parent.type !== 'CallExpression') { context.report({ node: node, message: "`".concat(getSourceCode().getText(node), "` is a function created with React Hook \"useEffectEvent\", and can only be called from ") + 'the same component. They cannot be assigned to variables or passed down.', }); } }, 'CallExpression:exit': function (node) { if (node === lastEffect) { lastEffect = null; } }, FunctionDeclaration: function (node) { // function MyComponent() { const onClick = useEffectEvent(...) } if (isInsideComponentOrHook(node)) { recordAllUseEffectEventFunctions(getScope(node)); } }, ArrowFunctionExpression: function (node) { // const MyComponent = () => { const onClick = useEffectEvent(...) } if (isInsideComponentOrHook(node)) { recordAllUseEffectEventFunctions(getScope(node)); } }, }; }, }; /** * Gets the static name of a function AST node. For function declarations it is * easy. For anonymous function expressions it is much harder. If you search for * `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places * where JS gives anonymous function expressions names. We roughly detect the * same AST nodes with some exceptions to better fit our use case. */ function getFunctionName(node) { var _a, _b, _c, _d; if (node.type === 'FunctionDeclaration' || (node.type === 'FunctionExpression' && node.id)) { // function useHook() {} // const whatever = function useHook() {}; // // Function declaration or function expression names win over any // assignment statements or other renames. return node.id; } else if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') { if (((_a = node.parent) === null || _a === void 0 ? void 0 : _a.type) === 'VariableDeclarator' && node.parent.init === node) { // const useHook = () => {}; return node.parent.id; } else if (((_b = node.parent) === null || _b === void 0 ? void 0 : _b.type) === 'AssignmentExpression' && node.parent.right === node && node.parent.operator === '=') { // useHook = () => {}; return node.parent.left; } else if (((_c = node.parent) === null || _c === void 0 ? void 0 : _c.type) === 'Property' && node.parent.value === node && !node.parent.computed) { // {useHook: () => {}} // {useHook() {}} return node.parent.key; // NOTE: We could also support `ClassProperty` and `MethodDefinition` // here to be pedantic. However, hooks in a class are an anti-pattern. So // we don't allow it to error early. // // class {useHook = () => {}} // class {useHook() {}} } else if (((_d = node.parent) === null || _d === void 0 ? void 0 : _d.type) === 'AssignmentPattern' && node.parent.right === node && // @ts-expect-error Property computed does not exist on type `AssignmentPattern`. !node.parent.computed) { // const {useHook = () => {}} = {}; // ({useHook = () => {}} = {}); // // Kinda clowny, but we'd said we'd follow spec convention for // `IsAnonymousFunctionDefinition()` usage. return node.parent.left; } else { return undefined; } } else { return undefined; } } /** * Convenience function for peeking the last item in a stack. */ function last(array) { return array[array.length - 1]; } var rule = { meta: { type: 'suggestion', docs: { description: 'verifies the list of dependencies for Hooks like useEffect and similar', recommended: true, url: 'https://github.com/facebook/react/issues/14920', }, fixable: 'code', hasSuggestions: true, schema: [ { type: 'object', additionalProperties: false, enableDangerousAutofixThisMayCauseInfiniteLoops: false, properties: { additionalHooks: { type: 'string', }, enableDangerousAutofixThisMayCauseInfiniteLoops: { type: 'boolean', }, }, }, ], }, create: function (context) { // Parse the `additionalHooks` regex. var additionalHooks = context.options && context.options[0] && context.options[0].additionalHooks ? new RegExp(context.options[0].additionalHooks) : undefined; var enableDangerousAutofixThisMayCauseInfiniteLoops = (context.options && context.options[0] && context.options[0].enableDangerousAutofixThisMayCauseInfiniteLoops) || false; var options = { additionalHooks: additionalHooks, enableDangerousAutofixThisMayCauseInfiniteLoops: enableDangerousAutofixThisMayCauseInfiniteLoops, }; function reportProblem(problem) { if (enableDangerousAutofixThisMayCauseInfiniteLoops) { // Used to enable legacy behavior. Dangerous. // Keep this as an option until major IDEs upgrade (including VSCode FB ESLint extension). if (Array.isArray(problem.suggest) && problem.suggest.length > 0 && problem.suggest[0]) { problem.fix = problem.suggest[0].fix; } } context.report(problem); } /** * SourceCode that also works down to ESLint 3.0.0 */ var getSourceCode = typeof context.getSourceCode === 'function' ? function () { return context.getSourceCode(); } : function () { return context.sourceCode; }; /** * SourceCode#getScope that also works down to ESLint 3.0.0 */ var getScope = typeof context.getScope === 'function' ? function () { return context.getScope(); } : function (node) { return context.sourceCode.getScope(node); }; var scopeManager = getSourceCode().scopeManager; // Should be shared between visitors. var setStateCallSites = new WeakMap(); var stateVariables = new WeakSet(); var stableKnownValueCache = new WeakMap(); var functionWithoutCapturedValueCache = new WeakMap(); var useEffectEventVariables = new WeakSet(); function memoizeWithWeakMap(fn, map) { return function (arg) { if (map.has(arg)) { // to verify cache hits: // console.log(arg.name) return map.get(arg); } var result = fn(arg); map.set(arg, result); return result; }; } /** * Visitor for both function expressions and arrow function expressions. */ function visitFunctionWithDependencies(node, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect) { var e_1, _a, e_2, _b, e_3, _c; if (isEffect && node.async) { reportProblem({ node: node, message: "Effect callbacks are synchronous to prevent race conditions. " + "Put the async function inside:\n\n" + 'useEffect(() => {\n' + ' async function fetchData() {\n' + ' // You can await here\n' + ' const response = await MyAPI.getData(someId);\n' + ' // ...\n' + ' }\n' + ' fetchData();\n' + "}, [someId]); // Or [] if effect doesn't need props or state\n\n" + 'Learn more about data fetching with Hooks: https://react.dev/link/hooks-data-fetching', }); } // Get the current scope. var scope = scopeManager.acquire(node); if (!scope) { throw new Error('Unable to acquire scope for the current node. This is a bug in eslint-plugin-react-hooks, please file an issue.'); } // Find all our "pure scopes". On every re-render of a component these // pure scopes may have changes to the variables declared within. So all // variables used in our reactive hook callback but declared in a pure // scope need to be listed as dependencies of our reactive hook callback. // // According to the rules of React you can't read a mutable value in pure // scope. We can't enforce this in a lint so we trust that all variables // declared outside of pure scope are indeed frozen. var pureScopes = new Set(); var componentScope = null; { var currentScope = scope.upper; while (currentScope) { pureScopes.add(currentScope); if (currentScope.type === 'function') { break; } currentScope = currentScope.upper; } // If there is no parent function scope then there are no pure scopes. // The ones we've collected so far are incorrect. So don't continue with // the lint. if (!currentScope) { return; } componentScope = currentScope; } var isArray = Array.isArray; // Next we'll define a few helpers that helps us // tell if some values don't have to be declared as deps. // Some are known to be stable based on Hook calls. // const [state, setState] = useState() / React.useState() // ^^^ true for this reference // const [state, dispatch] = useReducer() / React.useReducer() // ^^^ true for this reference // const [state, dispatch] = useActionState() / React.useActionState() // ^^^ true for this reference // const ref = useRef() // ^^^ true for this reference // const onStuff = useEffectEvent(() => {}) // ^^^ true for this reference // False for everything else. function isStableKnownHookValue(resolved) { var e_4, _a, e_5, _b, e_6, _c; if (!isArray(resolved.defs)) { return false; } var def = resolved.defs[0]; if (def == null) { return false; } // Look for `let stuff = ...` var defNode = def.node; if (defNode.type !== 'VariableDeclarator') { return false; } var init = defNode.init; if (init == null) { return false; } while (init.type === 'TSAsExpression' || init.type === 'AsExpression') { init = init.expression; } // Detect primitive constants // const foo = 42 var declaration = defNode.parent; if (declaration == null && componentScope != null) { // This might happen if variable is declared after the callback. // In that case ESLint won't set up .parent refs. // So we'll set them up manually. fastFindReferenceWithParent(componentScope.block, def.node.id); declaration = def.node.parent; if (declaration == null) { return false; } } if (declaration != null && 'kind' in declarati