UNPKG

eslint-plugin-react-hooks-extra

Version:

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

677 lines (667 loc) • 25 kB
import * as AST from '@eslint-react/ast'; import * as ER7 from '@eslint-react/core'; import { identity, _, getOrUpdate, constTrue } from '@eslint-react/eff'; import { getDocsUrl, getSettingsFromContext } from '@eslint-react/shared'; import * as VAR4 from '@eslint-react/var'; import { AST_NODE_TYPES } from '@typescript-eslint/types'; import { match } from 'ts-pattern'; import { ESLintUtils } from '@typescript-eslint/utils'; var __defProp = Object.defineProperty; var __export = (target, all) => { for (var name3 in all) __defProp(target, name3, { get: all[name3], enumerable: true }); }; // src/configs/recommended.ts var recommended_exports = {}; __export(recommended_exports, { name: () => name, rules: () => rules }); var name = "react-hooks-extra/recommended"; var rules = { "react-hooks-extra/hooks-extra/no-direct-set-state-in-use-effect": "warn", "react-hooks-extra/hooks-extra/no-unnecessary-use-prefix": "warn", "react-hooks-extra/hooks-extra/prefer-use-state-lazy-initialization": "warn" }; // package.json var name2 = "eslint-plugin-react-hooks-extra"; var version = "1.41.0"; var createRule = ESLintUtils.RuleCreator(getDocsUrl("hooks-extra")); function isFromHookCall(context, name3, settings, predicate = constTrue) { const hookAlias = settings.additionalHooks[name3] ?? []; return (topLevelId) => { const variable = VAR4.findVariable(topLevelId, context.sourceCode.getScope(topLevelId)); const variableNode = VAR4.getVariableInitNode(variable, 0); if (variableNode == null) return false; if (variableNode.type !== AST_NODE_TYPES.CallExpression) return false; if (!ER7.isReactHookCallWithNameAlias(context, name3, hookAlias)(variableNode)) return false; return predicate(topLevelId, variableNode); }; } function isFromUseStateCall(context, settings) { const predicate = (topLevelId, call) => { const { parent } = call; if (!("id" in parent) || parent.id?.type !== AST_NODE_TYPES.ArrayPattern) { return true; } return parent.id.elements.findIndex((e) => e?.type === AST_NODE_TYPES.Identifier && e.name === topLevelId.name) === 1; }; return isFromHookCall(context, "useState", settings, predicate); } function isFunctionOfImmediatelyInvoked(node) { return node.type !== AST_NODE_TYPES.FunctionDeclaration && node.parent.type === AST_NODE_TYPES.CallExpression && node.parent.callee === node; } function isSetFunctionCall(context, settings) { const isIdFromUseStateCall = isFromUseStateCall(context, settings); return (node) => { switch (node.callee.type) { // const data = useState(); // data.at(1)(); 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; } const indexScope = context.sourceCode.getScope(node); const indexValue = VAR4.toStaticValue({ kind: "lazy", node: index, initialScope: indexScope }).value; return indexValue === 1 && isIdFromUseStateCall(callee.object); } // const [data, setData] = useState(); // setData(); case AST_NODE_TYPES.Identifier: { return isIdFromUseStateCall(node.callee); } // const data = useState(); // data[1](); case AST_NODE_TYPES.MemberExpression: { if (!("name" in node.callee.object)) { return false; } const property = node.callee.property; const propertyScope = context.sourceCode.getScope(node); const propertyValue = VAR4.toStaticValue({ kind: "lazy", node: property, initialScope: propertyScope }).value; return propertyValue === 1 && isIdFromUseStateCall(node.callee.object); } default: { return false; } } }; } function isThenCall(node) { return node.callee.type === AST_NODE_TYPES.MemberExpression && node.callee.property.type === AST_NODE_TYPES.Identifier && node.callee.property.name === "then"; } function isVariableDeclaratorFromHookCall(node) { if (node.type !== AST_NODE_TYPES.VariableDeclarator) { return false; } if (node.id.type !== AST_NODE_TYPES.Identifier) { return false; } if (node.init?.type !== AST_NODE_TYPES.CallExpression) { return false; } switch (node.init.callee.type) { case AST_NODE_TYPES.Identifier: return ER7.isReactHookName(node.init.callee.name); case AST_NODE_TYPES.MemberExpression: return node.init.callee.property.type === AST_NODE_TYPES.Identifier && ER7.isReactHookName(node.init.callee.property.name); default: return false; } } // src/hooks/use-no-direct-set-state-in-use-effect.ts function useNoDirectSetStateInUseEffect(context, options) { const { onViolation, useEffectKind } = options; const settings = getSettingsFromContext(context); const additionalHooks = settings.additionalHooks; const isUseEffectLikeCall = ER7.isReactHookCallWithNameAlias(context, useEffectKind, additionalHooks[useEffectKind]); const isUseStateCall2 = ER7.isReactHookCallWithNameAlias(context, "useState", additionalHooks.useState); const isUseMemoCall2 = ER7.isReactHookCallWithNameAlias(context, "useMemo", additionalHooks.useMemo); const isUseCallbackCall2 = ER7.isReactHookCallWithNameAlias(context, "useCallback", additionalHooks.useCallback); const isSetStateCall = isSetFunctionCall(context, settings); const isIdFromUseStateCall = isFromUseStateCall(context, settings); const functionEntries = []; const setupFunctionRef = { current: null }; const setupFunctionIdentifiers = []; const indFunctionCalls = []; const indSetStateCalls = /* @__PURE__ */ new Map(); const indSetStateCallsInUseEffectArg0 = /* @__PURE__ */ new Map(); const indSetStateCallsInUseEffectSetup = /* @__PURE__ */ new Map(); const indSetStateCallsInUseMemoOrCallback = /* @__PURE__ */ new Map(); const onSetupFunctionEnter = (node) => { setupFunctionRef.current = node; }; const onSetupFunctionExit = (node) => { if (setupFunctionRef.current === node) { setupFunctionRef.current = null; } }; function isFunctionOfUseEffectSetup(node) { return node.parent?.type === AST_NODE_TYPES.CallExpression && node.parent.callee !== node && isUseEffectLikeCall(node.parent); } function getCallKind(node) { return match(node).when(isUseStateCall2, () => "useState").when(isUseEffectLikeCall, () => useEffectKind).when(isSetStateCall, () => "setState").when(isThenCall, () => "then").otherwise(() => "other"); } function getFunctionKind(node) { return match(node).when(isFunctionOfUseEffectSetup, () => "setup").when(isFunctionOfImmediatelyInvoked, () => "immediate").otherwise(() => "other"); } 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 = setupFunctionRef.current; const pEntry = functionEntries.at(-1); if (pEntry == null || pEntry.node.async) { return; } match(getCallKind(node)).with("setState", () => { switch (true) { case pEntry.node === setupFunction: case (pEntry.kind === "immediate" && AST.findParentNode(pEntry.node, AST.isFunction) === setupFunction): { onViolation(context, node, { name: context.sourceCode.getText(node.callee) }); return; } default: { const vd = AST.findParentNode(node, isVariableDeclaratorFromHookCall); if (vd == null) getOrUpdate(indSetStateCalls, pEntry.node, () => []).push(node); else getOrUpdate(indSetStateCallsInUseMemoOrCallback, vd.init, () => []).push(node); } } }).with(useEffectKind, () => { if (AST.isFunction(node.arguments.at(0))) return; setupFunctionIdentifiers.push(...AST.getNestedIdentifiers(node)); }).with("other", () => { if (pEntry.node !== setupFunction) return; indFunctionCalls.push(node); }).otherwise(() => _); }, Identifier(node) { if (node.parent.type === AST_NODE_TYPES.CallExpression && node.parent.callee === node) { return; } if (!isIdFromUseStateCall(node)) { return; } switch (node.parent.type) { case AST_NODE_TYPES.ArrowFunctionExpression: { const parent = node.parent.parent; if (parent.type !== AST_NODE_TYPES.CallExpression) { break; } if (!isUseMemoCall2(parent)) { break; } const vd = AST.findParentNode(parent, isVariableDeclaratorFromHookCall); if (vd != null) { getOrUpdate(indSetStateCallsInUseEffectArg0, vd.init, () => []).push(node); } break; } case AST_NODE_TYPES.CallExpression: { if (node !== node.parent.arguments.at(0)) { break; } if (isUseCallbackCall2(node.parent)) { const vd = AST.findParentNode(node.parent, isVariableDeclaratorFromHookCall); if (vd != null) { getOrUpdate(indSetStateCallsInUseEffectArg0, vd.init, () => []).push(node); } break; } if (isUseEffectLikeCall(node.parent)) { getOrUpdate(indSetStateCallsInUseEffectSetup, node.parent, () => []).push(node); } } } }, "Program:exit"() { const getSetStateCalls = (id, initialScope) => { const node = VAR4.getVariableInitNode(VAR4.findVariable(id, initialScope), 0); switch (node?.type) { case AST_NODE_TYPES.ArrowFunctionExpression: case AST_NODE_TYPES.FunctionDeclaration: case AST_NODE_TYPES.FunctionExpression: return indSetStateCalls.get(node) ?? []; case AST_NODE_TYPES.CallExpression: return indSetStateCallsInUseMemoOrCallback.get(node) ?? indSetStateCallsInUseEffectArg0.get(node) ?? []; } return []; }; for (const [, calls] of indSetStateCallsInUseEffectSetup) { for (const call of calls) { onViolation(context, call, { name: call.name }); } } for (const { callee } of indFunctionCalls) { if (!("name" in callee)) { continue; } const { name: name3 } = callee; const setStateCalls = getSetStateCalls(name3, context.sourceCode.getScope(callee)); for (const setStateCall of setStateCalls) { onViolation(context, setStateCall, { name: AST.toReadableNodeName(setStateCall, (n) => context.sourceCode.getText(n)) }); } } for (const id of setupFunctionIdentifiers) { const setStateCalls = getSetStateCalls(id.name, context.sourceCode.getScope(id)); for (const setStateCall of setStateCalls) { onViolation(context, setStateCall, { name: AST.toReadableNodeName(setStateCall, (n) => context.sourceCode.getText(n)) }); } } } }; } // src/rules/no-direct-set-state-in-use-effect.ts var RULE_NAME = "no-direct-set-state-in-use-effect"; var 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 {}; return useNoDirectSetStateInUseEffect(context, { onViolation(ctx, node, data) { ctx.report({ messageId: "noDirectSetStateInUseEffect", node, data }); }, useEffectKind: "useEffect" }); } // src/rules/no-direct-set-state-in-use-layout-effect.ts var RULE_NAME2 = "no-direct-set-state-in-use-layout-effect"; var RULE_FEATURES2 = [ "EXP" ]; var no_direct_set_state_in_use_layout_effect_default = createRule({ meta: { type: "problem", docs: { description: "Disallow direct calls to the `set` function of `useState` in `useLayoutEffect`.", [Symbol.for("rule_features")]: RULE_FEATURES2 }, messages: { noDirectSetStateInUseLayoutEffect: "Do not call the 'set' function '{{name}}' of 'useState' directly in 'useLayoutEffect'." }, schema: [] }, name: RULE_NAME2, create: create2, defaultOptions: [] }); function create2(context) { if (!/use\w*Effect/u.test(context.sourceCode.text)) return {}; return useNoDirectSetStateInUseEffect(context, { onViolation(ctx, node, data) { ctx.report({ messageId: "noDirectSetStateInUseLayoutEffect", node, data }); }, useEffectKind: "useLayoutEffect" }); } var RULE_NAME3 = "no-unnecessary-use-callback"; var RULE_FEATURES3 = [ "EXP" ]; var no_unnecessary_use_callback_default = createRule({ meta: { type: "problem", docs: { description: "Disallow unnecessary usage of `useCallback`.", [Symbol.for("rule_features")]: RULE_FEATURES3 }, messages: { noUnnecessaryUseCallback: "An 'useCallback' with empty deps and no references to the component scope may be unnecessary." }, schema: [] }, name: RULE_NAME3, create: create3, defaultOptions: [] }); function create3(context) { if (!context.sourceCode.text.includes("use")) return {}; const alias = getSettingsFromContext(context).additionalHooks.useCallback ?? []; return { CallExpression(node) { if (!ER7.isReactHookCall(node)) { return; } const initialScope = context.sourceCode.getScope(node); if (!ER7.isUseCallbackCall(context, node) && !alias.some(ER7.isReactHookCallWithNameLoose(node))) { return; } const scope = context.sourceCode.getScope(node); const component = scope.block; if (!AST.isFunction(component)) { return; } const [arg0, arg1] = node.arguments; if (arg0 == null || arg1 == null) { return; } const hasEmptyDeps = match(arg1).with({ type: AST_NODE_TYPES.ArrayExpression }, (n) => n.elements.length === 0).with({ type: AST_NODE_TYPES.Identifier }, (n) => { const variable = VAR4.findVariable(n.name, initialScope); const variableNode = VAR4.getVariableInitNode(variable, 0); if (variableNode?.type !== AST_NODE_TYPES.ArrayExpression) { return false; } return variableNode.elements.length === 0; }).otherwise(() => false); if (!hasEmptyDeps) { return; } const arg0Node = match(arg0).with({ type: AST_NODE_TYPES.ArrowFunctionExpression }, (n) => { if (n.body.type === AST_NODE_TYPES.ArrowFunctionExpression) { return n.body; } return n; }).with({ type: AST_NODE_TYPES.FunctionExpression }, identity).with({ type: AST_NODE_TYPES.Identifier }, (n) => { const variable = VAR4.findVariable(n.name, initialScope); const variableNode = VAR4.getVariableInitNode(variable, 0); if (variableNode?.type !== AST_NODE_TYPES.ArrowFunctionExpression && variableNode?.type !== AST_NODE_TYPES.FunctionExpression) { return _; } return variableNode; }).otherwise(() => _); if (arg0Node == null) return; const arg0NodeScope = context.sourceCode.getScope(arg0Node); const arg0NodeReferences = VAR4.getChidScopes(arg0NodeScope).flatMap((x) => x.references); const isReferencedToComponentScope = arg0NodeReferences.some((x) => x.resolved?.scope.block === component); if (!isReferencedToComponentScope) { context.report({ messageId: "noUnnecessaryUseCallback", node }); } } }; } var RULE_NAME4 = "no-unnecessary-use-memo"; var RULE_FEATURES4 = [ "EXP" ]; var no_unnecessary_use_memo_default = createRule({ meta: { type: "problem", docs: { description: "Disallow unnecessary usage of `useMemo`.", [Symbol.for("rule_features")]: RULE_FEATURES4 }, messages: { noUnnecessaryUseMemo: "An 'useMemo' with empty deps and no references to the component scope may be unnecessary." }, schema: [] }, name: RULE_NAME4, create: create4, defaultOptions: [] }); function create4(context) { if (!context.sourceCode.text.includes("use")) return {}; const alias = getSettingsFromContext(context).additionalHooks.useMemo ?? []; return { CallExpression(node) { if (!ER7.isReactHookCall(node)) { return; } const initialScope = context.sourceCode.getScope(node); if (!ER7.isUseMemoCall(context, node) && !alias.some(ER7.isReactHookCallWithNameLoose(node))) { return; } const scope = context.sourceCode.getScope(node); const component = scope.block; if (!AST.isFunction(component)) { return; } const [arg0, arg1] = node.arguments; if (arg0 == null || arg1 == null) { return; } const hasCallInArg0 = AST.isFunction(arg0) && [...AST.getNestedCallExpressions(arg0.body), ...AST.getNestedNewExpressions(arg0.body)].length > 0; if (hasCallInArg0) { return; } const hasEmptyDeps = match(arg1).with({ type: AST_NODE_TYPES.ArrayExpression }, (n) => n.elements.length === 0).with({ type: AST_NODE_TYPES.Identifier }, (n) => { const variable = VAR4.findVariable(n.name, initialScope); const variableNode = VAR4.getVariableInitNode(variable, 0); if (variableNode?.type !== AST_NODE_TYPES.ArrayExpression) { return false; } return variableNode.elements.length === 0; }).otherwise(() => false); if (!hasEmptyDeps) { return; } const arg0Node = match(arg0).with({ type: AST_NODE_TYPES.ArrowFunctionExpression }, (n) => { if (n.body.type === AST_NODE_TYPES.ArrowFunctionExpression) { return n.body; } return n; }).with({ type: AST_NODE_TYPES.FunctionExpression }, identity).with({ type: AST_NODE_TYPES.Identifier }, (n) => { const variable = VAR4.findVariable(n.name, initialScope); const variableNode = VAR4.getVariableInitNode(variable, 0); if (variableNode?.type !== AST_NODE_TYPES.ArrowFunctionExpression && variableNode?.type !== AST_NODE_TYPES.FunctionExpression) { return _; } return variableNode; }).otherwise(() => _); if (arg0Node == null) return; const arg0NodeScope = context.sourceCode.getScope(arg0Node); const arg0NodeReferences = VAR4.getChidScopes(arg0NodeScope).flatMap((x) => x.references); const isReferencedToComponentScope = arg0NodeReferences.some((x) => x.resolved?.scope.block === component); if (!isReferencedToComponentScope) { context.report({ messageId: "noUnnecessaryUseMemo", node }); } } }; } var RULE_NAME5 = "no-unnecessary-use-prefix"; var RULE_FEATURES5 = []; function isNodeContainsUseCallComments(context, node) { return context.sourceCode.getCommentsInside(node).some((comment) => /use\w+\(/u.test(comment.value)); } var no_unnecessary_use_prefix_default = createRule({ meta: { type: "problem", docs: { description: "Enforces that a function with the `use` prefix should use at least one Hook inside of it.", [Symbol.for("rule_features")]: RULE_FEATURES5 }, messages: { noUnnecessaryUsePrefix: "If your function doesn't call any Hooks, avoid the 'use' prefix. Instead, write it as a regular function without the 'use' prefix." }, schema: [] }, name: RULE_NAME5, create: create5, defaultOptions: [] }); function create5(context) { const { ctx, listeners } = ER7.useHookCollector(); return { ...listeners, "Program:exit"(node) { const allHooks = ctx.getAllHooks(node); for (const { name: name3, node: node2, hookCalls } of allHooks.values()) { if (AST.isEmptyFunction(node2)) { continue; } if (hookCalls.length > 0) { continue; } if (isNodeContainsUseCallComments(context, node2)) { continue; } context.report({ messageId: "noUnnecessaryUsePrefix", node: node2, data: { name: name3 } }); } } }; } var RULE_NAME6 = "prefer-use-state-lazy-initialization"; var RULE_FEATURES6 = [ "EXP" ]; var ALLOW_LIST = [ "Boolean", "String", "Number" ]; var prefer_use_state_lazy_initialization_default = createRule({ meta: { type: "problem", docs: { description: "Enforces function calls made inside `useState` to be wrapped in an `initializer function`.", [Symbol.for("rule_features")]: RULE_FEATURES6 }, messages: { preferUseStateLazyInitialization: "To prevent re-computation, consider using lazy initial state for useState calls that involve function calls. Ex: 'useState(() => getValue())'." }, schema: [] }, name: RULE_NAME6, create: create6, defaultOptions: [] }); function create6(context) { if (!context.sourceCode.text.includes("use")) return {}; const alias = getSettingsFromContext(context).additionalHooks.useState ?? []; return { CallExpression(node) { if (!ER7.isReactHookCall(node)) { return; } if (!ER7.isUseStateCall(context, node) && !alias.some(ER7.isReactHookCallWithNameLoose(node))) { return; } const [useStateInput] = node.arguments; if (useStateInput == null) { return; } for (const expr of AST.getNestedNewExpressions(useStateInput)) { if (!("name" in expr.callee)) continue; if (ALLOW_LIST.includes(expr.callee.name)) continue; if (AST.findParentNode(expr, (n) => ER7.isUseCall(context, n)) != null) continue; context.report({ messageId: "preferUseStateLazyInitialization", node: expr }); } for (const expr of AST.getNestedCallExpressions(useStateInput)) { if (!("name" in expr.callee)) continue; if (ER7.isReactHookNameLoose(expr.callee.name)) continue; if (ALLOW_LIST.includes(expr.callee.name)) continue; if (AST.findParentNode(expr, (n) => ER7.isUseCall(context, n)) != null) continue; context.report({ messageId: "preferUseStateLazyInitialization", node: expr }); } } }; } // src/plugin.ts var plugin = { meta: { name: name2, version }, rules: { "no-direct-set-state-in-use-effect": no_direct_set_state_in_use_effect_default, "no-direct-set-state-in-use-layout-effect": no_direct_set_state_in_use_layout_effect_default, "no-unnecessary-use-callback": no_unnecessary_use_callback_default, "no-unnecessary-use-memo": no_unnecessary_use_memo_default, "no-unnecessary-use-prefix": no_unnecessary_use_prefix_default, "prefer-use-state-lazy-initialization": prefer_use_state_lazy_initialization_default, // Part: deprecated rules /** @deprecated Use `no-unnecessary-use-prefix` instead */ "ensure-custom-hooks-using-other-hooks": no_unnecessary_use_prefix_default, /** @deprecated Use `no-unnecessary-use-callback` instead */ "ensure-use-callback-has-non-empty-deps": no_unnecessary_use_callback_default, /** @deprecated Use `no-unnecessary-use-memo` instead */ "ensure-use-memo-has-non-empty-deps": no_unnecessary_use_memo_default, /** @deprecated Use `no-unnecessary-use-prefix` instead */ "no-redundant-custom-hook": no_unnecessary_use_prefix_default, /** @deprecated Use `no-unnecessary-use-prefix` instead */ "no-useless-custom-hooks": no_unnecessary_use_prefix_default } }; // src/index.ts function makeConfig(config) { return { ...config, plugins: { "react-hooks-extra": plugin } }; } function makeLegacyConfig({ rules: rules2 }) { return { plugins: ["react-hooks-extra"], rules: rules2 }; } var index_default = { ...plugin, configs: { ["recommended"]: makeConfig(recommended_exports), ["recommended-legacy"]: makeLegacyConfig(recommended_exports) } }; export { index_default as default };