diff --git "a/node_modules/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js" "b/node_modules/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js" new file mode 100644--- /dev/null +++ "b/node_modules/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js" @@ -0,0 +1,2737 @@ +/** + * @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 declaration && + declaration.kind === 'const' && + init.type === 'Literal' && + (typeof init.value === 'string' || + typeof init.value === 'number' || + init.value === null)) { + // Definitely stable + return true; + } + // Detect known Hook calls + // const [_, setState] = useState() + if (init.type !== 'CallExpression') { + return false; + } + var callee = init.callee; + // Step into `= React.something` initializer. + if (callee.type === 'MemberExpression' && + 'name' in callee.object && + callee.object.name === 'React' && + callee.property != null && + !callee.computed) { + callee = callee.property; + } + if (callee.type !== 'Identifier') { + return false; + } + var definitionNode = def.node; + var id = definitionNode.id; + var name = callee.name; + if (name === 'useRef' && id.type === 'Identifier') { + // useRef() return value is stable. + return true; + } + else if (isUseEffectEventIdentifier() && + id.type === 'Identifier') { + try { + for (var _d = __values(resolved.references), _e = _d.next(); !_e.done; _e = _d.next()) { + var ref = _e.value; + // @ts-expect-error These types are not compatible (Reference and Identifier) + if (ref !== id) { + useEffectEventVariables.add(ref.identifier); + } + } + } + catch (e_4_1) { e_4 = { error: e_4_1 }; } + finally { + try { + if (_e && !_e.done && (_a = _d.return)) _a.call(_d); + } + finally { if (e_4) throw e_4.error; } + } + // useEffectEvent() return value is always unstable. + return true; + } + else if (name === 'useState' || + name === 'useReducer' || + name === 'useActionState') { + // Only consider second value in initializing tuple stable. + if (id.type === 'ArrayPattern' && + id.elements.length === 2 && + isArray(resolved.identifiers)) { + // Is second tuple value the same reference we're checking? + if (id.elements[1] === resolved.identifiers[0]) { + if (name === 'useState') { + var references = resolved.references; + var writeCount = 0; + try { + for (var references_2 = __values(references), references_2_1 = references_2.next(); !references_2_1.done; references_2_1 = references_2.next()) { + var reference = references_2_1.value; + if (reference.isWrite()) { + writeCount++; + } + if (writeCount > 1) { + return false; + } + setStateCallSites.set(reference.identifier, id.elements[0]); + } + } + catch (e_5_1) { e_5 = { error: e_5_1 }; } + finally { + try { + if (references_2_1 && !references_2_1.done && (_b = references_2.return)) _b.call(references_2); + } + finally { if (e_5) throw e_5.error; } + } + } + // Setter is stable. + return true; + } + else if (id.elements[0] === resolved.identifiers[0]) { + if (name === 'useState') { + var references = resolved.references; + try { + for (var references_3 = __values(references), references_3_1 = references_3.next(); !references_3_1.done; references_3_1 = references_3.next()) { + var reference = references_3_1.value; + stateVariables.add(reference.identifier); + } + } + catch (e_6_1) { e_6 = { error: e_6_1 }; } + finally { + try { + if (references_3_1 && !references_3_1.done && (_c = references_3.return)) _c.call(references_3); + } + finally { if (e_6) throw e_6.error; } + } + } + // State variable itself is dynamic. + return false; + } + } + } + else if (name === 'useTransition') { + // Only consider second value in initializing tuple stable. + if (id.type === 'ArrayPattern' && + id.elements.length === 2 && + Array.isArray(resolved.identifiers)) { + // Is second tuple value the same reference we're checking? + if (id.elements[1] === resolved.identifiers[0]) { + // Setter is stable. + return true; + } + } + } + // By default assume it's dynamic. + return false; + } + // Some are just functions that don't reference anything dynamic. + function isFunctionWithoutCapturedValues(resolved) { + var e_7, _a, e_8, _b; + if (!isArray(resolved.defs)) { + return false; + } + var def = resolved.defs[0]; + if (def == null) { + return false; + } + if (def.node == null || def.node.id == null) { + return false; + } + // Search the direct component subscopes for + // top-level function definitions matching this reference. + var fnNode = def.node; + var childScopes = (componentScope === null || componentScope === void 0 ? void 0 : componentScope.childScopes) || []; + var fnScope = null; + try { + for (var childScopes_1 = __values(childScopes), childScopes_1_1 = childScopes_1.next(); !childScopes_1_1.done; childScopes_1_1 = childScopes_1.next()) { + var childScope = childScopes_1_1.value; + var childScopeBlock = childScope.block; + if ( + // function handleChange() {} + (fnNode.type === 'FunctionDeclaration' && + childScopeBlock === fnNode) || + // const handleChange = () => {} + // const handleChange = function() {} + (fnNode.type === 'VariableDeclarator' && + childScopeBlock.parent === fnNode)) { + // Found it! + fnScope = childScope; + break; + } + } + } + catch (e_7_1) { e_7 = { error: e_7_1 }; } + finally { + try { + if (childScopes_1_1 && !childScopes_1_1.done && (_a = childScopes_1.return)) _a.call(childScopes_1); + } + finally { if (e_7) throw e_7.error; } + } + if (fnScope == null) { + return false; + } + try { + // Does this function capture any values + // that are in pure scopes (aka render)? + for (var _c = __values(fnScope.through), _d = _c.next(); !_d.done; _d = _c.next()) { + var ref = _d.value; + if (ref.resolved == null) { + continue; + } + if (pureScopes.has(ref.resolved.scope) && + // Stable values are fine though, + // although we won't check functions deeper. + !memoizedIsStableKnownHookValue(ref.resolved)) { + return false; + } + } + } + catch (e_8_1) { e_8 = { error: e_8_1 }; } + finally { + try { + if (_d && !_d.done && (_b = _c.return)) _b.call(_c); + } + finally { if (e_8) throw e_8.error; } + } + // If we got here, this function doesn't capture anything + // from render--or everything it captures is known stable. + return true; + } + // Remember such values. Avoid re-running extra checks on them. + var memoizedIsStableKnownHookValue = memoizeWithWeakMap(isStableKnownHookValue, stableKnownValueCache); + var memoizedIsFunctionWithoutCapturedValues = memoizeWithWeakMap(isFunctionWithoutCapturedValues, functionWithoutCapturedValueCache); + // These are usually mistaken. Collect them. + var currentRefsInEffectCleanup = new Map(); + // Is this reference inside a cleanup function for this effect node? + // We can check by traversing scopes upwards from the reference, and checking + // if the last "return () => " we encounter is located directly inside the effect. + function isInsideEffectCleanup(reference) { + var curScope = reference.from; + var isInReturnedFunction = false; + while (curScope != null && curScope.block !== node) { + if (curScope.type === 'function') { + isInReturnedFunction = + curScope.block.parent != null && + curScope.block.parent.type === 'ReturnStatement'; + } + curScope = curScope.upper; + } + return isInReturnedFunction; + } + // Get dependencies from all our resolved references in pure scopes. + // Key is dependency string, value is whether it's stable. + var dependencies = new Map(); + var optionalChains = new Map(); + gatherDependenciesRecursively(scope); + function gatherDependenciesRecursively(currentScope) { + var e_9, _a, e_10, _b; + var _c, _d, _e, _f, _g; + try { + for (var _h = __values(currentScope.references), _j = _h.next(); !_j.done; _j = _h.next()) { + var reference = _j.value; + // If this reference is not resolved or it is not declared in a pure + // scope then we don't care about this reference. + if (!reference.resolved) { + continue; + } + if (!pureScopes.has(reference.resolved.scope)) { + continue; + } + // Narrow the scope of a dependency if it is, say, a member expression. + // Then normalize the narrowed dependency. + var referenceNode = fastFindReferenceWithParent(node, reference.identifier); + if (referenceNode == null) { + continue; + } + var dependencyNode = getDependency(referenceNode); + var dependency = analyzePropertyChain(dependencyNode, optionalChains); + // Accessing ref.current inside effect cleanup is bad. + if ( + // We're in an effect... + isEffect && + // ... and this look like accessing .current... + dependencyNode.type === 'Identifier' && + (((_c = dependencyNode.parent) === null || _c === void 0 ? void 0 : _c.type) === 'MemberExpression' || + ((_d = dependencyNode.parent) === null || _d === void 0 ? void 0 : _d.type) === 'OptionalMemberExpression') && + !dependencyNode.parent.computed && + dependencyNode.parent.property.type === 'Identifier' && + dependencyNode.parent.property.name === 'current' && + // ...in a cleanup function or below... + isInsideEffectCleanup(reference)) { + currentRefsInEffectCleanup.set(dependency, { + reference: reference, + dependencyNode: dependencyNode, + }); + } + if (((_e = dependencyNode.parent) === null || _e === void 0 ? void 0 : _e.type) === 'TSTypeQuery' || + ((_f = dependencyNode.parent) === null || _f === void 0 ? void 0 : _f.type) === 'TSTypeReference') { + continue; + } + var def = reference.resolved.defs[0]; + if (def == null) { + continue; + } + // Ignore references to the function itself as it's not defined yet. + if (def.node != null && def.node.init === node.parent) { + continue; + } + // Ignore Flow type parameters + // @ts-expect-error We don't have flow types + if (def.type === 'TypeParameter') { + continue; + } + // Add the dependency to a map so we can make sure it is referenced + // again in our dependencies array. Remember whether it's stable. + if (!dependencies.has(dependency)) { + var resolved = reference.resolved; + var isStable = memoizedIsStableKnownHookValue(resolved) || + memoizedIsFunctionWithoutCapturedValues(resolved); + dependencies.set(dependency, { + isStable: isStable, + references: [reference], + }); + } + else { + (_g = dependencies.get(dependency)) === null || _g === void 0 ? void 0 : _g.references.push(reference); + } + } + } + catch (e_9_1) { e_9 = { error: e_9_1 }; } + finally { + try { + if (_j && !_j.done && (_a = _h.return)) _a.call(_h); + } + finally { if (e_9) throw e_9.error; } + } + try { + for (var _k = __values(currentScope.childScopes), _l = _k.next(); !_l.done; _l = _k.next()) { + var childScope = _l.value; + gatherDependenciesRecursively(childScope); + } + } + catch (e_10_1) { e_10 = { error: e_10_1 }; } + finally { + try { + if (_l && !_l.done && (_b = _k.return)) _b.call(_k); + } + finally { if (e_10) throw e_10.error; } + } + } + // Warn about accessing .current in cleanup effects. + currentRefsInEffectCleanup.forEach(function (_a, dependency) { + var e_11, _b; + var _c, _d; + var reference = _a.reference, dependencyNode = _a.dependencyNode; + var references = ((_c = reference.resolved) === null || _c === void 0 ? void 0 : _c.references) || []; + // Is React managing this ref or us? + // Let's see if we can find a .current assignment. + var foundCurrentAssignment = false; + try { + for (var references_4 = __values(references), references_4_1 = references_4.next(); !references_4_1.done; references_4_1 = references_4.next()) { + var ref = references_4_1.value; + var identifier = ref.identifier; + var parent = identifier.parent; + if (parent != null && + // ref.current + // Note: no need to handle OptionalMemberExpression because it can't be LHS. + parent.type === 'MemberExpression' && + !parent.computed && + parent.property.type === 'Identifier' && + parent.property.name === 'current' && + // ref.current = + ((_d = parent.parent) === null || _d === void 0 ? void 0 : _d.type) === 'AssignmentExpression' && + parent.parent.left === parent) { + foundCurrentAssignment = true; + break; + } + } + } + catch (e_11_1) { e_11 = { error: e_11_1 }; } + finally { + try { + if (references_4_1 && !references_4_1.done && (_b = references_4.return)) _b.call(references_4); + } + finally { if (e_11) throw e_11.error; } + } + // We only want to warn about React-managed refs. + if (foundCurrentAssignment) { + return; + } + reportProblem({ + // @ts-expect-error We can do better here (dependencyNode.parent has not been type narrowed) + node: dependencyNode.parent.property, + message: "The ref value '".concat(dependency, ".current' will likely have ") + + "changed by the time this effect cleanup function runs. If " + + "this ref points to a node rendered by React, copy " + + "'".concat(dependency, ".current' to a variable inside the effect, and ") + + "use that variable in the cleanup function.", + }); + }); + // Warn about assigning to variables in the outer scope. + // Those are usually bugs. + var staleAssignments = new Set(); + function reportStaleAssignment(writeExpr, key) { + if (staleAssignments.has(key)) { + return; + } + staleAssignments.add(key); + reportProblem({ + node: writeExpr, + message: "Assignments to the '".concat(key, "' variable from inside React Hook ") + + "".concat(getSourceCode().getText(reactiveHook), " will be lost after each ") + + "render. To preserve the value over time, store it in a useRef " + + "Hook and keep the mutable value in the '.current' property. " + + "Otherwise, you can move this variable directly inside " + + "".concat(getSourceCode().getText(reactiveHook), "."), + }); + } + // Remember which deps are stable and report bad usage first. + var stableDependencies = new Set(); + dependencies.forEach(function (_a, key) { + var isStable = _a.isStable, references = _a.references; + if (isStable) { + stableDependencies.add(key); + } + references.forEach(function (reference) { + if (reference.writeExpr) { + reportStaleAssignment(reference.writeExpr, key); + } + }); + }); + if (staleAssignments.size > 0) { + // The intent isn't clear so we'll wait until you fix those first. + return; + } + if (!declaredDependenciesNode) { + // Check if there are any top-level setState() calls. + // Those tend to lead to infinite loops. + var setStateInsideEffectWithoutDeps_1 = null; + dependencies.forEach(function (_a, key) { + var references = _a.references; + if (setStateInsideEffectWithoutDeps_1) { + return; + } + references.forEach(function (reference) { + if (setStateInsideEffectWithoutDeps_1) { + return; + } + var id = reference.identifier; + var isSetState = setStateCallSites.has(id); + if (!isSetState) { + return; + } + var fnScope = reference.from; + while (fnScope != null && fnScope.type !== 'function') { + fnScope = fnScope.upper; + } + var isDirectlyInsideEffect = (fnScope === null || fnScope === void 0 ? void 0 : fnScope.block) === node; + if (isDirectlyInsideEffect) { + // TODO: we could potentially ignore early returns. + setStateInsideEffectWithoutDeps_1 = key; + } + }); + }); + if (setStateInsideEffectWithoutDeps_1) { + var suggestedDependencies_1 = collectRecommendations({ + dependencies: dependencies, + declaredDependencies: [], + stableDependencies: stableDependencies, + externalDependencies: new Set(), + isEffect: true, + }).suggestedDependencies; + reportProblem({ + node: reactiveHook, + message: "React Hook ".concat(reactiveHookName, " contains a call to '").concat(setStateInsideEffectWithoutDeps_1, "'. ") + + "Without a list of dependencies, this can lead to an infinite chain of updates. " + + "To fix this, pass [" + + suggestedDependencies_1.join(', ') + + "] as a second argument to the ".concat(reactiveHookName, " Hook."), + suggest: [ + { + desc: "Add dependencies array: [".concat(suggestedDependencies_1.join(', '), "]"), + fix: function (fixer) { + return fixer.insertTextAfter(node, ", [".concat(suggestedDependencies_1.join(', '), "]")); + }, + }, + ], + }); + } + return; + } + var declaredDependencies = []; + var externalDependencies = new Set(); + var isArrayExpression = declaredDependenciesNode.type === 'ArrayExpression'; + var isTSAsArrayExpression = declaredDependenciesNode.type === 'TSAsExpression' && + declaredDependenciesNode.expression.type === 'ArrayExpression'; + if (!isArrayExpression && !isTSAsArrayExpression) { + // If the declared dependencies are not an array expression then we + // can't verify that the user provided the correct dependencies. Tell + // the user this in an error. + reportProblem({ + node: declaredDependenciesNode, + message: "React Hook ".concat(getSourceCode().getText(reactiveHook), " was passed a ") + + 'dependency list that is not an array literal. This means we ' + + "can't statically verify whether you've passed the correct " + + 'dependencies.', + }); + } + else { + var arrayExpression = isTSAsArrayExpression + ? declaredDependenciesNode.expression + : declaredDependenciesNode; + arrayExpression.elements.forEach(function (declaredDependencyNode) { + // Skip elided elements. + if (declaredDependencyNode === null) { + return; + } + // If we see a spread element then add a special warning. + if (declaredDependencyNode.type === 'SpreadElement') { + reportProblem({ + node: declaredDependencyNode, + message: "React Hook ".concat(getSourceCode().getText(reactiveHook), " has a spread ") + + "element in its dependency array. This means we can't " + + "statically verify whether you've passed the " + + 'correct dependencies.', + }); + return; + } + if (useEffectEventVariables.has(declaredDependencyNode)) { + reportProblem({ + node: declaredDependencyNode, + message: 'Functions returned from `useEffectEvent` must not be included in the dependency array. ' + + "Remove `".concat(getSourceCode().getText(declaredDependencyNode), "` from the list."), + suggest: [ + { + desc: "Remove the dependency `".concat(getSourceCode().getText(declaredDependencyNode), "`"), + fix: function (fixer) { + return fixer.removeRange(declaredDependencyNode.range); + }, + }, + ], + }); + } + // Try to normalize the declared dependency. If we can't then an error + // will be thrown. We will catch that error and report an error. + var declaredDependency; + try { + declaredDependency = analyzePropertyChain(declaredDependencyNode, null); + } + catch (error) { + if (error instanceof Error && + /Unsupported node type/.test(error.message)) { + if (declaredDependencyNode.type === 'Literal') { + if (declaredDependencyNode.value && + dependencies.has(declaredDependencyNode.value)) { + reportProblem({ + node: declaredDependencyNode, + message: "The ".concat(declaredDependencyNode.raw, " literal is not a valid dependency ") + + "because it never changes. " + + "Did you mean to include ".concat(declaredDependencyNode.value, " in the array instead?"), + }); + } + else { + reportProblem({ + node: declaredDependencyNode, + message: "The ".concat(declaredDependencyNode.raw, " literal is not a valid dependency ") + + 'because it never changes. You can safely remove it.', + }); + } + } + else { + reportProblem({ + node: declaredDependencyNode, + message: "React Hook ".concat(getSourceCode().getText(reactiveHook), " has a ") + + "complex expression in the dependency array. " + + 'Extract it to a separate variable so it can be statically checked.', + }); + } + return; + } + else { + throw error; + } + } + var maybeID = declaredDependencyNode; + while (maybeID.type === 'MemberExpression' || + maybeID.type === 'OptionalMemberExpression' || + maybeID.type === 'ChainExpression') { + // @ts-expect-error This can be done better + maybeID = maybeID.object || maybeID.expression.object; + } + var isDeclaredInComponent = !componentScope.through.some(function (ref) { return ref.identifier === maybeID; }); + // Add the dependency to our declared dependency map. + declaredDependencies.push({ + key: declaredDependency, + node: declaredDependencyNode, + }); + if (!isDeclaredInComponent) { + externalDependencies.add(declaredDependency); + } + }); + } + var _d = collectRecommendations({ + dependencies: dependencies, + declaredDependencies: declaredDependencies, + stableDependencies: stableDependencies, + externalDependencies: externalDependencies, + isEffect: isEffect, + }), suggestedDependencies = _d.suggestedDependencies, unnecessaryDependencies = _d.unnecessaryDependencies, missingDependencies = _d.missingDependencies, duplicateDependencies = _d.duplicateDependencies; + var suggestedDeps = suggestedDependencies; + var problemCount = duplicateDependencies.size + + missingDependencies.size + + unnecessaryDependencies.size; + if (problemCount === 0) { + // If nothing else to report, check if some dependencies would + // invalidate on every render. + var constructions = scanForConstructions({ + declaredDependencies: declaredDependencies, + declaredDependenciesNode: declaredDependenciesNode, + componentScope: componentScope, + scope: scope, + }); + constructions.forEach(function (_a) { + var _b; + var construction = _a.construction, isUsedOutsideOfHook = _a.isUsedOutsideOfHook, depType = _a.depType; + var wrapperHook = depType === 'function' ? 'useCallback' : 'useMemo'; + var constructionType = depType === 'function' ? 'definition' : 'initialization'; + var defaultAdvice = "wrap the ".concat(constructionType, " of '").concat(construction.name.name, "' in its own ").concat(wrapperHook, "() Hook."); + var advice = isUsedOutsideOfHook + ? "To fix this, ".concat(defaultAdvice) + : "Move it inside the ".concat(reactiveHookName, " callback. Alternatively, ").concat(defaultAdvice); + var causation = depType === 'conditional' || depType === 'logical expression' + ? 'could make' + : 'makes'; + var message = "The '".concat(construction.name.name, "' ").concat(depType, " ").concat(causation, " the dependencies of ") + + "".concat(reactiveHookName, " Hook (at line ").concat((_b = declaredDependenciesNode.loc) === null || _b === void 0 ? void 0 : _b.start.line, ") ") + + "change on every render. ".concat(advice); + var suggest; + // Only handle the simple case of variable assignments. + // Wrapping function declarations can mess up hoisting. + if (isUsedOutsideOfHook && + construction.type === 'Variable' && + // Objects may be mutated after construction, which would make this + // fix unsafe. Functions _probably_ won't be mutated, so we'll + // allow this fix for them. + depType === 'function') { + suggest = [ + { + desc: "Wrap the ".concat(constructionType, " of '").concat(construction.name.name, "' in its own ").concat(wrapperHook, "() Hook."), + fix: function (fixer) { + var _a = __read(wrapperHook === 'useMemo' + ? ["useMemo(() => { return ", '; })'] + : ['useCallback(', ')'], 2), before = _a[0], after = _a[1]; + return [ + // TODO: also add an import? + fixer.insertTextBefore(construction.node.init, before), + // TODO: ideally we'd gather deps here but it would require + // restructuring the rule code. This will cause a new lint + // error to appear immediately for useCallback. Note we're + // not adding [] because would that changes semantics. + fixer.insertTextAfter(construction.node.init, after), + ]; + }, + }, + ]; + } + // TODO: What if the function needs to change on every render anyway? + // Should we suggest removing effect deps as an appropriate fix too? + reportProblem({ + // TODO: Why not report this at the dependency site? + node: construction.node, + message: message, + suggest: suggest, + }); + }); + return; + } + // If we're going to report a missing dependency, + // we might as well recalculate the list ignoring + // the currently specified deps. This can result + // in some extra deduplication. We can't do this + // for effects though because those have legit + // use cases for over-specifying deps. + if (!isEffect && missingDependencies.size > 0) { + suggestedDeps = collectRecommendations({ + dependencies: dependencies, + declaredDependencies: [], // Pretend we don't know + stableDependencies: stableDependencies, + externalDependencies: externalDependencies, + isEffect: isEffect, + }).suggestedDependencies; + } + // Alphabetize the suggestions, but only if deps were already alphabetized. + function areDeclaredDepsAlphabetized() { + if (declaredDependencies.length === 0) { + return true; + } + var declaredDepKeys = declaredDependencies.map(function (dep) { return dep.key; }); + var sortedDeclaredDepKeys = declaredDepKeys.slice().sort(); + return declaredDepKeys.join(',') === sortedDeclaredDepKeys.join(','); + } + if (areDeclaredDepsAlphabetized()) { + suggestedDeps.sort(); + } + // Most of our algorithm deals with dependency paths with optional chaining stripped. + // This function is the last step before printing a dependency, so now is a good time to + // check whether any members in our path are always used as optional-only. In that case, + // we will use ?. instead of . to concatenate those parts of the path. + function formatDependency(path) { + var members = path.split('.'); + var finalPath = ''; + for (var i = 0; i < members.length; i++) { + if (i !== 0) { + var pathSoFar = members.slice(0, i + 1).join('.'); + var isOptional = optionalChains.get(pathSoFar) === true; + finalPath += isOptional ? '?.' : '.'; + } + finalPath += members[i]; + } + return finalPath; + } + function getWarningMessage(deps, singlePrefix, label, fixVerb) { + if (deps.size === 0) { + return null; + } + return ((deps.size > 1 ? '' : singlePrefix + ' ') + + label + + ' ' + + (deps.size > 1 ? 'dependencies' : 'dependency') + + ': ' + + joinEnglish(Array.from(deps) + .sort() + .map(function (name) { return "'" + formatDependency(name) + "'"; })) + + ". Either ".concat(fixVerb, " ").concat(deps.size > 1 ? 'them' : 'it', " or remove the dependency array.")); + } + var extraWarning = ''; + if (unnecessaryDependencies.size > 0) { + var badRef_1 = null; + Array.from(unnecessaryDependencies.keys()).forEach(function (key) { + if (badRef_1 !== null) { + return; + } + if (key.endsWith('.current')) { + badRef_1 = key; + } + }); + if (badRef_1 !== null) { + extraWarning = + " Mutable values like '".concat(badRef_1, "' aren't valid dependencies ") + + "because mutating them doesn't re-render the component."; + } + else if (externalDependencies.size > 0) { + var dep = Array.from(externalDependencies)[0]; + // Don't show this warning for things that likely just got moved *inside* the callback + // because in that case they're clearly not referring to globals. + if (!scope.set.has(dep)) { + extraWarning = + " Outer scope values like '".concat(dep, "' aren't valid dependencies ") + + "because mutating them doesn't re-render the component."; + } + } + } + // `props.foo()` marks `props` as a dependency because it has + // a `this` value. This warning can be confusing. + // So if we're going to show it, append a clarification. + if (!extraWarning && missingDependencies.has('props')) { + var propDep = dependencies.get('props'); + if (propDep == null) { + return; + } + var refs = propDep.references; + if (!Array.isArray(refs)) { + return; + } + var isPropsOnlyUsedInMembers = true; + try { + for (var refs_1 = __values(refs), refs_1_1 = refs_1.next(); !refs_1_1.done; refs_1_1 = refs_1.next()) { + var ref = refs_1_1.value; + var id = fastFindReferenceWithParent(componentScope.block, ref.identifier); + if (!id) { + isPropsOnlyUsedInMembers = false; + break; + } + var parent = id.parent; + if (parent == null) { + isPropsOnlyUsedInMembers = false; + break; + } + if (parent.type !== 'MemberExpression' && + parent.type !== 'OptionalMemberExpression') { + isPropsOnlyUsedInMembers = false; + break; + } + } + } + catch (e_1_1) { e_1 = { error: e_1_1 }; } + finally { + try { + if (refs_1_1 && !refs_1_1.done && (_a = refs_1.return)) _a.call(refs_1); + } + finally { if (e_1) throw e_1.error; } + } + if (isPropsOnlyUsedInMembers) { + extraWarning = + " However, 'props' will change when *any* prop changes, so the " + + "preferred fix is to destructure the 'props' object outside of " + + "the ".concat(reactiveHookName, " call and refer to those specific props ") + + "inside ".concat(getSourceCode().getText(reactiveHook), "."); + } + } + if (!extraWarning && missingDependencies.size > 0) { + // See if the user is trying to avoid specifying a callable prop. + // This usually means they're unaware of useCallback. + var missingCallbackDep_1 = null; + missingDependencies.forEach(function (missingDep) { + var e_12, _a; + var _b; + if (missingCallbackDep_1) { + return; + } + // Is this a variable from top scope? + var topScopeRef = componentScope.set.get(missingDep); + var usedDep = dependencies.get(missingDep); + if (!(usedDep === null || usedDep === void 0 ? void 0 : usedDep.references) || + ((_b = usedDep === null || usedDep === void 0 ? void 0 : usedDep.references[0]) === null || _b === void 0 ? void 0 : _b.resolved) !== topScopeRef) { + return; + } + // Is this a destructured prop? + var def = topScopeRef === null || topScopeRef === void 0 ? void 0 : topScopeRef.defs[0]; + if (def == null || def.name == null || def.type !== 'Parameter') { + return; + } + // Was it called in at least one case? Then it's a function. + var isFunctionCall = false; + var id; + try { + for (var _c = __values(usedDep.references), _d = _c.next(); !_d.done; _d = _c.next()) { + var reference = _d.value; + id = reference.identifier; + if (id != null && + id.parent != null && + (id.parent.type === 'CallExpression' || + id.parent.type === 'OptionalCallExpression') && + id.parent.callee === id) { + isFunctionCall = true; + break; + } + } + } + catch (e_12_1) { e_12 = { error: e_12_1 }; } + finally { + try { + if (_d && !_d.done && (_a = _c.return)) _a.call(_c); + } + finally { if (e_12) throw e_12.error; } + } + if (!isFunctionCall) { + return; + } + // If it's missing (i.e. in component scope) *and* it's a parameter + // then it is definitely coming from props destructuring. + // (It could also be props itself but we wouldn't be calling it then.) + missingCallbackDep_1 = missingDep; + }); + if (missingCallbackDep_1 !== null) { + extraWarning = + " If '".concat(missingCallbackDep_1, "' changes too often, ") + + "find the parent component that defines it " + + "and wrap that definition in useCallback."; + } + } + if (!extraWarning && missingDependencies.size > 0) { + var setStateRecommendation = null; + try { + for (var missingDependencies_1 = __values(missingDependencies), missingDependencies_1_1 = missingDependencies_1.next(); !missingDependencies_1_1.done; missingDependencies_1_1 = missingDependencies_1.next()) { + var missingDep = missingDependencies_1_1.value; + if (setStateRecommendation !== null) { + break; + } + var usedDep = dependencies.get(missingDep); + var references = usedDep.references; + var id = void 0; + var maybeCall = void 0; + try { + for (var references_1 = (e_3 = void 0, __values(references)), references_1_1 = references_1.next(); !references_1_1.done; references_1_1 = references_1.next()) { + var reference = references_1_1.value; + id = reference.identifier; + maybeCall = id.parent; + // Try to see if we have setState(someExpr(missingDep)). + while (maybeCall != null && maybeCall !== componentScope.block) { + if (maybeCall.type === 'CallExpression') { + var correspondingStateVariable = setStateCallSites.get(maybeCall.callee); + if (correspondingStateVariable != null) { + if ('name' in correspondingStateVariable && + correspondingStateVariable.name === missingDep) { + // setCount(count + 1) + setStateRecommendation = { + missingDep: missingDep, + setter: 'name' in maybeCall.callee ? maybeCall.callee.name : '', + form: 'updater', + }; + } + else if (stateVariables.has(id)) { + // setCount(count + increment) + setStateRecommendation = { + missingDep: missingDep, + setter: 'name' in maybeCall.callee ? maybeCall.callee.name : '', + form: 'reducer', + }; + } + else { + var resolved = reference.resolved; + if (resolved != null) { + // If it's a parameter *and* a missing dep, + // it must be a prop or something inside a prop. + // Therefore, recommend an inline reducer. + var def = resolved.defs[0]; + if (def != null && def.type === 'Parameter') { + setStateRecommendation = { + missingDep: missingDep, + setter: 'name' in maybeCall.callee + ? maybeCall.callee.name + : '', + form: 'inlineReducer', + }; + } + } + } + break; + } + } + maybeCall = maybeCall.parent; + } + if (setStateRecommendation !== null) { + break; + } + } + } + catch (e_3_1) { e_3 = { error: e_3_1 }; } + finally { + try { + if (references_1_1 && !references_1_1.done && (_c = references_1.return)) _c.call(references_1); + } + finally { if (e_3) throw e_3.error; } + } + } + } + catch (e_2_1) { e_2 = { error: e_2_1 }; } + finally { + try { + if (missingDependencies_1_1 && !missingDependencies_1_1.done && (_b = missingDependencies_1.return)) _b.call(missingDependencies_1); + } + finally { if (e_2) throw e_2.error; } + } + if (setStateRecommendation !== null) { + switch (setStateRecommendation.form) { + case 'reducer': + extraWarning = + " You can also replace multiple useState variables with useReducer " + + "if '".concat(setStateRecommendation.setter, "' needs the ") + + "current value of '".concat(setStateRecommendation.missingDep, "'."); + break; + case 'inlineReducer': + extraWarning = + " If '".concat(setStateRecommendation.setter, "' needs the ") + + "current value of '".concat(setStateRecommendation.missingDep, "', ") + + "you can also switch to useReducer instead of useState and " + + "read '".concat(setStateRecommendation.missingDep, "' in the reducer."); + break; + case 'updater': + extraWarning = + " You can also do a functional update '".concat(setStateRecommendation.setter, "(").concat(setStateRecommendation.missingDep.slice(0, 1), " => ...)' if you only need '").concat(setStateRecommendation.missingDep, "'") + " in the '".concat(setStateRecommendation.setter, "' call."); + break; + default: + throw new Error('Unknown case.'); + } + } + } + reportProblem({ + node: declaredDependenciesNode, + message: "React Hook ".concat(getSourceCode().getText(reactiveHook), " has ") + + // To avoid a long message, show the next actionable item. + (getWarningMessage(missingDependencies, 'a', 'missing', 'include') || + getWarningMessage(unnecessaryDependencies, 'an', 'unnecessary', 'exclude') || + getWarningMessage(duplicateDependencies, 'a', 'duplicate', 'omit')) + + extraWarning, + suggest: [ + { + desc: "Update the dependencies array to be: [".concat(suggestedDeps + .map(formatDependency) + .join(', '), "]"), + fix: function (fixer) { + // TODO: consider preserving the comments or formatting? + return fixer.replaceText(declaredDependenciesNode, "[".concat(suggestedDeps.map(formatDependency).join(', '), "]")); + }, + }, + ], + }); + } + function visitCallExpression(node) { + var callbackIndex = getReactiveHookCallbackIndex(node.callee, options); + if (callbackIndex === -1) { + // Not a React Hook call that needs deps. + return; + } + var callback = node.arguments[callbackIndex]; + var reactiveHook = node.callee; + var nodeWithoutNamespace = getNodeWithoutReactNamespace(reactiveHook); + var reactiveHookName = 'name' in nodeWithoutNamespace ? nodeWithoutNamespace.name : ''; + var maybeNode = node.arguments[callbackIndex + 1]; + var declaredDependenciesNode = maybeNode && + !(maybeNode.type === 'Identifier' && maybeNode.name === 'undefined') + ? maybeNode + : undefined; + var isEffect = /Effect($|[^a-z])/g.test(reactiveHookName); + // Check whether a callback is supplied. If there is no callback supplied + // then the hook will not work and React will throw a TypeError. + // So no need to check for dependency inclusion. + if (!callback) { + reportProblem({ + node: reactiveHook, + message: "React Hook ".concat(reactiveHookName, " requires an effect callback. ") + + "Did you forget to pass a callback to the hook?", + }); + return; + } + // Check the declared dependencies for this reactive hook. If there is no + // second argument then the reactive callback will re-run on every render. + // So no need to check for dependency inclusion. + if (!declaredDependenciesNode && !isEffect) { + // These are only used for optimization. + if (reactiveHookName === 'useMemo' || + reactiveHookName === 'useCallback') { + // TODO: Can this have a suggestion? + reportProblem({ + node: reactiveHook, + message: "React Hook ".concat(reactiveHookName, " does nothing when called with ") + + "only one argument. Did you forget to pass an array of " + + "dependencies?", + }); + } + return; + } + while (callback.type === 'TSAsExpression' || + callback.type === 'AsExpression') { + callback = callback.expression; + } + switch (callback.type) { + case 'FunctionExpression': + case 'ArrowFunctionExpression': + visitFunctionWithDependencies(callback, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect); + return; // Handled + case 'Identifier': + if (!declaredDependenciesNode) { + // No deps, no problems. + return; // Handled + } + // The function passed as a callback is not written inline. + // But perhaps it's in the dependencies array? + if ('elements' in declaredDependenciesNode && + declaredDependenciesNode.elements && + declaredDependenciesNode.elements.some(function (el) { return el && el.type === 'Identifier' && el.name === callback.name; })) { + // If it's already in the list of deps, we don't care because + // this is valid regardless. + return; // Handled + } + // We'll do our best effort to find it, complain otherwise. + var variable = getScope(callback).set.get(callback.name); + if (variable == null || variable.defs == null) { + // If it's not in scope, we don't care. + return; // Handled + } + // The function passed as a callback is not written inline. + // But it's defined somewhere in the render scope. + // We'll do our best effort to find and check it, complain otherwise. + var def = variable.defs[0]; + if (!def || !def.node) { + break; // Unhandled + } + if (def.type === 'Parameter') { + reportProblem({ + node: reactiveHook, + message: getUnknownDependenciesMessage(reactiveHookName), + }); + return; + } + if (def.type !== 'Variable' && def.type !== 'FunctionName') { + // Parameter or an unusual pattern. Bail out. + break; // Unhandled + } + switch (def.node.type) { + case 'FunctionDeclaration': + // useEffect(() => { ... }, []); + visitFunctionWithDependencies(def.node, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect); + return; // Handled + case 'VariableDeclarator': + var init = def.node.init; + if (!init) { + break; // Unhandled + } + switch (init.type) { + // const effectBody = () => {...}; + // useEffect(effectBody, []); + case 'ArrowFunctionExpression': + case 'FunctionExpression': + // We can inspect this function as if it were inline. + visitFunctionWithDependencies(init, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect); + return; // Handled + } + break; // Unhandled + } + break; // Unhandled + default: + // useEffect(generateEffectBody(), []); + reportProblem({ + node: reactiveHook, + message: getUnknownDependenciesMessage(reactiveHookName), + }); + return; // Handled + } + // Something unusual. Fall back to suggesting to add the body itself as a dep. + reportProblem({ + node: reactiveHook, + message: "React Hook ".concat(reactiveHookName, " has a missing dependency: '").concat(callback.name, "'. ") + + "Either include it or remove the dependency array.", + suggest: [ + { + desc: "Update the dependencies array to be: [".concat(callback.name, "]"), + fix: function (fixer) { + return fixer.replaceText(declaredDependenciesNode, "[".concat(callback.name, "]")); + }, + }, + ], + }); + } + return { + CallExpression: visitCallExpression, + }; + }, +}; +// The meat of the logic. +function collectRecommendations(_a) { + var dependencies = _a.dependencies, declaredDependencies = _a.declaredDependencies, stableDependencies = _a.stableDependencies, externalDependencies = _a.externalDependencies, isEffect = _a.isEffect; + // Our primary data structure. + // It is a logical representation of property chains: + // `props` -> `props.foo` -> `props.foo.bar` -> `props.foo.bar.baz` + // -> `props.lol` + // -> `props.huh` -> `props.huh.okay` + // -> `props.wow` + // We'll use it to mark nodes that are *used* by the programmer, + // and the nodes that were *declared* as deps. Then we will + // traverse it to learn which deps are missing or unnecessary. + var depTree = createDepTree(); + function createDepTree() { + return { + isUsed: false, // True if used in code + isSatisfiedRecursively: false, // True if specified in deps + isSubtreeUsed: false, // True if something deeper is used by code + children: new Map(), // Nodes for properties + }; + } + // Mark all required nodes first. + // Imagine exclamation marks next to each used deep property. + dependencies.forEach(function (_, key) { + var node = getOrCreateNodeByPath(depTree, key); + node.isUsed = true; + markAllParentsByPath(depTree, key, function (parent) { + parent.isSubtreeUsed = true; + }); + }); + // Mark all satisfied nodes. + // Imagine checkmarks next to each declared dependency. + declaredDependencies.forEach(function (_a) { + var key = _a.key; + var node = getOrCreateNodeByPath(depTree, key); + node.isSatisfiedRecursively = true; + }); + stableDependencies.forEach(function (key) { + var node = getOrCreateNodeByPath(depTree, key); + node.isSatisfiedRecursively = true; + }); + // Tree manipulation helpers. + function getOrCreateNodeByPath(rootNode, path) { + var e_13, _a; + var keys = path.split('.'); + var node = rootNode; + try { + for (var keys_1 = __values(keys), keys_1_1 = keys_1.next(); !keys_1_1.done; keys_1_1 = keys_1.next()) { + var key = keys_1_1.value; + var child = node.children.get(key); + if (!child) { + child = createDepTree(); + node.children.set(key, child); + } + node = child; + } + } + catch (e_13_1) { e_13 = { error: e_13_1 }; } + finally { + try { + if (keys_1_1 && !keys_1_1.done && (_a = keys_1.return)) _a.call(keys_1); + } + finally { if (e_13) throw e_13.error; } + } + return node; + } + function markAllParentsByPath(rootNode, path, fn) { + var e_14, _a; + var keys = path.split('.'); + var node = rootNode; + try { + for (var keys_2 = __values(keys), keys_2_1 = keys_2.next(); !keys_2_1.done; keys_2_1 = keys_2.next()) { + var key = keys_2_1.value; + var child = node.children.get(key); + if (!child) { + return; + } + fn(child); + node = child; + } + } + catch (e_14_1) { e_14 = { error: e_14_1 }; } + finally { + try { + if (keys_2_1 && !keys_2_1.done && (_a = keys_2.return)) _a.call(keys_2); + } + finally { if (e_14) throw e_14.error; } + } + } + // Now we can learn which dependencies are missing or necessary. + var missingDependencies = new Set(); + var satisfyingDependencies = new Set(); + scanTreeRecursively(depTree, missingDependencies, satisfyingDependencies, function (key) { return key; }); + function scanTreeRecursively(node, missingPaths, satisfyingPaths, keyToPath) { + node.children.forEach(function (child, key) { + var path = keyToPath(key); + if (child.isSatisfiedRecursively) { + if (child.isSubtreeUsed) { + // Remember this dep actually satisfied something. + satisfyingPaths.add(path); + } + // It doesn't matter if there's something deeper. + // It would be transitively satisfied since we assume immutability. + // `props.foo` is enough if you read `props.foo.id`. + return; + } + if (child.isUsed) { + // Remember that no declared deps satisfied this node. + missingPaths.add(path); + // If we got here, nothing in its subtree was satisfied. + // No need to search further. + return; + } + scanTreeRecursively(child, missingPaths, satisfyingPaths, function (childKey) { return path + '.' + childKey; }); + }); + } + // Collect suggestions in the order they were originally specified. + var suggestedDependencies = []; + var unnecessaryDependencies = new Set(); + var duplicateDependencies = new Set(); + declaredDependencies.forEach(function (_a) { + var key = _a.key; + // Does this declared dep satisfy a real need? + if (satisfyingDependencies.has(key)) { + if (suggestedDependencies.indexOf(key) === -1) { + // Good one. + suggestedDependencies.push(key); + } + else { + // Duplicate. + duplicateDependencies.add(key); + } + } + else { + if (isEffect && + !key.endsWith('.current') && + !externalDependencies.has(key)) { + // Effects are allowed extra "unnecessary" deps. + // Such as resetting scroll when ID changes. + // Consider them legit. + // The exception is ref.current which is always wrong. + if (suggestedDependencies.indexOf(key) === -1) { + suggestedDependencies.push(key); + } + } + else { + // It's definitely not needed. + unnecessaryDependencies.add(key); + } + } + }); + // Then add the missing ones at the end. + missingDependencies.forEach(function (key) { + suggestedDependencies.push(key); + }); + return { + suggestedDependencies: suggestedDependencies, + unnecessaryDependencies: unnecessaryDependencies, + duplicateDependencies: duplicateDependencies, + missingDependencies: missingDependencies, + }; +} +// If the node will result in constructing a referentially unique value, return +// its human readable type name, else return null. +function getConstructionExpressionType(node) { + switch (node.type) { + case 'ObjectExpression': + return 'object'; + case 'ArrayExpression': + return 'array'; + case 'ArrowFunctionExpression': + case 'FunctionExpression': + return 'function'; + case 'ClassExpression': + return 'class'; + case 'ConditionalExpression': + if (getConstructionExpressionType(node.consequent) != null || + getConstructionExpressionType(node.alternate) != null) { + return 'conditional'; + } + return null; + case 'LogicalExpression': + if (getConstructionExpressionType(node.left) != null || + getConstructionExpressionType(node.right) != null) { + return 'logical expression'; + } + return null; + case 'JSXFragment': + return 'JSX fragment'; + case 'JSXElement': + return 'JSX element'; + case 'AssignmentExpression': + if (getConstructionExpressionType(node.right) != null) { + return 'assignment expression'; + } + return null; + case 'NewExpression': + return 'object construction'; + case 'Literal': + if (node.value instanceof RegExp) { + return 'regular expression'; + } + return null; + case 'TypeCastExpression': + case 'AsExpression': + case 'TSAsExpression': + return getConstructionExpressionType(node.expression); + } + return null; +} +// Finds variables declared as dependencies +// that would invalidate on every render. +function scanForConstructions(_a) { + var declaredDependencies = _a.declaredDependencies, declaredDependenciesNode = _a.declaredDependenciesNode, componentScope = _a.componentScope, scope = _a.scope; + var constructions = declaredDependencies + .map(function (_a) { + var key = _a.key; + var ref = componentScope.variables.find(function (v) { return v.name === key; }); + if (ref == null) { + return null; + } + var node = ref.defs[0]; + if (node == null) { + return null; + } + // const handleChange = function () {} + // const handleChange = () => {} + // const foo = {} + // const foo = [] + // etc. + if (node.type === 'Variable' && + node.node.type === 'VariableDeclarator' && + node.node.id.type === 'Identifier' && // Ensure this is not destructed assignment + node.node.init != null) { + var constantExpressionType = getConstructionExpressionType(node.node.init); + if (constantExpressionType) { + return [ref, constantExpressionType]; + } + } + // function handleChange() {} + if (node.type === 'FunctionName' && + node.node.type === 'FunctionDeclaration') { + return [ref, 'function']; + } + // class Foo {} + if (node.type === 'ClassName' && node.node.type === 'ClassDeclaration') { + return [ref, 'class']; + } + return null; + }) + .filter(Boolean); + function isUsedOutsideOfHook(ref) { + var e_15, _a; + var foundWriteExpr = false; + try { + for (var _b = __values(ref.references), _c = _b.next(); !_c.done; _c = _b.next()) { + var reference = _c.value; + if (reference.writeExpr) { + if (foundWriteExpr) { + // Two writes to the same function. + return true; + } + else { + // Ignore first write as it's not usage. + foundWriteExpr = true; + continue; + } + } + var currentScope = reference.from; + while (currentScope !== scope && currentScope != null) { + currentScope = currentScope.upper; + } + if (currentScope !== scope) { + // This reference is outside the Hook callback. + // It can only be legit if it's the deps array. + if (!isAncestorNodeOf(declaredDependenciesNode, reference.identifier)) { + return true; + } + } + } + } + catch (e_15_1) { e_15 = { error: e_15_1 }; } + finally { + try { + if (_c && !_c.done && (_a = _b.return)) _a.call(_b); + } + finally { if (e_15) throw e_15.error; } + } + return false; + } + return constructions.map(function (_a) { + var _b = __read(_a, 2), ref = _b[0], depType = _b[1]; + return ({ + construction: ref.defs[0], + depType: depType, + isUsedOutsideOfHook: isUsedOutsideOfHook(ref), + }); + }); +} +/** + * Assuming () means the passed/returned node: + * (props) => (props) + * props.(foo) => (props.foo) + * props.foo.(bar) => (props).foo.bar + * props.foo.bar.(baz) => (props).foo.bar.baz + */ +function getDependency(node) { + if (node.parent && + (node.parent.type === 'MemberExpression' || + node.parent.type === 'OptionalMemberExpression') && + node.parent.object === node && + 'name' in node.parent.property && + node.parent.property.name !== 'current' && + !node.parent.computed && + !(node.parent.parent != null && + (node.parent.parent.type === 'CallExpression' || + node.parent.parent.type === 'OptionalCallExpression') && + node.parent.parent.callee === node.parent)) { + return getDependency(node.parent); + } + else if ( + // Note: we don't check OptionalMemberExpression because it can't be LHS. + node.type === 'MemberExpression' && + node.parent && + node.parent.type === 'AssignmentExpression' && + node.parent.left === node) { + return node.object; + } + else { + return node; + } +} +/** + * Mark a node as either optional or required. + * Note: If the node argument is an OptionalMemberExpression, it doesn't necessarily mean it is optional. + * It just means there is an optional member somewhere inside. + * This particular node might still represent a required member, so check .optional field. + */ +function markNode(node, optionalChains, result) { + if (optionalChains) { + if ('optional' in node && node.optional) { + // We only want to consider it optional if *all* usages were optional. + if (!optionalChains.has(result)) { + // Mark as (maybe) optional. If there's a required usage, this will be overridden. + optionalChains.set(result, true); + } + } + else { + // Mark as required. + optionalChains.set(result, false); + } + } +} +/** + * Assuming () means the passed node. + * (foo) -> 'foo' + * foo(.)bar -> 'foo.bar' + * foo.bar(.)baz -> 'foo.bar.baz' + * Otherwise throw. + */ +function analyzePropertyChain(node, optionalChains) { + if (node.type === 'Identifier' || node.type === 'JSXIdentifier') { + var result = node.name; + if (optionalChains) { + // Mark as required. + optionalChains.set(result, false); + } + return result; + } + else if (node.type === 'MemberExpression' && !node.computed) { + var object = analyzePropertyChain(node.object, optionalChains); + var property = analyzePropertyChain(node.property, null); + var result = "".concat(object, ".").concat(property); + markNode(node, optionalChains, result); + return result; + } + else if (node.type === 'OptionalMemberExpression' && !node.computed) { + var object = analyzePropertyChain(node.object, optionalChains); + var property = analyzePropertyChain(node.property, null); + var result = "".concat(object, ".").concat(property); + markNode(node, optionalChains, result); + return result; + } + else if (node.type === 'ChainExpression' && + (!('computed' in node) || !node.computed)) { + var expression = node.expression; + if (expression.type === 'CallExpression') { + throw new Error("Unsupported node type: ".concat(expression.type)); + } + var object = analyzePropertyChain(expression.object, optionalChains); + var property = analyzePropertyChain(expression.property, null); + var result = "".concat(object, ".").concat(property); + markNode(expression, optionalChains, result); + return result; + } + else { + throw new Error("Unsupported node type: ".concat(node.type)); + } +} +function getNodeWithoutReactNamespace(node) { + if (node.type === 'MemberExpression' && + node.object.type === 'Identifier' && + node.object.name === 'React' && + node.property.type === 'Identifier' && + !node.computed) { + return node.property; + } + return node; +} +// What's the index of callback that needs to be analyzed for a given Hook? +// -1 if it's not a Hook we care about (e.g. useState). +// 0 for useEffect/useMemo/useCallback(fn). +// 1 for useImperativeHandle(ref, fn). +// For additionally configured Hooks, assume that they're like useEffect (0). +function getReactiveHookCallbackIndex(calleeNode, options) { + var node = getNodeWithoutReactNamespace(calleeNode); + if (node.type !== 'Identifier') { + return -1; + } + switch (node.name) { + case 'useEffect': + case 'useLayoutEffect': + case 'useCallback': + case 'useMemo': + // useEffect(fn) + return 0; + case 'useImperativeHandle': + // useImperativeHandle(ref, fn) + return 1; + default: + if (node === calleeNode && options && options.additionalHooks) { + // Allow the user to provide a regular expression which enables the lint to + // target custom reactive hooks. + var name = void 0; + try { + name = analyzePropertyChain(node, null); + } + catch (error) { + if (error instanceof Error && + /Unsupported node type/.test(error.message)) { + return 0; + } + else { + throw error; + } + } + return options.additionalHooks.test(name) ? 0 : -1; + } + else { + return -1; + } + } +} +/** + * ESLint won't assign node.parent to references from context.getScope() + * + * So instead we search for the node from an ancestor assigning node.parent + * as we go. This mutates the AST. + * + * This traversal is: + * - optimized by only searching nodes with a range surrounding our target node + * - agnostic to AST node types, it looks for `{ type: string, ... }` + */ +function fastFindReferenceWithParent(start, target) { + var e_16, _a; + var queue = [start]; + var item; + while (queue.length) { + item = queue.shift(); + if (isSameIdentifier(item, target)) { + return item; + } + if (!isAncestorNodeOf(item, target)) { + continue; + } + try { + for (var _b = (e_16 = void 0, __values(Object.entries(item))), _c = _b.next(); !_c.done; _c = _b.next()) { + var _d = __read(_c.value, 2), key = _d[0], value = _d[1]; + if (key === 'parent') { + continue; + } + if (isNodeLike(value)) { + value.parent = item; + queue.push(value); + } + else if (Array.isArray(value)) { + value.forEach(function (val) { + if (isNodeLike(val)) { + val.parent = item; + queue.push(val); + } + }); + } + } + } + catch (e_16_1) { e_16 = { error: e_16_1 }; } + finally { + try { + if (_c && !_c.done && (_a = _b.return)) _a.call(_b); + } + finally { if (e_16) throw e_16.error; } + } + } + return null; +} +function joinEnglish(arr) { + var s = ''; + for (var i = 0; i < arr.length; i++) { + s += arr[i]; + if (i === 0 && arr.length === 2) { + s += ' and '; + } + else if (i === arr.length - 2 && arr.length > 2) { + s += ', and '; + } + else if (i < arr.length - 1) { + s += ', '; + } + } + return s; +} +function isNodeLike(val) { + return (typeof val === 'object' && + val !== null && + !Array.isArray(val) && + 'type' in val && + typeof val.type === 'string'); +} +function isSameIdentifier(a, b) { + return ((a.type === 'Identifier' || a.type === 'JSXIdentifier') && + a.type === b.type && + a.name === b.name && + !!a.range && + !!b.range && + a.range[0] === b.range[0] && + a.range[1] === b.range[1]); +} +function isAncestorNodeOf(a, b) { + return (!!a.range && + !!b.range && + a.range[0] <= b.range[0] && + a.range[1] >= b.range[1]); +} +function isUseEffectEventIdentifier(node) { + return false; +} +function getUnknownDependenciesMessage(reactiveHookName) { + return ("React Hook ".concat(reactiveHookName, " received a function whose dependencies ") + + "are unknown. Pass an inline function instead."); +} + +// All rules +var rules = { + 'rules-of-hooks': rule$1, + 'exhaustive-deps': rule, +}; +// Config rules +var configRules = { + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', +}; +// Legacy config +var legacyRecommendedConfig = { + plugins: ['react-hooks'], + rules: configRules, +}; +// Plugin object +var plugin = { + // TODO: Make this more dynamic to populate version from package.json. + // This can be done by injecting at build time, since importing the package.json isn't an option in Meta + meta: { name: 'eslint-plugin-react-hooks' }, + rules: rules, + configs: { + /** Legacy recommended config, to be used with rc-based configurations */ + 'recommended-legacy': legacyRecommendedConfig, + /** + * 'recommended' is currently aliased to the legacy / rc recommended config) to maintain backwards compatibility. + * This is deprecated and in v6, it will switch to alias the flat recommended config. + */ + recommended: legacyRecommendedConfig, + /** Latest recommended config, to be used with flat configurations */ + 'recommended-latest': { + name: 'react-hooks/recommended', + plugins: { + get 'react-hooks'() { + return plugin; + }, + }, + rules: configRules, + }, + }, +}; +var configs = plugin.configs; +var meta = plugin.meta; +// TODO: If the plugin is ever updated to be pure ESM and drops support for rc-based configs, then it should be exporting the plugin as default +// instead of individual named exports. +// export default plugin; + +exports.configs = configs; +exports.meta = meta; +exports.rules = rules; + })(); +}