eslint-plugin-react-hooks-extra
Version:
ESLint React's ESLint plugin for React Hooks related rules.
677 lines (667 loc) • 25 kB
JavaScript
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 };