From 5991d775c53fa3fb521f3de3a06d59e256c47632 Mon Sep 17 00:00:00 2001 From: Rel1cx Date: Wed, 30 Apr 2025 03:35:16 +0800 Subject: [PATCH] refactor: code optimizations --- .../use-no-direct-set-state-in-use-effect.ts | 240 ------------- .../no-direct-set-state-in-use-effect.ts | 325 +++++++++++++++++- ...o-direct-set-state-in-use-layout-effect.ts | 2 +- .../eslint-plugin-react-x/src/utils/index.ts | 4 - .../src/utils/is-from-hook-call.ts | 26 -- .../src/utils/is-from-use-state-call.ts | 18 - .../src/utils/is-set-function-call.ts | 62 ---- .../is-variable-declarator-from-hook-call.ts | 29 -- .../utilities/ast/src/ast-array-pattern.ts | 13 + packages/utilities/ast/src/index.ts | 1 + 10 files changed, 339 insertions(+), 381 deletions(-) delete mode 100644 packages/plugins/eslint-plugin-react-x/src/hooks/use-no-direct-set-state-in-use-effect.ts delete mode 100644 packages/plugins/eslint-plugin-react-x/src/utils/is-from-hook-call.ts delete mode 100644 packages/plugins/eslint-plugin-react-x/src/utils/is-from-use-state-call.ts delete mode 100644 packages/plugins/eslint-plugin-react-x/src/utils/is-set-function-call.ts delete mode 100644 packages/plugins/eslint-plugin-react-x/src/utils/is-variable-declarator-from-hook-call.ts create mode 100644 packages/utilities/ast/src/ast-array-pattern.ts diff --git a/packages/plugins/eslint-plugin-react-x/src/hooks/use-no-direct-set-state-in-use-effect.ts b/packages/plugins/eslint-plugin-react-x/src/hooks/use-no-direct-set-state-in-use-effect.ts deleted file mode 100644 index 11ec4c0e18..0000000000 --- a/packages/plugins/eslint-plugin-react-x/src/hooks/use-no-direct-set-state-in-use-effect.ts +++ /dev/null @@ -1,240 +0,0 @@ -import type { RuleContext } from "@eslint-react/kit"; -import type { ESLintUtils, TSESTree } from "@typescript-eslint/utils"; -import type { Scope } from "@typescript-eslint/utils/ts-eslint"; -import * as AST from "@eslint-react/ast"; -import * as ER from "@eslint-react/core"; -import { constVoid, getOrElseUpdate } from "@eslint-react/eff"; -import { getSettingsFromContext } from "@eslint-react/shared"; -import * as VAR from "@eslint-react/var"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; -import { match } from "ts-pattern"; - -import { isFromUseStateCall, isSetFunctionCall, isVariableDeclaratorFromHookCall } from "../utils"; - -type CallKind = - | "useEffect" - | "useLayoutEffect" - | "useState" - | "setState" - | "then" - | "other"; - -type FunctionKind = - | "setup" - | "cleanup" - | "deferred" - | "immediate" - | "other"; - -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 hooks = settings.additionalHooks; - const getText = (n: TSESTree.Node) => context.sourceCode.getText(n); - const isUseEffectLikeCall = ER.isReactHookCallWithNameAlias(context, useEffectKind, hooks[useEffectKind]); - const isUseStateCall = ER.isReactHookCallWithNameAlias(context, "useState", hooks.useState); - const isUseMemoCall = ER.isReactHookCallWithNameAlias(context, "useMemo", hooks.useMemo); - const isUseCallbackCall = ER.isReactHookCallWithNameAlias(context, "useCallback", hooks.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 WeakMap(); - const indSetStateCallsInUseEffectArg0 = new WeakMap(); - const indSetStateCallsInUseEffectSetup = new Map(); - const indSetStateCallsInUseMemoOrCallback = new WeakMap(); - - 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 getCallName(node: TSESTree.Node) { - if (node.type === T.CallExpression) { - return AST.toString(node.callee, getText); - } - return AST.toString(node, getText); - } - - function getCallKind(node: TSESTree.CallExpression) { - return match(node) - .when(isUseStateCall, () => "useState") - .when(isUseEffectLikeCall, () => useEffectKind) - .when(isSetStateCall, () => "setState") - .when(AST.isThenCall, () => "then") - .otherwise(() => "other"); - } - - function getFunctionKind(node: AST.TSESTreeFunction) { - return match(node) - .when(isFunctionOfUseEffectSetup, () => "setup") - .when(AST.isImmediatelyInvokedFunction, () => "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: - 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) getOrElseUpdate(indSetStateCalls, pEntry.node, () => []).push(node); - else getOrElseUpdate(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(constVoid); - }, - 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) { - getOrElseUpdate(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) { - getOrElseUpdate(indSetStateCallsInUseEffectArg0, vd.init, () => []).push(node); - } - break; - } - // const [state, setState] = useState(); - // useEffect(setState); - if (isUseEffectLikeCall(node.parent)) { - getOrElseUpdate(indSetStateCallsInUseEffectSetup, node.parent, () => []).push(node); - } - } - } - }, - "Program:exit"() { - const getSetStateCalls = ( - id: string | TSESTree.Identifier, - initialScope: Scope.Scope, - ): TSESTree.CallExpression[] | TSESTree.Identifier[] => { - const node = VAR.getVariableInitNode(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: getCallName(setStateCall), - }); - } - } - for (const id of setupFunctionIdentifiers) { - const setStateCalls = getSetStateCalls(id.name, context.sourceCode.getScope(id)); - for (const setStateCall of setStateCalls) { - onViolation(context, setStateCall, { - name: getCallName(setStateCall), - }); - } - } - }, - }; -} diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-effect.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-effect.ts index c5edf42bd5..89d0874cf4 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-effect.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-effect.ts @@ -1,8 +1,16 @@ import type { RuleContext, RuleFeature } from "@eslint-react/kit"; +import type { ESLintUtils, TSESTree } from "@typescript-eslint/utils"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; +import type { Scope } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; +import * as AST from "@eslint-react/ast"; +import * as ER from "@eslint-react/core"; +import { constVoid, getOrElseUpdate } from "@eslint-react/eff"; +import { getSettingsFromContext } from "@eslint-react/shared"; +import * as VAR from "@eslint-react/var"; +import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; -import { useNoDirectSetStateInUseEffect } from "../hooks/use-no-direct-set-state-in-use-effect"; +import { match } from "ts-pattern"; import { createRule } from "../utils"; export const RULE_NAME = "no-direct-set-state-in-use-effect"; @@ -39,3 +47,318 @@ export function create(context: RuleContext): RuleListener { useEffectKind: "useEffect", }); } + +type CallKind = + | "useEffect" + | "useLayoutEffect" + | "useState" + | "setState" + | "then" + | "other"; + +type FunctionKind = + | "setup" + | "cleanup" + | "deferred" + | "immediate" + | "other"; + +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 hooks = settings.additionalHooks; + const getText = (n: TSESTree.Node) => context.sourceCode.getText(n); + const isUseEffectLikeCall = ER.isReactHookCallWithNameAlias(context, useEffectKind, hooks[useEffectKind]); + const isUseStateCall = ER.isReactHookCallWithNameAlias(context, "useState", hooks.useState); + const isUseMemoCall = ER.isReactHookCallWithNameAlias(context, "useMemo", hooks.useMemo); + const isUseCallbackCall = ER.isReactHookCallWithNameAlias(context, "useCallback", hooks.useCallback); + + 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 WeakMap(); + const indSetStateCallsInUseEffectArg0 = new WeakMap(); + const indSetStateCallsInUseEffectSetup = new Map(); + const indSetStateCallsInUseMemoOrCallback = new WeakMap(); + + 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 getCallName(node: TSESTree.Node) { + if (node.type === T.CallExpression) { + return AST.toString(node.callee, getText); + } + return AST.toString(node, getText); + } + + function getCallKind(node: TSESTree.CallExpression) { + return match(node) + .when(isUseStateCall, () => "useState") + .when(isUseEffectLikeCall, () => useEffectKind) + .when(isSetStateCall, () => "setState") + .when(AST.isThenCall, () => "then") + .otherwise(() => "other"); + } + + function getFunctionKind(node: AST.TSESTreeFunction) { + return match(node) + .when(isFunctionOfUseEffectSetup, () => "setup") + .when(AST.isImmediatelyInvokedFunction, () => "immediate") + .otherwise(() => "other"); + } + + function isIdFromUseStateCall(topLevelId: TSESTree.Identifier, at?: number) { + const variable = VAR.findVariable(topLevelId, context.sourceCode.getScope(topLevelId)); + const variableNode = VAR.getVariableInitNode(variable, 0); + if (variableNode == null) return false; + if (variableNode.type !== T.CallExpression) return false; + if (!ER.isReactHookCallWithNameAlias(context, "useState", hooks.useState)(variableNode)) return false; + const variableNodeParent = variableNode.parent; + if (!("id" in variableNodeParent) || variableNodeParent.id?.type !== T.ArrayPattern) { + return true; + } + return variableNodeParent + .id + .elements + .findIndex((e) => e?.type === T.Identifier && e.name === topLevelId.name) === at; + } + + function isSetStateCall(node: TSESTree.CallExpression) { + switch (node.callee.type) { + // const data = useState(); + // data.at(1)(); + case T.CallExpression: { + const { callee } = node.callee; + if (callee.type !== T.MemberExpression) { + return false; + } + if (!("name" in callee.object)) { + return false; + } + const isAt = callee.property.type === T.Identifier && callee.property.name === "at"; + const [index] = node.callee.arguments; + if (!isAt || index == null) { + return false; + } + const indexScope = context.sourceCode.getScope(node); + const indexValue = VAR.toStaticValue({ + kind: "lazy", + node: index, + initialScope: indexScope, + }).value; + return indexValue === 1 && isIdFromUseStateCall(callee.object); + } + // const [data, setData] = useState(); + // setData(); + case T.Identifier: { + return isIdFromUseStateCall(node.callee, 1); + } + // const data = useState(); + // data[1](); + case T.MemberExpression: { + if (!("name" in node.callee.object)) { + return false; + } + const property = node.callee.property; + const propertyScope = context.sourceCode.getScope(node); + const propertyValue = VAR.toStaticValue({ + kind: "lazy", + node: property, + initialScope: propertyScope, + }).value; + return propertyValue === 1 && isIdFromUseStateCall(node.callee.object, 1); + } + default: { + return false; + } + } + } + + 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: + 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) getOrElseUpdate(indSetStateCalls, pEntry.node, () => []).push(node); + else getOrElseUpdate(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(constVoid); + }, + Identifier(node) { + if (node.parent.type === T.CallExpression && node.parent.callee === node) { + return; + } + if (!isIdFromUseStateCall(node, 1)) { + 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) { + getOrElseUpdate(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) { + getOrElseUpdate(indSetStateCallsInUseEffectArg0, vd.init, () => []).push(node); + } + break; + } + // const [state, setState] = useState(); + // useEffect(setState); + if (isUseEffectLikeCall(node.parent)) { + getOrElseUpdate(indSetStateCallsInUseEffectSetup, node.parent, () => []).push(node); + } + } + } + }, + "Program:exit"() { + const getSetStateCalls = ( + id: string | TSESTree.Identifier, + initialScope: Scope.Scope, + ): TSESTree.CallExpression[] | TSESTree.Identifier[] => { + const node = VAR.getVariableInitNode(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: getCallName(setStateCall), + }); + } + } + for (const id of setupFunctionIdentifiers) { + const setStateCalls = getSetStateCalls(id.name, context.sourceCode.getScope(id)); + for (const setStateCall of setStateCalls) { + onViolation(context, setStateCall, { + name: getCallName(setStateCall), + }); + } + } + }, + }; +} + +function isInitFromHookCall(init: TSESTree.Expression | null) { + if (init?.type !== T.CallExpression) return false; + switch (init.callee.type) { + case T.Identifier: + return ER.isReactHookName(init.callee.name); + case T.MemberExpression: + return init.callee.property.type === T.Identifier + && ER.isReactHookName(init.callee.property.name); + default: + return false; + } +} + +function isVariableDeclaratorFromHookCall(node: TSESTree.Node): node is + & TSESTree.VariableDeclarator + & { init: TSESTree.VariableDeclarator["init"] & {} } +{ + if (node.type !== T.VariableDeclarator) return false; + if (node.id.type !== T.Identifier) return false; + return isInitFromHookCall(node.init); +} diff --git a/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-layout-effect.ts b/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-layout-effect.ts index c34709635b..52141413ff 100644 --- a/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-layout-effect.ts +++ b/packages/plugins/eslint-plugin-react-x/src/rules/no-direct-set-state-in-use-layout-effect.ts @@ -2,8 +2,8 @@ import type { RuleContext, RuleFeature } from "@eslint-react/kit"; import type { RuleListener } from "@typescript-eslint/utils/ts-eslint"; import type { CamelCase } from "string-ts"; -import { useNoDirectSetStateInUseEffect } from "../hooks/use-no-direct-set-state-in-use-effect"; import { createRule } from "../utils"; +import { useNoDirectSetStateInUseEffect } from "./no-direct-set-state-in-use-effect"; export const RULE_NAME = "no-direct-set-state-in-use-layout-effect"; diff --git a/packages/plugins/eslint-plugin-react-x/src/utils/index.ts b/packages/plugins/eslint-plugin-react-x/src/utils/index.ts index 1359fb6da3..d7afa99eda 100644 --- a/packages/plugins/eslint-plugin-react-x/src/utils/index.ts +++ b/packages/plugins/eslint-plugin-react-x/src/utils/index.ts @@ -1,6 +1,2 @@ export * from "./constants"; export * from "./create-rule"; -export * from "./is-from-hook-call"; -export * from "./is-from-use-state-call"; -export * from "./is-set-function-call"; -export * from "./is-variable-declarator-from-hook-call"; diff --git a/packages/plugins/eslint-plugin-react-x/src/utils/is-from-hook-call.ts b/packages/plugins/eslint-plugin-react-x/src/utils/is-from-hook-call.ts deleted file mode 100644 index 7656ab078a..0000000000 --- a/packages/plugins/eslint-plugin-react-x/src/utils/is-from-hook-call.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { RuleContext } from "@eslint-react/kit"; -import type { ESLintReactSettingsNormalized } from "@eslint-react/shared"; -import type { TSESTree } from "@typescript-eslint/types"; -import type { REACT_BUILD_IN_HOOKS } from "./constants"; -import * as ER from "@eslint-react/core"; -import { constTrue } from "@eslint-react/eff"; -import * as VAR from "@eslint-react/var"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; - -export function isFromHookCall( - context: RuleContext, - name: (typeof REACT_BUILD_IN_HOOKS)[number], - settings: ESLintReactSettingsNormalized, - predicate: (topLevelId: TSESTree.Identifier, call: TSESTree.CallExpression) => boolean = constTrue, -) { - const hookAlias = settings.additionalHooks[name] ?? []; - // eslint-disable-next-line function/function-return-boolean - return (topLevelId: TSESTree.Identifier) => { - const variable = VAR.findVariable(topLevelId, context.sourceCode.getScope(topLevelId)); - const variableNode = VAR.getVariableInitNode(variable, 0); - if (variableNode == null) return false; - if (variableNode.type !== T.CallExpression) return false; - if (!ER.isReactHookCallWithNameAlias(context, name, hookAlias)(variableNode)) return false; - return predicate(topLevelId, variableNode); - }; -} diff --git a/packages/plugins/eslint-plugin-react-x/src/utils/is-from-use-state-call.ts b/packages/plugins/eslint-plugin-react-x/src/utils/is-from-use-state-call.ts deleted file mode 100644 index 14e265557f..0000000000 --- a/packages/plugins/eslint-plugin-react-x/src/utils/is-from-use-state-call.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { RuleContext } from "@eslint-react/kit"; -import type { ESLintReactSettingsNormalized } from "@eslint-react/shared"; -import type { TSESTree } from "@typescript-eslint/types"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; - -import { isFromHookCall } from "./is-from-hook-call"; - -export function isFromUseStateCall(context: RuleContext, settings: ESLintReactSettingsNormalized) { - const predicate = (topLevelId: TSESTree.Identifier, call: TSESTree.CallExpression) => { - const { parent } = call; - if (!("id" in parent) || parent.id?.type !== T.ArrayPattern) { - return true; - } - return parent.id.elements.findIndex((e) => e?.type === T.Identifier && e.name === topLevelId.name) === 1; - }; - // eslint-disable-next-line function/function-return-boolean - return isFromHookCall(context, "useState", settings, predicate); -} diff --git a/packages/plugins/eslint-plugin-react-x/src/utils/is-set-function-call.ts b/packages/plugins/eslint-plugin-react-x/src/utils/is-set-function-call.ts deleted file mode 100644 index a29fcdf1e2..0000000000 --- a/packages/plugins/eslint-plugin-react-x/src/utils/is-set-function-call.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { RuleContext } from "@eslint-react/kit"; -import type { ESLintReactSettingsNormalized } from "@eslint-react/shared"; -import type { TSESTree } from "@typescript-eslint/types"; -import * as VAR from "@eslint-react/var"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; - -import { isFromUseStateCall } from "./is-from-use-state-call"; - -export function isSetFunctionCall(context: RuleContext, settings: ESLintReactSettingsNormalized) { - const isIdFromUseStateCall = isFromUseStateCall(context, settings); - // eslint-disable-next-line function/function-return-boolean - return (node: TSESTree.CallExpression) => { - switch (node.callee.type) { - // const data = useState(); - // data.at(1)(); - case T.CallExpression: { - const { callee } = node.callee; - if (callee.type !== T.MemberExpression) { - return false; - } - if (!("name" in callee.object)) { - return false; - } - const isAt = callee.property.type === T.Identifier && callee.property.name === "at"; - const [index] = node.callee.arguments; - if (!isAt || index == null) { - return false; - } - const indexScope = context.sourceCode.getScope(node); - const indexValue = VAR.toStaticValue({ - kind: "lazy", - node: index, - initialScope: indexScope, - }).value; - return indexValue === 1 && isIdFromUseStateCall(callee.object); - } - // const [data, setData] = useState(); - // setData(); - case T.Identifier: { - return isIdFromUseStateCall(node.callee); - } - // const data = useState(); - // data[1](); - case T.MemberExpression: { - if (!("name" in node.callee.object)) { - return false; - } - const property = node.callee.property; - const propertyScope = context.sourceCode.getScope(node); - const propertyValue = VAR.toStaticValue({ - kind: "lazy", - node: property, - initialScope: propertyScope, - }).value; - return propertyValue === 1 && isIdFromUseStateCall(node.callee.object); - } - default: { - return false; - } - } - }; -} diff --git a/packages/plugins/eslint-plugin-react-x/src/utils/is-variable-declarator-from-hook-call.ts b/packages/plugins/eslint-plugin-react-x/src/utils/is-variable-declarator-from-hook-call.ts deleted file mode 100644 index 11c09f3ec4..0000000000 --- a/packages/plugins/eslint-plugin-react-x/src/utils/is-variable-declarator-from-hook-call.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { TSESTree } from "@typescript-eslint/types"; -import * as ER from "@eslint-react/core"; -import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; - -type VariableDeclaratorFromHookCall = { - id: TSESTree.Identifier; - init: { callee: TSESTree.Identifier | TSESTree.MemberExpression } & TSESTree.CallExpression; -} & TSESTree.VariableDeclarator; - -export function isVariableDeclaratorFromHookCall(node: TSESTree.Node): node is VariableDeclaratorFromHookCall { - if (node.type !== T.VariableDeclarator) { - return false; - } - if (node.id.type !== T.Identifier) { - return false; - } - if (node.init?.type !== T.CallExpression) { - return false; - } - switch (node.init.callee.type) { - case T.Identifier: - return ER.isReactHookName(node.init.callee.name); - case T.MemberExpression: - return node.init.callee.property.type === T.Identifier - && ER.isReactHookName(node.init.callee.property.name); - default: - return false; - } -} diff --git a/packages/utilities/ast/src/ast-array-pattern.ts b/packages/utilities/ast/src/ast-array-pattern.ts new file mode 100644 index 0000000000..9fa612cea7 --- /dev/null +++ b/packages/utilities/ast/src/ast-array-pattern.ts @@ -0,0 +1,13 @@ +import type { TSESTree } from "@typescript-eslint/types"; +import { _ } from "@eslint-react/eff"; +import { AST_NODE_TYPES as T } from "@typescript-eslint/types"; + +export function getArrayPatternElementNameAt(node: TSESTree.Identifier, at: number) { + const { parent } = node; + if (!("id" in parent) || parent.id?.type !== T.ArrayPattern) return true; + const element = parent + .id + .elements[at]; + if (element?.type !== T.Identifier) return _; + return element.name; +} diff --git a/packages/utilities/ast/src/index.ts b/packages/utilities/ast/src/index.ts index 946f626d23..cde637cb7b 100644 --- a/packages/utilities/ast/src/index.ts +++ b/packages/utilities/ast/src/index.ts @@ -1,5 +1,6 @@ export * from "./ast-array-method"; export * from "./ast-array-method-callback"; +export * from "./ast-array-pattern"; export * from "./ast-class-id"; export * from "./ast-expression"; export * from "./ast-function-id";