UNPKG

eslint-plugin-react-hooks-extra

Version:

ESLint React's ESLint plugin for React Hooks related rules.

271 lines (262 loc) • 10.6 kB
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 };