eslint-plugin-react-hooks-extra
Version:
ESLint React's ESLint plugin for React Hooks related rules.
271 lines (262 loc) • 10.6 kB
JavaScript
import { WEBSITE_URL, getConfigAdapters } from "@eslint-react/shared";
import * as AST from "@eslint-react/ast";
import { isReactHookName, isUseCallbackCall, isUseEffectLikeCall, isUseMemoCall, isUseStateCall } from "@eslint-react/core";
import { constVoid, getOrElseUpdate, not } from "@eslint-react/eff";
import { findVariable, getVariableDefinitionNode } from "@eslint-react/var";
import { AST_NODE_TYPES } from "@typescript-eslint/types";
import { getStaticValue } from "@typescript-eslint/utils/ast-utils";
import { match } from "ts-pattern";
import { ESLintUtils } from "@typescript-eslint/utils";
//#region rolldown:runtime
var __defProp = Object.defineProperty;
var __export = (all, symbols) => {
let target = {};
for (var name$2 in all) {
__defProp(target, name$2, {
get: all[name$2],
enumerable: true
});
}
if (symbols) {
__defProp(target, Symbol.toStringTag, { value: "Module" });
}
return target;
};
//#endregion
//#region src/configs/recommended.ts
var recommended_exports = /* @__PURE__ */ __export({
name: () => name$1,
rules: () => rules
});
const name$1 = "react-hooks-extra/recommended";
const rules = { "react-hooks-extra/no-direct-set-state-in-use-effect": "warn" };
//#endregion
//#region package.json
var name = "eslint-plugin-react-hooks-extra";
var version = "2.3.13";
//#endregion
//#region src/utils/create-rule.ts
function getDocsUrl(ruleName) {
return `${WEBSITE_URL}/docs/rules/hooks-extra-${ruleName}`;
}
const createRule = ESLintUtils.RuleCreator(getDocsUrl);
//#endregion
//#region src/utils/is-variable-declarator-from-hook-call.ts
function isInitFromHookCall(init) {
if (init?.type !== AST_NODE_TYPES.CallExpression) return false;
switch (init.callee.type) {
case AST_NODE_TYPES.Identifier: return isReactHookName(init.callee.name);
case AST_NODE_TYPES.MemberExpression: return init.callee.property.type === AST_NODE_TYPES.Identifier && isReactHookName(init.callee.property.name);
default: return false;
}
}
function isVariableDeclaratorFromHookCall(node) {
if (node.type !== AST_NODE_TYPES.VariableDeclarator) return false;
if (node.id.type !== AST_NODE_TYPES.Identifier) return false;
return isInitFromHookCall(node.init);
}
//#endregion
//#region src/rules/no-direct-set-state-in-use-effect.ts
const RULE_NAME = "no-direct-set-state-in-use-effect";
const RULE_FEATURES = ["EXP"];
var no_direct_set_state_in_use_effect_default = createRule({
meta: {
type: "problem",
docs: {
description: "Disallow direct calls to the `set` function of `useState` in `useEffect`.",
[Symbol.for("rule_features")]: RULE_FEATURES
},
messages: { noDirectSetStateInUseEffect: "Do not call the 'set' function '{{name}}' of 'useState' directly in 'useEffect'." },
schema: []
},
name: RULE_NAME,
create,
defaultOptions: []
});
function create(context) {
if (!/use\w*Effect/u.test(context.sourceCode.text)) return {};
const functionEntries = [];
const setupFnRef = { current: null };
const setupFnIds = [];
const trackedFnCalls = [];
const setStateCallsByFn = /* @__PURE__ */ new WeakMap();
const setStateInEffectArg = /* @__PURE__ */ new WeakMap();
const setStateInEffectSetup = /* @__PURE__ */ new Map();
const setStateInHookCallbacks = /* @__PURE__ */ new WeakMap();
const getText = (n) => context.sourceCode.getText(n);
const onSetupFunctionEnter = (node) => {
setupFnRef.current = node;
};
const onSetupFunctionExit = (node) => {
if (setupFnRef.current === node) setupFnRef.current = null;
};
function isFunctionOfUseEffectSetup(node) {
return node.parent?.type === AST_NODE_TYPES.CallExpression && node.parent.callee !== node && isUseEffectLikeCall(node.parent);
}
function getCallName(node) {
if (node.type === AST_NODE_TYPES.CallExpression) return AST.toStringFormat(node.callee, getText);
return AST.toStringFormat(node, getText);
}
function getCallKind(node) {
return match(node).when(isUseStateCall, () => "useState").when(isUseEffectLikeCall, () => "useEffect").when(isSetStateCall, () => "setState").when(AST.isThenCall, () => "then").otherwise(() => "other");
}
function getFunctionKind(node) {
const parent = AST.findParentNode(node, not(AST.isTypeExpression)) ?? node.parent;
switch (true) {
case node.async:
case parent.type === AST_NODE_TYPES.CallExpression && AST.isThenCall(parent): return "deferred";
case node.type !== AST_NODE_TYPES.FunctionDeclaration && parent.type === AST_NODE_TYPES.CallExpression && parent.callee === node: return "immediate";
case isFunctionOfUseEffectSetup(node): return "setup";
default: return "other";
}
}
function isIdFromUseStateCall(topLevelId, at) {
const variableNode = getVariableDefinitionNode(findVariable(topLevelId, context.sourceCode.getScope(topLevelId)), 0);
if (variableNode == null) return false;
if (variableNode.type !== AST_NODE_TYPES.CallExpression) return false;
if (!isUseStateCall(variableNode)) return false;
const variableNodeParent = variableNode.parent;
if (!("id" in variableNodeParent) || variableNodeParent.id?.type !== AST_NODE_TYPES.ArrayPattern) return true;
return variableNodeParent.id.elements.findIndex((e) => e?.type === AST_NODE_TYPES.Identifier && e.name === topLevelId.name) === at;
}
function isSetStateCall(node) {
switch (node.callee.type) {
case AST_NODE_TYPES.CallExpression: {
const { callee } = node.callee;
if (callee.type !== AST_NODE_TYPES.MemberExpression) return false;
if (!("name" in callee.object)) return false;
const isAt = callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === "at";
const [index] = node.callee.arguments;
if (!isAt || index == null) return false;
return getStaticValue(index, context.sourceCode.getScope(node))?.value === 1 && isIdFromUseStateCall(callee.object);
}
case AST_NODE_TYPES.Identifier: return isIdFromUseStateCall(node.callee, 1);
case AST_NODE_TYPES.MemberExpression: {
if (!("name" in node.callee.object)) return false;
const property = node.callee.property;
return getStaticValue(property, context.sourceCode.getScope(node))?.value === 1 && isIdFromUseStateCall(node.callee.object, 1);
}
default: return false;
}
}
return {
":function"(node) {
const kind = getFunctionKind(node);
functionEntries.push({
kind,
node
});
if (kind === "setup") onSetupFunctionEnter(node);
},
":function:exit"(node) {
const { kind } = functionEntries.at(-1) ?? {};
if (kind === "setup") onSetupFunctionExit(node);
functionEntries.pop();
},
CallExpression(node) {
const setupFunction = setupFnRef.current;
const pEntry = functionEntries.at(-1);
if (pEntry == null || pEntry.node.async) return;
match(getCallKind(node)).with("setState", () => {
switch (true) {
case pEntry.kind === "deferred":
case pEntry.node.async: break;
case pEntry.node === setupFunction:
case pEntry.kind === "immediate" && AST.findParentNode(pEntry.node, AST.isFunction) === setupFunction:
context.report({
messageId: "noDirectSetStateInUseEffect",
node,
data: { name: context.sourceCode.getText(node.callee) }
});
return;
default: {
const init = AST.findParentNode(node, isVariableDeclaratorFromHookCall)?.init;
if (init == null) getOrElseUpdate(setStateCallsByFn, pEntry.node, () => []).push(node);
else getOrElseUpdate(setStateInHookCallbacks, init, () => []).push(node);
}
}
}).with("useEffect", () => {
if (AST.isFunction(node.arguments.at(0))) return;
setupFnIds.push(...AST.getNestedIdentifiers(node));
}).with("other", () => {
if (pEntry.node !== setupFunction) return;
trackedFnCalls.push(node);
}).otherwise(constVoid);
},
Identifier(node) {
if (node.parent.type === AST_NODE_TYPES.CallExpression && node.parent.callee === node) return;
if (!isIdFromUseStateCall(node, 1)) return;
switch (node.parent.type) {
case AST_NODE_TYPES.ArrowFunctionExpression: {
const parent = node.parent.parent;
if (parent.type !== AST_NODE_TYPES.CallExpression) break;
if (!isUseMemoCall(parent)) break;
const init = AST.findParentNode(parent, isVariableDeclaratorFromHookCall)?.init;
if (init != null) getOrElseUpdate(setStateInEffectArg, init, () => []).push(node);
break;
}
case AST_NODE_TYPES.CallExpression:
if (node !== node.parent.arguments.at(0)) break;
if (isUseCallbackCall(node.parent)) {
const init = AST.findParentNode(node.parent, isVariableDeclaratorFromHookCall)?.init;
if (init != null) getOrElseUpdate(setStateInEffectArg, init, () => []).push(node);
break;
}
if (isUseEffectLikeCall(node.parent)) getOrElseUpdate(setStateInEffectSetup, node.parent, () => []).push(node);
}
},
"Program:exit"() {
const getSetStateCalls = (id, initialScope) => {
const node = getVariableDefinitionNode(findVariable(id, initialScope), 0);
switch (node?.type) {
case AST_NODE_TYPES.ArrowFunctionExpression:
case AST_NODE_TYPES.FunctionDeclaration:
case AST_NODE_TYPES.FunctionExpression: return setStateCallsByFn.get(node) ?? [];
case AST_NODE_TYPES.CallExpression: return setStateInHookCallbacks.get(node) ?? setStateInEffectArg.get(node) ?? [];
}
return [];
};
for (const [, calls] of setStateInEffectSetup) for (const call of calls) context.report({
messageId: "noDirectSetStateInUseEffect",
node: call,
data: { name: call.name }
});
for (const { callee } of trackedFnCalls) {
if (!("name" in callee)) continue;
const { name: name$2 } = callee;
const setStateCalls = getSetStateCalls(name$2, context.sourceCode.getScope(callee));
for (const setStateCall of setStateCalls) context.report({
messageId: "noDirectSetStateInUseEffect",
node: setStateCall,
data: { name: getCallName(setStateCall) }
});
}
for (const id of setupFnIds) {
const setStateCalls = getSetStateCalls(id.name, context.sourceCode.getScope(id));
for (const setStateCall of setStateCalls) context.report({
messageId: "noDirectSetStateInUseEffect",
node: setStateCall,
data: { name: getCallName(setStateCall) }
});
}
}
};
}
//#endregion
//#region src/plugin.ts
const plugin = {
meta: {
name,
version
},
rules: { "no-direct-set-state-in-use-effect": no_direct_set_state_in_use_effect_default }
};
//#endregion
//#region src/index.ts
const { toFlatConfig } = getConfigAdapters("react-hooks-extra", plugin);
var src_default = {
...plugin,
configs: { ["recommended"]: toFlatConfig(recommended_exports) }
};
//#endregion
export { src_default as default };