diff --git a/packages/core/docs/functions/isReactHookCallWithNameAlias.md b/packages/core/docs/functions/isReactHookCallWithNameAlias.md index af985a6245..cf70c837a1 100644 --- a/packages/core/docs/functions/isReactHookCallWithNameAlias.md +++ b/packages/core/docs/functions/isReactHookCallWithNameAlias.md @@ -20,7 +20,7 @@ ### alias -`string`[] +`undefined` | `string`[] ## Returns diff --git a/packages/core/src/hook/is.ts b/packages/core/src/hook/is.ts index b13bae6f96..55d6680476 100644 --- a/packages/core/src/hook/is.ts +++ b/packages/core/src/hook/is.ts @@ -71,7 +71,7 @@ export function isReactHookCallWithNameLoose(node: TSESTree.CallExpression | _) }; } -export function isReactHookCallWithNameAlias(context: RuleContext, name: string, alias: string[]) { +export function isReactHookCallWithNameAlias(context: RuleContext, name: string, alias: _ | string[] = []) { const { importSource = DEFAULT_ESLINT_REACT_SETTINGS.importSource, skipImportCheck = true, diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/hooks/use-no-direct-set-state-in-use-effect.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/hooks/use-no-direct-set-state-in-use-effect.ts new file mode 100644 index 0000000000..d391b92b02 --- /dev/null +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/hooks/use-no-direct-set-state-in-use-effect.ts @@ -0,0 +1,224 @@ +import * as AST from "@eslint-react/ast"; +import { isReactHookCallWithNameAlias } from "@eslint-react/core"; +import { _, getOrUpdate } from "@eslint-react/eff"; +import { getSettingsFromContext, type RuleContext } from "@eslint-react/shared"; +import * as VAR from "@eslint-react/var"; +import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; +import type { ESLintUtils, TSESTree } from "@typescript-eslint/utils"; +import type { Scope } from "@typescript-eslint/utils/ts-eslint"; +import { match } from "ts-pattern"; + +import { + isFromUseStateCall, + isFunctionOfImmediatelyInvoked, + isSetFunctionCall, + isThenCall, + isVariableDeclaratorFromHookCall, +} from "../utils"; + +type CallKind = "other" | "setState" | "then" | "useEffect" | "useLayoutEffect" | "useState"; +type FunctionKind = "cleanup" | "deferred" | "immediate" | "other" | "setup"; + +export declare namespace useNoDirectSetStateInUseEffect { + type Options = { + onViolation: (context: Ctx, node: TSESTree.Node | TSESTree.Token, data: { name: string }) => void; + useEffectKind: "useEffect" | "useLayoutEffect"; + }; + type ReturnType = ESLintUtils.RuleListener; +} + +export function useNoDirectSetStateInUseEffect( + context: Ctx, + options: useNoDirectSetStateInUseEffect.Options, +): useNoDirectSetStateInUseEffect.ReturnType { + const { onViolation, useEffectKind } = options; + const settings = getSettingsFromContext(context); + const additionalHooks = settings.additionalHooks; + const isUseEffectLikeCall = isReactHookCallWithNameAlias(context, useEffectKind, additionalHooks[useEffectKind]); + const isUseStateCall = isReactHookCallWithNameAlias(context, "useState", additionalHooks.useState); + const isUseMemoCall = isReactHookCallWithNameAlias(context, "useMemo", additionalHooks.useMemo); + const isUseCallbackCall = isReactHookCallWithNameAlias(context, "useCallback", additionalHooks.useCallback); + const isSetStateCall = isSetFunctionCall(context, settings); + const isIdFromUseStateCall = isFromUseStateCall(context, settings); + + const functionEntries: { kind: FunctionKind; node: AST.TSESTreeFunction }[] = []; + const setupFunctionRef: { current: AST.TSESTreeFunction | null } = { current: null }; + const setupFunctionIdentifiers: TSESTree.Identifier[] = []; + + const indFunctionCalls: TSESTree.CallExpression[] = []; + const indSetStateCalls = new Map(); + const indSetStateCallsInUseEffectArg0 = new Map(); + const indSetStateCallsInUseEffectSetup = new Map(); + const indSetStateCallsInUseMemoOrCallback = new Map(); + + const onSetupFunctionEnter = (node: AST.TSESTreeFunction) => { + setupFunctionRef.current = node; + }; + + const onSetupFunctionExit = (node: AST.TSESTreeFunction) => { + if (setupFunctionRef.current === node) { + setupFunctionRef.current = null; + } + }; + + function isFunctionOfUseEffectSetup(node: TSESTree.Node) { + return node.parent?.type === T.CallExpression + && node.parent.callee !== node + && isUseEffectLikeCall(node.parent); + } + + function getCallKind(node: TSESTree.CallExpression) { + return match(node) + .when(isUseStateCall, () => "useState") + .when(isUseEffectLikeCall, () => useEffectKind) + .when(isSetStateCall, () => "setState") + .when(isThenCall, () => "then") + .otherwise(() => "other"); + } + + function getFunctionKind(node: AST.TSESTreeFunction) { + return match(node) + .when(isFunctionOfUseEffectSetup, () => "setup") + .when(isFunctionOfImmediatelyInvoked, () => "immediate") + .otherwise(() => "other"); + } + + return { + ":function"(node: AST.TSESTreeFunction) { + const kind = getFunctionKind(node); + functionEntries.push({ kind, node }); + if (kind === "setup") { + onSetupFunctionEnter(node); + } + }, + ":function:exit"(node: AST.TSESTreeFunction) { + 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 + || pEntry.kind === "immediate": { + 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 === T.CallExpression && node.parent.callee === node) { + return; + } + if (!isIdFromUseStateCall(node)) { + return; + } + switch (node.parent.type) { + case T.ArrowFunctionExpression: { + const parent = node.parent.parent; + if (parent.type !== T.CallExpression) { + break; + } + // const [state, setState] = useState(); + // const set = useMemo(() => setState, []); + // useEffect(set, []); + if (!isUseMemoCall(parent)) { + break; + } + const vd = AST.findParentNode(parent, isVariableDeclaratorFromHookCall); + if (vd != null) { + getOrUpdate(indSetStateCallsInUseEffectArg0, vd.init, () => []).push(node); + } + break; + } + case T.CallExpression: { + if (node !== node.parent.arguments.at(0)) { + break; + } + // const [state, setState] = useState(); + // const set = useCallback(setState, []); + // useEffect(set, []); + if (isUseCallbackCall(node.parent)) { + const vd = AST.findParentNode(node.parent, isVariableDeclaratorFromHookCall); + if (vd != null) { + getOrUpdate(indSetStateCallsInUseEffectArg0, vd.init, () => []).push(node); + } + break; + } + // const [state, setState] = useState(); + // useEffect(setState); + if (isUseEffectLikeCall(node.parent)) { + getOrUpdate(indSetStateCallsInUseEffectSetup, node.parent, () => []).push(node); + } + } + } + }, + "Program:exit"() { + const getSetStateCalls = ( + id: string | TSESTree.Identifier, + initialScope: Scope.Scope, + ): TSESTree.CallExpression[] | TSESTree.Identifier[] => { + const node = VAR.getVariableNode(VAR.findVariable(id, initialScope), 0); + switch (node?.type) { + case T.ArrowFunctionExpression: + case T.FunctionDeclaration: + case T.FunctionExpression: + return indSetStateCalls.get(node) ?? []; + case T.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 } = callee; + const setStateCalls = getSetStateCalls(name, 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)), + }); + } + } + }, + }; +} diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-effect.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-effect.ts index cc0f532968..355338e122 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-effect.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-effect.ts @@ -1,23 +1,8 @@ -import * as AST from "@eslint-react/ast"; -import { isReactHookCallWithNameAlias } from "@eslint-react/core"; -import { _, getOrUpdate } from "@eslint-react/eff"; import type { RuleFeature } from "@eslint-react/shared"; -import { getSettingsFromContext } from "@eslint-react/shared"; -import * as VAR from "@eslint-react/var"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; -import type { TSESTree } from "@typescript-eslint/utils"; -import type { Scope } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; -import { match } from "ts-pattern"; -import { - createRule, - isFromUseStateCall, - isFunctionOfImmediatelyInvoked, - isSetFunctionCall, - isThenCall, - isVariableDeclaratorFromHookCall, -} from "../utils"; +import { useNoDirectSetStateInUseEffect } from "../hooks/use-no-direct-set-state-in-use-effect"; +import { createRule } from "../utils"; export const RULE_NAME = "no-direct-set-state-in-use-effect"; @@ -26,8 +11,6 @@ export const RULE_FEATURES = [ ] as const satisfies RuleFeature[]; type MessageID = CamelCase; -type CallKind = "other" | "setState" | "then" | "useEffect" | "useState"; -type FunctionKind = "cleanup" | "deferred" | "immediate" | "other" | "setup"; export default createRule<[], MessageID>({ meta: { @@ -43,211 +26,13 @@ export default createRule<[], MessageID>({ }, name: RULE_NAME, create(context) { - if (!/use\w*Effect/u.test(context.sourceCode.text)) { - return {}; - } - const settings = getSettingsFromContext(context); - const additionalHooks = settings.additionalHooks; - - const isUseEffectLikeCall = isReactHookCallWithNameAlias(context, "useEffect", additionalHooks.useEffect ?? []); - const isUseStateCall = isReactHookCallWithNameAlias(context, "useState", additionalHooks.useState ?? []); - const isUseMemoCall = isReactHookCallWithNameAlias(context, "useMemo", additionalHooks.useMemo ?? []); - const isUseCallbackCall = isReactHookCallWithNameAlias(context, "useCallback", additionalHooks.useCallback ?? []); - const isSetStateCall = isSetFunctionCall(context, settings); - const isIdFromUseStateCall = isFromUseStateCall(context, settings); - - const functionEntries: { kind: FunctionKind; node: AST.TSESTreeFunction }[] = []; - const setupFunctionRef: { current: AST.TSESTreeFunction | null } = { current: null }; - const setupFunctionIdentifiers: TSESTree.Identifier[] = []; - - const indFunctionCalls: TSESTree.CallExpression[] = []; - const indSetStateCalls = new Map(); - const indSetStateCallsInUseEffectArg0 = new Map(); - const indSetStateCallsInUseEffectSetup = new Map(); - const indSetStateCallsInUseMemoOrCallback = new Map(); - - const onSetupFunctionEnter = (node: AST.TSESTreeFunction) => { - setupFunctionRef.current = node; - }; - const onSetupFunctionExit = (node: AST.TSESTreeFunction) => { - if (setupFunctionRef.current === node) { - setupFunctionRef.current = null; - } - }; - - function isFunctionOfUseEffectSetup(node: TSESTree.Node) { - return node.parent?.type === T.CallExpression - && node.parent.callee !== node - && isUseEffectLikeCall(node.parent); - } - function getCallKind(node: TSESTree.CallExpression) { - return match(node) - .when(isUseStateCall, () => "useState") - .when(isUseEffectLikeCall, () => "useEffect") - .when(isSetStateCall, () => "setState") - .when(isThenCall, () => "then") - .otherwise(() => "other"); - } - function getFunctionKind(node: AST.TSESTreeFunction) { - return match(node) - .when(isFunctionOfUseEffectSetup, () => "setup") - .when(isFunctionOfImmediatelyInvoked, () => "immediate") - .otherwise(() => "other"); - } - return { - ":function"(node: AST.TSESTreeFunction) { - const kind = getFunctionKind(node); - functionEntries.push({ kind, node }); - if (kind === "setup") { - onSetupFunctionEnter(node); - } - }, - ":function:exit"(node: AST.TSESTreeFunction) { - 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 - || pEntry.kind === "immediate": { - context.report({ - messageId: "noDirectSetStateInUseEffect", - node, - data: { - 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("useEffect", () => { - 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 === T.CallExpression && node.parent.callee === node) { - return; - } - if (!isIdFromUseStateCall(node)) { - return; - } - switch (node.parent.type) { - case T.ArrowFunctionExpression: { - const parent = node.parent.parent; - if (parent.type !== T.CallExpression) { - break; - } - // const [state, setState] = useState(); - // const set = useMemo(() => setState, []); - // useEffect(set, []); - if (!isUseMemoCall(parent)) { - break; - } - const vd = AST.findParentNode(parent, isVariableDeclaratorFromHookCall); - if (vd != null) { - getOrUpdate(indSetStateCallsInUseEffectArg0, vd.init, () => []).push(node); - } - break; - } - case T.CallExpression: { - if (node !== node.parent.arguments.at(0)) { - break; - } - // const [state, setState] = useState(); - // const set = useCallback(setState, []); - // useEffect(set, []); - if (isUseCallbackCall(node.parent)) { - const vd = AST.findParentNode(node.parent, isVariableDeclaratorFromHookCall); - if (vd != null) { - getOrUpdate(indSetStateCallsInUseEffectArg0, vd.init, () => []).push(node); - } - break; - } - // const [state, setState] = useState(); - // useEffect(setState); - if (isUseEffectLikeCall(node.parent)) { - getOrUpdate(indSetStateCallsInUseEffectSetup, node.parent, () => []).push(node); - } - } - } - }, - "Program:exit"() { - const getSetStateCalls = ( - id: string | TSESTree.Identifier, - initialScope: Scope.Scope, - ): TSESTree.CallExpression[] | TSESTree.Identifier[] => { - const node = VAR.getVariableNode(VAR.findVariable(id, initialScope), 0); - switch (node?.type) { - case T.ArrowFunctionExpression: - case T.FunctionDeclaration: - case T.FunctionExpression: - return indSetStateCalls.get(node) ?? []; - case T.CallExpression: - return indSetStateCallsInUseMemoOrCallback.get(node) ?? indSetStateCallsInUseEffectArg0.get(node) ?? []; - } - return []; - }; - for (const [, calls] of indSetStateCallsInUseEffectSetup) { - for (const call of calls) { - context.report({ - messageId: "noDirectSetStateInUseEffect", - node: call, - data: { name: call.name }, - }); - } - } - for (const { callee } of indFunctionCalls) { - if (!("name" in callee)) { - continue; - } - const { name } = callee; - const setStateCalls = getSetStateCalls(name, context.sourceCode.getScope(callee)); - for (const setStateCall of setStateCalls) { - context.report({ - messageId: "noDirectSetStateInUseEffect", - node: setStateCall, - data: { - 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) { - context.report({ - messageId: "noDirectSetStateInUseEffect", - node: setStateCall, - data: { - name: AST.toReadableNodeName(setStateCall, (n) => context.sourceCode.getText(n)), - }, - }); - } - } + 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", + }); }, defaultOptions: [], }); diff --git a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-layout-effect.ts b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-layout-effect.ts index 171f9daca2..768803d2a6 100644 --- a/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-layout-effect.ts +++ b/packages/plugins/eslint-plugin-react-hooks-extra/src/rules/no-direct-set-state-in-use-layout-effect.ts @@ -1,23 +1,8 @@ -import * as AST from "@eslint-react/ast"; -import { isReactHookCallWithNameAlias } from "@eslint-react/core"; -import { _, getOrUpdate } from "@eslint-react/eff"; import type { RuleFeature } from "@eslint-react/shared"; -import { getSettingsFromContext } from "@eslint-react/shared"; -import * as VAR from "@eslint-react/var"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; -import type { TSESTree } from "@typescript-eslint/utils"; -import type { Scope } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; -import { match } from "ts-pattern"; -import { - createRule, - isFromUseStateCall, - isFunctionOfImmediatelyInvoked, - isSetFunctionCall, - isThenCall, - isVariableDeclaratorFromHookCall, -} from "../utils"; +import { useNoDirectSetStateInUseEffect } from "../hooks/use-no-direct-set-state-in-use-effect"; +import { createRule } from "../utils"; export const RULE_NAME = "no-direct-set-state-in-use-layout-effect"; @@ -26,8 +11,6 @@ export const RULE_FEATURES = [ ] as const satisfies RuleFeature[]; type MessageID = CamelCase; -type CallKind = "other" | "setState" | "then" | "useLayoutEffect" | "useState"; -type FunctionKind = "cleanup" | "deferred" | "immediate" | "other" | "setup"; export default createRule<[], MessageID>({ meta: { @@ -44,216 +27,13 @@ export default createRule<[], MessageID>({ }, name: RULE_NAME, create(context) { - if (!/use\w*Effect/u.test(context.sourceCode.text)) { - return {}; - } - const settings = getSettingsFromContext(context); - const additionalHooks = settings.additionalHooks; - - const isUseLayoutEffectLikeCall = isReactHookCallWithNameAlias( - context, - "useLayoutEffect", - additionalHooks.useLayoutEffect ?? [], - ); - const isUseStateCall = isReactHookCallWithNameAlias(context, "useState", additionalHooks.useState ?? []); - const isUseMemoCall = isReactHookCallWithNameAlias(context, "useMemo", additionalHooks.useMemo ?? []); - const isUseCallbackCall = isReactHookCallWithNameAlias(context, "useCallback", additionalHooks.useCallback ?? []); - const isSetStateCall = isSetFunctionCall(context, settings); - const isIdFromUseStateCall = isFromUseStateCall(context, settings); - - const functionEntries: { kind: FunctionKind; node: AST.TSESTreeFunction }[] = []; - const setupFunctionRef: { current: AST.TSESTreeFunction | null } = { current: null }; - const setupFunctionIdentifiers: TSESTree.Identifier[] = []; - - const indFunctionCalls: TSESTree.CallExpression[] = []; - const indSetStateCalls = new Map(); - const indSetStateCallsInUseLayoutEffectArg0 = new Map(); - const indSetStateCallsInUseLayoutEffectSetup = new Map(); - const indSetStateCallsInUseMemoOrCallback = new Map(); - - const onSetupFunctionEnter = (node: AST.TSESTreeFunction) => { - setupFunctionRef.current = node; - }; - const onSetupFunctionExit = (node: AST.TSESTreeFunction) => { - if (setupFunctionRef.current === node) { - setupFunctionRef.current = null; - } - }; - - function isFunctionOfUseEffectSetup(node: TSESTree.Node) { - return node.parent?.type === T.CallExpression - && node.parent.callee !== node - && isUseLayoutEffectLikeCall(node.parent); - } - function getCallKind(node: TSESTree.CallExpression) { - return match(node) - .when(isUseStateCall, () => "useState") - .when(isUseLayoutEffectLikeCall, () => "useLayoutEffect") - .when(isSetStateCall, () => "setState") - .when(isThenCall, () => "then") - .otherwise(() => "other"); - } - function getFunctionKind(node: AST.TSESTreeFunction) { - return match(node) - .when(isFunctionOfUseEffectSetup, () => "setup") - .when(isFunctionOfImmediatelyInvoked, () => "immediate") - .otherwise(() => "other"); - } - return { - ":function"(node: AST.TSESTreeFunction) { - const kind = getFunctionKind(node); - functionEntries.push({ kind, node }); - if (kind === "setup") { - onSetupFunctionEnter(node); - } - }, - ":function:exit"(node: AST.TSESTreeFunction) { - 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 - || pEntry.kind === "immediate": { - context.report({ - messageId: "noDirectSetStateInUseLayoutEffect", - node, - data: { - 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("useLayoutEffect", () => { - 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 === T.CallExpression && node.parent.callee === node) { - return; - } - if (!isIdFromUseStateCall(node)) { - return; - } - switch (node.parent.type) { - case T.ArrowFunctionExpression: { - const parent = node.parent.parent; - if (parent.type !== T.CallExpression) { - break; - } - // const [state, setState] = useState(); - // const set = useMemo(() => setState, []); - // useLayoutEffect(set, []); - if (!isUseMemoCall(parent)) { - break; - } - const vd = AST.findParentNode(parent, isVariableDeclaratorFromHookCall); - if (vd != null) { - getOrUpdate(indSetStateCallsInUseLayoutEffectArg0, vd.init, () => []).push(node); - } - break; - } - case T.CallExpression: { - if (node !== node.parent.arguments.at(0)) { - break; - } - // const [state, setState] = useState(); - // const set = useCallback(setState, []); - // useLayoutEffect(set, []); - if (isUseCallbackCall(node.parent)) { - const vd = AST.findParentNode(node.parent, isVariableDeclaratorFromHookCall); - if (vd != null) { - getOrUpdate(indSetStateCallsInUseLayoutEffectArg0, vd.init, () => []).push(node); - } - break; - } - // const [state, setState] = useState(); - // useLayoutEffect(setState); - if (isUseLayoutEffectLikeCall(node.parent)) { - getOrUpdate(indSetStateCallsInUseLayoutEffectSetup, node.parent, () => []).push(node); - } - } - } - }, - "Program:exit"() { - const getSetStateCalls = ( - id: string | TSESTree.Identifier, - initialScope: Scope.Scope, - ): TSESTree.CallExpression[] | TSESTree.Identifier[] => { - const node = VAR.getVariableNode(VAR.findVariable(id, initialScope), 0); - switch (node?.type) { - case T.ArrowFunctionExpression: - case T.FunctionDeclaration: - case T.FunctionExpression: - return indSetStateCalls.get(node) ?? []; - case T.CallExpression: - return indSetStateCallsInUseMemoOrCallback.get(node) ?? indSetStateCallsInUseLayoutEffectArg0.get(node) - ?? []; - } - return []; - }; - for (const [, calls] of indSetStateCallsInUseLayoutEffectSetup) { - for (const call of calls) { - context.report({ - messageId: "noDirectSetStateInUseLayoutEffect", - node: call, - data: { name: call.name }, - }); - } - } - for (const { callee } of indFunctionCalls) { - if (!("name" in callee)) { - continue; - } - const { name } = callee; - const setStateCalls = getSetStateCalls(name, context.sourceCode.getScope(callee)); - for (const setStateCall of setStateCalls) { - context.report({ - messageId: "noDirectSetStateInUseLayoutEffect", - node: setStateCall, - data: { - 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) { - context.report({ - messageId: "noDirectSetStateInUseLayoutEffect", - node: setStateCall, - data: { - name: AST.toReadableNodeName(setStateCall, (n) => context.sourceCode.getText(n)), - }, - }); - } - } + 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", + }); }, defaultOptions: [], });