eslint-plugin-react-hooks
Version:
ESLint rules for React Hooks
1,014 lines (1,008 loc) • 2.12 MB
JavaScript
/**
* @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);