Skip to content

Commit 8fd1c4f

Browse files
committed
refactor(plugins/hooks-extra): improve code reusability
1 parent 3ae33d7 commit 8fd1c4f

File tree

5 files changed

+242
-453
lines changed

5 files changed

+242
-453
lines changed

packages/core/docs/functions/isReactHookCallWithNameAlias.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
### alias
2222

23-
`string`[]
23+
`undefined` | `string`[]
2424

2525
## Returns
2626

packages/core/src/hook/is.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export function isReactHookCallWithNameLoose(node: TSESTree.CallExpression | _)
7171
};
7272
}
7373

74-
export function isReactHookCallWithNameAlias(context: RuleContext, name: string, alias: string[]) {
74+
export function isReactHookCallWithNameAlias(context: RuleContext, name: string, alias: _ | string[] = []) {
7575
const {
7676
importSource = DEFAULT_ESLINT_REACT_SETTINGS.importSource,
7777
skipImportCheck = true,
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import * as AST from "@eslint-react/ast";
2+
import { isReactHookCallWithNameAlias } from "@eslint-react/core";
3+
import { _, getOrUpdate } from "@eslint-react/eff";
4+
import { getSettingsFromContext, type RuleContext } from "@eslint-react/shared";
5+
import * as VAR from "@eslint-react/var";
6+
import { AST_NODE_TYPES as T } from "@typescript-eslint/types";
7+
import type { ESLintUtils, TSESTree } from "@typescript-eslint/utils";
8+
import type { Scope } from "@typescript-eslint/utils/ts-eslint";
9+
import { match } from "ts-pattern";
10+
11+
import {
12+
isFromUseStateCall,
13+
isFunctionOfImmediatelyInvoked,
14+
isSetFunctionCall,
15+
isThenCall,
16+
isVariableDeclaratorFromHookCall,
17+
} from "../utils";
18+
19+
type CallKind = "other" | "setState" | "then" | "useEffect" | "useLayoutEffect" | "useState";
20+
type FunctionKind = "cleanup" | "deferred" | "immediate" | "other" | "setup";
21+
22+
export declare namespace useNoDirectSetStateInUseEffect {
23+
type Options<Ctx> = {
24+
onViolation: (context: Ctx, node: TSESTree.Node | TSESTree.Token, data: { name: string }) => void;
25+
useEffectKind: "useEffect" | "useLayoutEffect";
26+
};
27+
type ReturnType = ESLintUtils.RuleListener;
28+
}
29+
30+
export function useNoDirectSetStateInUseEffect<Ctx extends RuleContext>(
31+
context: Ctx,
32+
options: useNoDirectSetStateInUseEffect.Options<Ctx>,
33+
): useNoDirectSetStateInUseEffect.ReturnType {
34+
const { onViolation, useEffectKind } = options;
35+
const settings = getSettingsFromContext(context);
36+
const additionalHooks = settings.additionalHooks;
37+
const isUseEffectLikeCall = isReactHookCallWithNameAlias(context, useEffectKind, additionalHooks[useEffectKind]);
38+
const isUseStateCall = isReactHookCallWithNameAlias(context, "useState", additionalHooks.useState);
39+
const isUseMemoCall = isReactHookCallWithNameAlias(context, "useMemo", additionalHooks.useMemo);
40+
const isUseCallbackCall = isReactHookCallWithNameAlias(context, "useCallback", additionalHooks.useCallback);
41+
const isSetStateCall = isSetFunctionCall(context, settings);
42+
const isIdFromUseStateCall = isFromUseStateCall(context, settings);
43+
44+
const functionEntries: { kind: FunctionKind; node: AST.TSESTreeFunction }[] = [];
45+
const setupFunctionRef: { current: AST.TSESTreeFunction | null } = { current: null };
46+
const setupFunctionIdentifiers: TSESTree.Identifier[] = [];
47+
48+
const indFunctionCalls: TSESTree.CallExpression[] = [];
49+
const indSetStateCalls = new Map<AST.TSESTreeFunction, TSESTree.CallExpression[]>();
50+
const indSetStateCallsInUseEffectArg0 = new Map<TSESTree.CallExpression, TSESTree.Identifier[]>();
51+
const indSetStateCallsInUseEffectSetup = new Map<TSESTree.CallExpression, TSESTree.Identifier[]>();
52+
const indSetStateCallsInUseMemoOrCallback = new Map<TSESTree.Node, TSESTree.CallExpression[]>();
53+
54+
const onSetupFunctionEnter = (node: AST.TSESTreeFunction) => {
55+
setupFunctionRef.current = node;
56+
};
57+
58+
const onSetupFunctionExit = (node: AST.TSESTreeFunction) => {
59+
if (setupFunctionRef.current === node) {
60+
setupFunctionRef.current = null;
61+
}
62+
};
63+
64+
function isFunctionOfUseEffectSetup(node: TSESTree.Node) {
65+
return node.parent?.type === T.CallExpression
66+
&& node.parent.callee !== node
67+
&& isUseEffectLikeCall(node.parent);
68+
}
69+
70+
function getCallKind(node: TSESTree.CallExpression) {
71+
return match<TSESTree.CallExpression, CallKind>(node)
72+
.when(isUseStateCall, () => "useState")
73+
.when(isUseEffectLikeCall, () => useEffectKind)
74+
.when(isSetStateCall, () => "setState")
75+
.when(isThenCall, () => "then")
76+
.otherwise(() => "other");
77+
}
78+
79+
function getFunctionKind(node: AST.TSESTreeFunction) {
80+
return match<AST.TSESTreeFunction, FunctionKind>(node)
81+
.when(isFunctionOfUseEffectSetup, () => "setup")
82+
.when(isFunctionOfImmediatelyInvoked, () => "immediate")
83+
.otherwise(() => "other");
84+
}
85+
86+
return {
87+
":function"(node: AST.TSESTreeFunction) {
88+
const kind = getFunctionKind(node);
89+
functionEntries.push({ kind, node });
90+
if (kind === "setup") {
91+
onSetupFunctionEnter(node);
92+
}
93+
},
94+
":function:exit"(node: AST.TSESTreeFunction) {
95+
const { kind } = functionEntries.at(-1) ?? {};
96+
if (kind === "setup") {
97+
onSetupFunctionExit(node);
98+
}
99+
functionEntries.pop();
100+
},
101+
CallExpression(node) {
102+
const setupFunction = setupFunctionRef.current;
103+
const pEntry = functionEntries.at(-1);
104+
if (pEntry == null || pEntry.node.async) {
105+
return;
106+
}
107+
match(getCallKind(node))
108+
.with("setState", () => {
109+
switch (true) {
110+
case pEntry.node === setupFunction
111+
|| pEntry.kind === "immediate": {
112+
onViolation(context, node, {
113+
name: context.sourceCode.getText(node.callee),
114+
});
115+
return;
116+
}
117+
default: {
118+
const vd = AST.findParentNode(node, isVariableDeclaratorFromHookCall);
119+
if (vd == null) getOrUpdate(indSetStateCalls, pEntry.node, () => []).push(node);
120+
else getOrUpdate(indSetStateCallsInUseMemoOrCallback, vd.init, () => []).push(node);
121+
}
122+
}
123+
})
124+
.with(useEffectKind, () => {
125+
if (AST.isFunction(node.arguments.at(0))) return;
126+
setupFunctionIdentifiers.push(...AST.getNestedIdentifiers(node));
127+
})
128+
.with("other", () => {
129+
if (pEntry.node !== setupFunction) return;
130+
indFunctionCalls.push(node);
131+
})
132+
.otherwise(() => _);
133+
},
134+
Identifier(node) {
135+
if (node.parent.type === T.CallExpression && node.parent.callee === node) {
136+
return;
137+
}
138+
if (!isIdFromUseStateCall(node)) {
139+
return;
140+
}
141+
switch (node.parent.type) {
142+
case T.ArrowFunctionExpression: {
143+
const parent = node.parent.parent;
144+
if (parent.type !== T.CallExpression) {
145+
break;
146+
}
147+
// const [state, setState] = useState();
148+
// const set = useMemo(() => setState, []);
149+
// useEffect(set, []);
150+
if (!isUseMemoCall(parent)) {
151+
break;
152+
}
153+
const vd = AST.findParentNode(parent, isVariableDeclaratorFromHookCall);
154+
if (vd != null) {
155+
getOrUpdate(indSetStateCallsInUseEffectArg0, vd.init, () => []).push(node);
156+
}
157+
break;
158+
}
159+
case T.CallExpression: {
160+
if (node !== node.parent.arguments.at(0)) {
161+
break;
162+
}
163+
// const [state, setState] = useState();
164+
// const set = useCallback(setState, []);
165+
// useEffect(set, []);
166+
if (isUseCallbackCall(node.parent)) {
167+
const vd = AST.findParentNode(node.parent, isVariableDeclaratorFromHookCall);
168+
if (vd != null) {
169+
getOrUpdate(indSetStateCallsInUseEffectArg0, vd.init, () => []).push(node);
170+
}
171+
break;
172+
}
173+
// const [state, setState] = useState();
174+
// useEffect(setState);
175+
if (isUseEffectLikeCall(node.parent)) {
176+
getOrUpdate(indSetStateCallsInUseEffectSetup, node.parent, () => []).push(node);
177+
}
178+
}
179+
}
180+
},
181+
"Program:exit"() {
182+
const getSetStateCalls = (
183+
id: string | TSESTree.Identifier,
184+
initialScope: Scope.Scope,
185+
): TSESTree.CallExpression[] | TSESTree.Identifier[] => {
186+
const node = VAR.getVariableNode(VAR.findVariable(id, initialScope), 0);
187+
switch (node?.type) {
188+
case T.ArrowFunctionExpression:
189+
case T.FunctionDeclaration:
190+
case T.FunctionExpression:
191+
return indSetStateCalls.get(node) ?? [];
192+
case T.CallExpression:
193+
return indSetStateCallsInUseMemoOrCallback.get(node) ?? indSetStateCallsInUseEffectArg0.get(node) ?? [];
194+
}
195+
return [];
196+
};
197+
for (const [, calls] of indSetStateCallsInUseEffectSetup) {
198+
for (const call of calls) {
199+
onViolation(context, call, { name: call.name });
200+
}
201+
}
202+
for (const { callee } of indFunctionCalls) {
203+
if (!("name" in callee)) {
204+
continue;
205+
}
206+
const { name } = callee;
207+
const setStateCalls = getSetStateCalls(name, context.sourceCode.getScope(callee));
208+
for (const setStateCall of setStateCalls) {
209+
onViolation(context, setStateCall, {
210+
name: AST.toReadableNodeName(setStateCall, (n) => context.sourceCode.getText(n)),
211+
});
212+
}
213+
}
214+
for (const id of setupFunctionIdentifiers) {
215+
const setStateCalls = getSetStateCalls(id.name, context.sourceCode.getScope(id));
216+
for (const setStateCall of setStateCalls) {
217+
onViolation(context, setStateCall, {
218+
name: AST.toReadableNodeName(setStateCall, (n) => context.sourceCode.getText(n)),
219+
});
220+
}
221+
}
222+
},
223+
};
224+
}

0 commit comments

Comments
 (0)