UNPKG

eslint-plugin-react-hooks

Version:

ESLint rules for React Hooks

1,014 lines (1,008 loc) • 2.12 MB
/** * @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'; var core$1 = require('@babel/core'); var BabelParser = require('@babel/parser'); var zod = require('zod'); var zodValidationError = require('zod-validation-error'); var crypto = require('crypto'); var HermesParser = require('hermes-parser'); var util = require('util'); const SETTINGS_KEY = 'react-hooks'; const SETTINGS_ADDITIONAL_EFFECT_HOOKS_KEY = 'additionalEffectHooks'; function getAdditionalEffectHooksFromSettings(settings) { var _a; const additionalHooks = (_a = settings[SETTINGS_KEY]) === null || _a === void 0 ? void 0 : _a[SETTINGS_ADDITIONAL_EFFECT_HOOKS_KEY]; if (additionalHooks != null && typeof additionalHooks === 'string') { return new RegExp(additionalHooks); } return undefined; } const rule$1 = { 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', }, experimental_autoDependenciesHooks: { type: 'array', items: { type: 'string', }, }, requireExplicitEffectDeps: { type: 'boolean', }, }, }, ], }, create(context) { const rawOptions = context.options && context.options[0]; const settings = context.settings || {}; const additionalHooks = rawOptions && rawOptions.additionalHooks ? new RegExp(rawOptions.additionalHooks) : getAdditionalEffectHooksFromSettings(settings); const enableDangerousAutofixThisMayCauseInfiniteLoops = (rawOptions && rawOptions.enableDangerousAutofixThisMayCauseInfiniteLoops) || false; const experimental_autoDependenciesHooks = rawOptions && Array.isArray(rawOptions.experimental_autoDependenciesHooks) ? rawOptions.experimental_autoDependenciesHooks : []; const requireExplicitEffectDeps = (rawOptions && rawOptions.requireExplicitEffectDeps) || false; const options = { additionalHooks, experimental_autoDependenciesHooks, enableDangerousAutofixThisMayCauseInfiniteLoops, requireExplicitEffectDeps, }; function reportProblem(problem) { if (enableDangerousAutofixThisMayCauseInfiniteLoops) { if (Array.isArray(problem.suggest) && problem.suggest.length > 0 && problem.suggest[0]) { problem.fix = problem.suggest[0].fix; } } context.report(problem); } const getSourceCode = typeof context.getSourceCode === 'function' ? () => { return context.getSourceCode(); } : () => { return context.sourceCode; }; const getScope = typeof context.getScope === 'function' ? () => { return context.getScope(); } : (node) => { return context.sourceCode.getScope(node); }; const scopeManager = getSourceCode().scopeManager; const setStateCallSites = new WeakMap(); const stateVariables = new WeakSet(); const stableKnownValueCache = new WeakMap(); const functionWithoutCapturedValueCache = new WeakMap(); const useEffectEventVariables = new WeakSet(); function memoizeWithWeakMap(fn, map) { return function (arg) { if (map.has(arg)) { return map.get(arg); } const result = fn(arg); map.set(arg, result); return result; }; } function visitFunctionWithDependencies(node, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect, isAutoDepsHook) { 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', }); } const 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.'); } const pureScopes = new Set(); let componentScope = null; { let currentScope = scope.upper; while (currentScope) { pureScopes.add(currentScope); if (currentScope.type === 'function' || currentScope.type === 'hook' || currentScope.type === 'component') { break; } currentScope = currentScope.upper; } if (!currentScope) { return; } componentScope = currentScope; } const isArray = Array.isArray; function isStableKnownHookValue(resolved) { if (!isArray(resolved.defs)) { return false; } const def = resolved.defs[0]; if (def == null) { return false; } const defNode = def.node; if (defNode.type !== 'VariableDeclarator') { return false; } let init = defNode.init; if (init == null) { return false; } while (init.type === 'TSAsExpression' || init.type === 'AsExpression') { init = init.expression; } let declaration = defNode.parent; if (declaration == null && componentScope != null) { 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)) { return true; } if (init.type !== 'CallExpression') { return false; } let callee = init.callee; 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; } const definitionNode = def.node; const id = definitionNode.id; const { name } = callee; if (name === 'useRef' && id.type === 'Identifier') { return true; } else if (isUseEffectEventIdentifier$1(callee) && id.type === 'Identifier') { for (const ref of resolved.references) { if (ref !== id) { useEffectEventVariables.add(ref.identifier); } } return true; } else if (name === 'useState' || name === 'useReducer' || name === 'useActionState') { if (id.type === 'ArrayPattern' && id.elements.length === 2 && isArray(resolved.identifiers)) { if (id.elements[1] === resolved.identifiers[0]) { if (name === 'useState') { const references = resolved.references; let writeCount = 0; for (const reference of references) { if (reference.isWrite()) { writeCount++; } if (writeCount > 1) { return false; } setStateCallSites.set(reference.identifier, id.elements[0]); } } return true; } else if (id.elements[0] === resolved.identifiers[0]) { if (name === 'useState') { const references = resolved.references; for (const reference of references) { stateVariables.add(reference.identifier); } } return false; } } } else if (name === 'useTransition') { if (id.type === 'ArrayPattern' && id.elements.length === 2 && Array.isArray(resolved.identifiers)) { if (id.elements[1] === resolved.identifiers[0]) { return true; } } } return false; } function isFunctionWithoutCapturedValues(resolved) { if (!isArray(resolved.defs)) { return false; } const def = resolved.defs[0]; if (def == null) { return false; } if (def.node == null || def.node.id == null) { return false; } const fnNode = def.node; const childScopes = (componentScope === null || componentScope === void 0 ? void 0 : componentScope.childScopes) || []; let fnScope = null; for (const childScope of childScopes) { const childScopeBlock = childScope.block; if ((fnNode.type === 'FunctionDeclaration' && childScopeBlock === fnNode) || (fnNode.type === 'VariableDeclarator' && childScopeBlock.parent === fnNode)) { fnScope = childScope; break; } } if (fnScope == null) { return false; } for (const ref of fnScope.through) { if (ref.resolved == null) { continue; } if (pureScopes.has(ref.resolved.scope) && !memoizedIsStableKnownHookValue(ref.resolved)) { return false; } } return true; } const memoizedIsStableKnownHookValue = memoizeWithWeakMap(isStableKnownHookValue, stableKnownValueCache); const memoizedIsFunctionWithoutCapturedValues = memoizeWithWeakMap(isFunctionWithoutCapturedValues, functionWithoutCapturedValueCache); const currentRefsInEffectCleanup = new Map(); function isInsideEffectCleanup(reference) { let curScope = reference.from; let 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; } const dependencies = new Map(); const optionalChains = new Map(); gatherDependenciesRecursively(scope); function gatherDependenciesRecursively(currentScope) { var _a, _b, _c, _d, _e; for (const reference of currentScope.references) { if (!reference.resolved) { continue; } if (!pureScopes.has(reference.resolved.scope)) { continue; } const referenceNode = fastFindReferenceWithParent(node, reference.identifier); if (referenceNode == null) { continue; } const dependencyNode = getDependency(referenceNode); const dependency = analyzePropertyChain(dependencyNode, optionalChains); if (isEffect && dependencyNode.type === 'Identifier' && (((_a = dependencyNode.parent) === null || _a === void 0 ? void 0 : _a.type) === 'MemberExpression' || ((_b = dependencyNode.parent) === null || _b === void 0 ? void 0 : _b.type) === 'OptionalMemberExpression') && !dependencyNode.parent.computed && dependencyNode.parent.property.type === 'Identifier' && dependencyNode.parent.property.name === 'current' && isInsideEffectCleanup(reference)) { currentRefsInEffectCleanup.set(dependency, { reference, dependencyNode, }); } if (((_c = dependencyNode.parent) === null || _c === void 0 ? void 0 : _c.type) === 'TSTypeQuery' || ((_d = dependencyNode.parent) === null || _d === void 0 ? void 0 : _d.type) === 'TSTypeReference') { continue; } const def = reference.resolved.defs[0]; if (def == null) { continue; } if (def.node != null && def.node.init === node.parent) { continue; } if (def.type === 'TypeParameter') { continue; } if (!dependencies.has(dependency)) { const resolved = reference.resolved; const isStable = memoizedIsStableKnownHookValue(resolved) || memoizedIsFunctionWithoutCapturedValues(resolved); dependencies.set(dependency, { isStable, references: [reference], }); } else { (_e = dependencies.get(dependency)) === null || _e === void 0 ? void 0 : _e.references.push(reference); } } for (const childScope of currentScope.childScopes) { gatherDependenciesRecursively(childScope); } } currentRefsInEffectCleanup.forEach(({ reference, dependencyNode }, dependency) => { var _a, _b; const references = ((_a = reference.resolved) === null || _a === void 0 ? void 0 : _a.references) || []; let foundCurrentAssignment = false; for (const ref of references) { const { identifier } = ref; const { parent } = identifier; if (parent != null && parent.type === 'MemberExpression' && !parent.computed && parent.property.type === 'Identifier' && parent.property.name === 'current' && ((_b = parent.parent) === null || _b === void 0 ? void 0 : _b.type) === 'AssignmentExpression' && parent.parent.left === parent) { foundCurrentAssignment = true; break; } } if (foundCurrentAssignment) { return; } reportProblem({ node: dependencyNode.parent.property, message: `The ref value '${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 ` + `'${dependency}.current' to a variable inside the effect, and ` + `use that variable in the cleanup function.`, }); }); const staleAssignments = new Set(); function reportStaleAssignment(writeExpr, key) { if (staleAssignments.has(key)) { return; } staleAssignments.add(key); reportProblem({ node: writeExpr, message: `Assignments to the '${key}' variable from inside React Hook ` + `${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 ` + `${getSourceCode().getText(reactiveHook)}.`, }); } const stableDependencies = new Set(); dependencies.forEach(({ isStable, references }, key) => { if (isStable) { stableDependencies.add(key); } references.forEach(reference => { if (reference.writeExpr) { reportStaleAssignment(reference.writeExpr, key); } }); }); if (staleAssignments.size > 0) { return; } if (!declaredDependenciesNode) { if (isAutoDepsHook) { return; } let setStateInsideEffectWithoutDeps = null; dependencies.forEach(({ references }, key) => { if (setStateInsideEffectWithoutDeps) { return; } references.forEach(reference => { if (setStateInsideEffectWithoutDeps) { return; } const id = reference.identifier; const isSetState = setStateCallSites.has(id); if (!isSetState) { return; } let fnScope = reference.from; while (fnScope != null && fnScope.type !== 'function') { fnScope = fnScope.upper; } const isDirectlyInsideEffect = (fnScope === null || fnScope === void 0 ? void 0 : fnScope.block) === node; if (isDirectlyInsideEffect) { setStateInsideEffectWithoutDeps = key; } }); }); if (setStateInsideEffectWithoutDeps) { const { suggestedDependencies } = collectRecommendations({ dependencies, declaredDependencies: [], stableDependencies, externalDependencies: new Set(), isEffect: true, }); reportProblem({ node: reactiveHook, message: `React Hook ${reactiveHookName} contains a call to '${setStateInsideEffectWithoutDeps}'. ` + `Without a list of dependencies, this can lead to an infinite chain of updates. ` + `To fix this, pass [` + suggestedDependencies.join(', ') + `] as a second argument to the ${reactiveHookName} Hook.`, suggest: [ { desc: `Add dependencies array: [${suggestedDependencies.join(', ')}]`, fix(fixer) { return fixer.insertTextAfter(node, `, [${suggestedDependencies.join(', ')}]`); }, }, ], }); } return; } if (isAutoDepsHook && declaredDependenciesNode.type === 'Literal' && declaredDependenciesNode.value === null) { return; } const declaredDependencies = []; const externalDependencies = new Set(); const isArrayExpression = declaredDependenciesNode.type === 'ArrayExpression'; const isTSAsArrayExpression = declaredDependenciesNode.type === 'TSAsExpression' && declaredDependenciesNode.expression.type === 'ArrayExpression'; if (!isArrayExpression && !isTSAsArrayExpression) { reportProblem({ node: declaredDependenciesNode, message: `React Hook ${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 { const arrayExpression = isTSAsArrayExpression ? declaredDependenciesNode.expression : declaredDependenciesNode; arrayExpression.elements.forEach(declaredDependencyNode => { if (declaredDependencyNode === null) { return; } if (declaredDependencyNode.type === 'SpreadElement') { reportProblem({ node: declaredDependencyNode, message: `React Hook ${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 \`${getSourceCode().getText(declaredDependencyNode)}\` from the list.`, suggest: [ { desc: `Remove the dependency \`${getSourceCode().getText(declaredDependencyNode)}\``, fix(fixer) { return fixer.removeRange(declaredDependencyNode.range); }, }, ], }); } let 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 ${declaredDependencyNode.raw} literal is not a valid dependency ` + `because it never changes. ` + `Did you mean to include ${declaredDependencyNode.value} in the array instead?`, }); } else { reportProblem({ node: declaredDependencyNode, message: `The ${declaredDependencyNode.raw} literal is not a valid dependency ` + 'because it never changes. You can safely remove it.', }); } } else { reportProblem({ node: declaredDependencyNode, message: `React Hook ${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; } } let maybeID = declaredDependencyNode; while (maybeID.type === 'MemberExpression' || maybeID.type === 'OptionalMemberExpression' || maybeID.type === 'ChainExpression') { maybeID = maybeID.object || maybeID.expression.object; } const isDeclaredInComponent = !componentScope.through.some(ref => ref.identifier === maybeID); declaredDependencies.push({ key: declaredDependency, node: declaredDependencyNode, }); if (!isDeclaredInComponent) { externalDependencies.add(declaredDependency); } }); } const { suggestedDependencies, unnecessaryDependencies, missingDependencies, duplicateDependencies, } = collectRecommendations({ dependencies, declaredDependencies, stableDependencies, externalDependencies, isEffect, }); let suggestedDeps = suggestedDependencies; const problemCount = duplicateDependencies.size + missingDependencies.size + unnecessaryDependencies.size; if (problemCount === 0) { const constructions = scanForConstructions({ declaredDependencies, declaredDependenciesNode, componentScope, scope, }); constructions.forEach(({ construction, isUsedOutsideOfHook, depType }) => { var _a; const wrapperHook = depType === 'function' ? 'useCallback' : 'useMemo'; const constructionType = depType === 'function' ? 'definition' : 'initialization'; const defaultAdvice = `wrap the ${constructionType} of '${construction.name.name}' in its own ${wrapperHook}() Hook.`; const advice = isUsedOutsideOfHook ? `To fix this, ${defaultAdvice}` : `Move it inside the ${reactiveHookName} callback. Alternatively, ${defaultAdvice}`; const causation = depType === 'conditional' || depType === 'logical expression' ? 'could make' : 'makes'; const message = `The '${construction.name.name}' ${depType} ${causation} the dependencies of ` + `${reactiveHookName} Hook (at line ${(_a = declaredDependenciesNode.loc) === null || _a === void 0 ? void 0 : _a.start.line}) ` + `change on every render. ${advice}`; let suggest; if (isUsedOutsideOfHook && construction.type === 'Variable' && depType === 'function') { suggest = [ { desc: `Wrap the ${constructionType} of '${construction.name.name}' in its own ${wrapperHook}() Hook.`, fix(fixer) { const [before, after] = wrapperHook === 'useMemo' ? [`useMemo(() => { return `, '; })'] : ['useCallback(', ')']; return [ fixer.insertTextBefore(construction.node.init, before), fixer.insertTextAfter(construction.node.init, after), ]; }, }, ]; } reportProblem({ node: construction.node, message, suggest, }); }); return; } if (!isEffect && missingDependencies.size > 0) { suggestedDeps = collectRecommendations({ dependencies, declaredDependencies: [], stableDependencies, externalDependencies, isEffect, }).suggestedDependencies; } function areDeclaredDepsAlphabetized() { if (declaredDependencies.length === 0) { return true; } const declaredDepKeys = declaredDependencies.map(dep => dep.key); const sortedDeclaredDepKeys = declaredDepKeys.slice().sort(); return declaredDepKeys.join(',') === sortedDeclaredDepKeys.join(','); } if (areDeclaredDepsAlphabetized()) { suggestedDeps.sort(); } function formatDependency(path) { const members = path.split('.'); let finalPath = ''; for (let i = 0; i < members.length; i++) { if (i !== 0) { const pathSoFar = members.slice(0, i + 1).join('.'); const 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(name => "'" + formatDependency(name) + "'")) + `. Either ${fixVerb} ${deps.size > 1 ? 'them' : 'it'} or remove the dependency array.`); } let extraWarning = ''; if (unnecessaryDependencies.size > 0) { let badRef = null; Array.from(unnecessaryDependencies.keys()).forEach(key => { if (badRef !== null) { return; } if (key.endsWith('.current')) { badRef = key; } }); if (badRef !== null) { extraWarning = ` Mutable values like '${badRef}' aren't valid dependencies ` + "because mutating them doesn't re-render the component."; } else if (externalDependencies.size > 0) { const dep = Array.from(externalDependencies)[0]; if (!scope.set.has(dep)) { extraWarning = ` Outer scope values like '${dep}' aren't valid dependencies ` + `because mutating them doesn't re-render the component.`; } } } if (!extraWarning && missingDependencies.has('props')) { const propDep = dependencies.get('props'); if (propDep == null) { return; } const refs = propDep.references; if (!Array.isArray(refs)) { return; } let isPropsOnlyUsedInMembers = true; for (const ref of refs) { const id = fastFindReferenceWithParent(componentScope.block, ref.identifier); if (!id) { isPropsOnlyUsedInMembers = false; break; } const parent = id.parent; if (parent == null) { isPropsOnlyUsedInMembers = false; break; } if (parent.type !== 'MemberExpression' && parent.type !== 'OptionalMemberExpression') { isPropsOnlyUsedInMembers = false; break; } } if (isPropsOnlyUsedInMembers) { extraWarning = ` However, 'props' will change when *any* prop changes, so the ` + `preferred fix is to destructure the 'props' object outside of ` + `the ${reactiveHookName} call and refer to those specific props ` + `inside ${getSourceCode().getText(reactiveHook)}.`; } } if (!extraWarning && missingDependencies.size > 0) { let missingCallbackDep = null; missingDependencies.forEach(missingDep => { var _a; if (missingCallbackDep) { return; } const topScopeRef = componentScope.set.get(missingDep); const usedDep = dependencies.get(missingDep); if (!(usedDep === null || usedDep === void 0 ? void 0 : usedDep.references) || ((_a = usedDep === null || usedDep === void 0 ? void 0 : usedDep.references[0]) === null || _a === void 0 ? void 0 : _a.resolved) !== topScopeRef) { return; } const def = topScopeRef === null || topScopeRef === void 0 ? void 0 : topScopeRef.defs[0]; if (def == null || def.name == null || def.type !== 'Parameter') { return; } let isFunctionCall = false; let id; for (const reference of usedDep.references) { id = reference.identifier; if (id != null && id.parent != null && (id.parent.type === 'CallExpression' || id.parent.type === 'OptionalCallExpression') && id.parent.callee === id) { isFunctionCall = true; break; } } if (!isFunctionCall) { return; } missingCallbackDep = missingDep; }); if (missingCallbackDep !== null) { extraWarning = ` If '${missingCallbackDep}' changes too often, ` + `find the parent component that defines it ` + `and wrap that definition in useCallback.`; } } if (!extraWarning && missingDependencies.size > 0) { let setStateRecommendation = null; for (const missingDep of missingDependencies) { if (setStateRecommendation !== null) { break; } const usedDep = dependencies.get(missingDep); const references = usedDep.references; let id; let maybeCall; for (const reference of references) { id = reference.identifier; maybeCall = id.parent; while (maybeCall != null && maybeCall !== componentScope.block) { if (maybeCall.type === 'CallExpression') { const correspondingStateVariable = setStateCallSites.get(maybeCall.callee); if (correspondingStateVariable != null) { if ('name' in correspondingStateVariable && correspondingStateVariable.name === missingDep) { setStateRecommendation = { missingDep, setter: 'name' in maybeCall.callee ? maybeCall.callee.name : '', form: 'updater', }; } else if (stateVariables.has(id)) { setStateRecommendation = { missingDep, setter: 'name' in maybeCall.callee ? maybeCall.callee.name : '', form: 'reducer', }; } else { const resolved = reference.resolved; if (resolved != null) { const def = resolved.defs[0]; if (def != null && def.type === 'Parameter') { setStateRecommendation = { missingDep, setter: 'name' in maybeCall.callee ? maybeCall.callee.name : '', form: 'inlineReducer', }; } } } break; } } maybeCall = maybeCall.parent; } if (setStateRecommendation !== null) { break; } } } if (setStateRecommendation !== null) { switch (setStateRecommendation.form) { case 'reducer': extraWarning = ` You can also replace multiple useState variables with useReducer ` + `if '${setStateRecommendation.setter}' needs the ` + `current value of '${setStateRecommendation.missingDep}'.`; break; case 'inlineReducer': extraWarning = ` If '${setStateRecommendation.setter}' needs the ` + `current value of '${setStateRecommendation.missingDep}', ` + `you can also switch to useReducer instead of useState and ` + `read '${setStateRecommendation.missingDep}' in the reducer.`; break; case 'updater': extraWarning = ` You can also do a functional update '${setStateRecommendation.setter}(${setStateRecommendation.missingDep.slice(0, 1)} => ...)' if you only need '${setStateRecommendation.missingDep}'` + ` in the '${setStateRecommendation.setter}' call.`; break; default: throw new Error('Unknown case.'); } } } reportProblem({ node: declaredDependenciesNode, message: `React Hook ${getSourceCode().getText(reactiveHook)} has ` + (getWarningMessage(missingDependencies, 'a', 'missing', 'include') || getWarningMessage(unnecessaryDependencies, 'an', 'unnecessary', 'exclude') || getWarningMessage(duplicateDependencies, 'a', 'duplicate', 'omit')) + extraWarning, suggest: [ { desc: `Update the dependencies array to be: [${suggestedDeps .map(formatDependency) .join(', ')}]`, fix(fixer) { return fixer.replaceText(declaredDependenciesNode, `[${suggestedDeps.map(formatDependency).join(', ')}]`); }, }, ], }); } function visitCallExpression(node) { const callbackIndex = getReactiveHookCallbackIndex(node.callee, options); if (callbackIndex === -1) { return; } let callback = node.arguments[callbackIndex]; const reactiveHook = node.callee; const nodeWithoutNamespace = getNodeWithoutReactNamespace$1(reactiveHook); const reactiveHookName = 'name' in nodeWithoutNamespace ? nodeWithoutNamespace.name : ''; const maybeNode = node.arguments[callbackIndex + 1]; const declaredDependenciesNode = maybeNode && !(maybeNode.type === 'Identifier' && maybeNode.name === 'undefined') ? maybeNode : undefined; const isEffect = /Effect($|[^a-z])/g.test(reactiveHookName); if (!callback) { reportProblem({ node: reactiveHook, message: `React Hook ${reactiveHookName} requires an effect callback. ` + `Did you forget to pass a callback to the hook?`, }); return; } if (!maybeNode && isEffect && options.requireExplicitEffectDeps) { reportProblem({ node: reactiveHook, message: `React Hook ${reactiveHookName} always requires dependencies. ` + `Please add a dependency array or an explicit \`undefined\``, }); } const isAutoDepsHook = options.experimental_autoDependenciesHooks.includes(reactiveHookName); if ((!declaredDependenciesNode || (isAutoDepsHook && declaredDependenciesNode.type === 'Literal' && declaredDependenciesNode.value === null)) && !isEffect) { if (reactiveHookName === 'useMemo' || reactiveHookName === 'useCallback') { reportProblem({ node: reactiveHook, message: `React Hook ${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, isAutoDepsHook); return; case 'Identifier': if (!declaredDependenciesNode || (isAutoDepsHook && declaredDependenciesNode.type === 'Literal' && declaredDependenciesNode.value === null)) { return; } if ('elements' in declaredDependenciesNode && declaredDependenciesNode.elements && declaredDependenciesNode.elements.some(el => el && el.type === 'Identifier' && el.name === callback.name)) { return; } const variable = getScope(callback).set.get(callback.name); if (variable == null || variable.defs == null) { return; } const def = variable.defs[0]; if (!def || !def.node) { break; } if (def.type === 'Parameter') { reportProblem({ node: reactiveHook, message: getUnknownDependenciesMessage(reactiveHookName), }); return; } if (def.type !== 'Variable' && def.type !== 'FunctionName') { break; } switch (def.node.type) { case 'FunctionDeclaration': visitFunctionWithDependencies(def.node, declaredDependenciesNode, reactiveHook, reactiveHookName, isEffect, isAutoDepsHook);