@ashstack/lint 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,559 @@
1
+ //#region src/util/ast.ts
2
+ const ascend = (context, ref, visit, visited = /* @__PURE__ */ new Set()) => {
3
+ var _ref$resolved;
4
+ if (visited.has(ref)) return;
5
+ const cont = visit(ref);
6
+ visited.add(ref);
7
+ if (cont === false) return;
8
+ (_ref$resolved = ref.resolved) === null || _ref$resolved === void 0 || _ref$resolved.defs.filter((def) => def.type !== "ImportBinding").filter((def) => def.type !== "Parameter").map((def) => def.node.init ?? def.node.body).filter((n) => Boolean(n)).flatMap((node) => getDownstreamRefs(context, node)).forEach((ref) => ascend(context, ref, visit, visited));
9
+ };
10
+ const descend = (context, node, visit, visited = /* @__PURE__ */ new Set()) => {
11
+ if (visited.has(node)) return;
12
+ visit(node);
13
+ visited.add(node);
14
+ (context.sourceCode.visitorKeys[node.type] || []).filter((key) => key !== "arguments").map((key) => node[key]).filter(Boolean).flatMap((child) => Array.isArray(child) ? child : [child]).filter(Boolean).forEach((child) => descend(context, child, visit, visited));
15
+ };
16
+ const getUpstreamRefs = (context, ref) => {
17
+ const refs = [];
18
+ ascend(context, ref, (upRef) => {
19
+ refs.push(upRef);
20
+ });
21
+ return refs;
22
+ };
23
+ const findDownstreamNodes = (context, topNode, type) => {
24
+ const nodes = [];
25
+ descend(context, topNode, (node) => {
26
+ if (node.type === type) nodes.push(node);
27
+ });
28
+ return nodes;
29
+ };
30
+ const getDownstreamRefs = (context, node) => findDownstreamNodes(context, node, "Identifier").map((identifier) => getRef(context, identifier)).filter(Boolean);
31
+ const getCallExpr = (ref, current = ref.identifier.parent) => {
32
+ if (current.type === "CallExpression") {
33
+ let node = ref.identifier;
34
+ while (node.parent.type === "MemberExpression") node = node.parent;
35
+ if (current.callee === node) return current;
36
+ }
37
+ if (current.type === "MemberExpression") return getCallExpr(ref, current.parent);
38
+ };
39
+ const getArgsUpstreamRefs = (context, ref) => getUpstreamRefs(context, ref).map((ref) => getCallExpr(ref)).filter(Boolean).flatMap((callExpr) => {
40
+ if (!callExpr || callExpr.type !== "CallExpression") return [];
41
+ return callExpr.arguments;
42
+ }).flatMap((arg) => getDownstreamRefs(context, arg)).flatMap((ref) => getUpstreamRefs(context, ref));
43
+ const getRef = (context, identifier) => {
44
+ var _context$sourceCode$g;
45
+ return (_context$sourceCode$g = context.sourceCode.getScope(identifier)) === null || _context$sourceCode$g === void 0 ? void 0 : _context$sourceCode$g.references.find((ref) => ref.identifier == identifier);
46
+ };
47
+ const isSynchronous = (node, within) => {
48
+ if (node === within) return true;
49
+ else if (node.type === "AwaitExpression" || node.type === "UnaryExpression" && node.operator === "void" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") return false;
50
+ else return isSynchronous(node.parent, within);
51
+ };
52
+ const getSynchronousCallChain = (context, ref) => {
53
+ const findEnclosingFunction = (node) => {
54
+ if (!node) return;
55
+ else if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") return node;
56
+ else return findEnclosingFunction(node.parent);
57
+ };
58
+ const isAliasRef = (ref) => {
59
+ const containerParents = [
60
+ "Property",
61
+ "ObjectExpression",
62
+ "ArrayExpression",
63
+ "SequenceExpression"
64
+ ];
65
+ let node = ref.identifier;
66
+ for (;;) {
67
+ const parent = node.parent;
68
+ if (parent.type === "VariableDeclarator" && parent.init === node) return true;
69
+ if (containerParents.includes(parent.type)) {
70
+ node = parent;
71
+ continue;
72
+ }
73
+ return false;
74
+ }
75
+ };
76
+ const callExprRefs = [];
77
+ ascend(context, ref, (upRef) => {
78
+ const callExpr = getCallExpr(upRef);
79
+ const enclosingFn = findEnclosingFunction(callExpr);
80
+ if (callExpr && enclosingFn && isSynchronous(callExpr, enclosingFn)) callExprRefs.push(upRef);
81
+ else if (isAliasRef(upRef)) callExprRefs.push(upRef);
82
+ else return false;
83
+ });
84
+ return callExprRefs;
85
+ };
86
+ //#endregion
87
+ //#region src/util/react.ts
88
+ const isFunctionalComponent = (node) => {
89
+ var _node$init, _node$init2, _node$id;
90
+ return (node.type === "FunctionDeclaration" || node.type === "VariableDeclarator" && (((_node$init = node.init) === null || _node$init === void 0 ? void 0 : _node$init.type) === "ArrowFunctionExpression" || ((_node$init2 = node.init) === null || _node$init2 === void 0 ? void 0 : _node$init2.type) === "CallExpression")) && ((_node$id = node.id) === null || _node$id === void 0 ? void 0 : _node$id.type) === "Identifier" && node.id.name[0].toUpperCase() === node.id.name[0];
91
+ };
92
+ const isFunctionalHOC = (context, node) => {
93
+ const knownPureHocs = ["memo", "forwardRef"];
94
+ const isWrappedInline = (n) => {
95
+ var _n$init;
96
+ return n.type === "VariableDeclarator" && ((_n$init = n.init) === null || _n$init === void 0 ? void 0 : _n$init.type) === "CallExpression" && n.init.callee.type === "Identifier" && !knownPureHocs.includes(n.init.callee.name) && n.init.arguments.length > 0 && (n.init.arguments[0].type === "ArrowFunctionExpression" || n.init.arguments[0].type === "FunctionExpression");
97
+ };
98
+ const isWrappedSeparately = (n) => {
99
+ var _getRef;
100
+ return ((_getRef = getRef(context, n.id)) === null || _getRef === void 0 || (_getRef = _getRef.resolved) === null || _getRef === void 0 ? void 0 : _getRef.references.filter((ref) => {
101
+ const parent = ref.identifier.parent;
102
+ return (parent === null || parent === void 0 ? void 0 : parent.type) === "CallExpression" && parent.arguments.includes(ref.identifier);
103
+ }).map((ref) => ref.identifier.parent).some((wrapper) => wrapper.type === "CallExpression" && wrapper.callee.type === "Identifier" && !knownPureHocs.includes(wrapper.callee.name))) ?? false;
104
+ };
105
+ return isFunctionalComponent(node) && (isWrappedInline(node) || isWrappedSeparately(node));
106
+ };
107
+ const isCustomHook = (node) => {
108
+ var _node$id2;
109
+ if (node.type !== "FunctionDeclaration" && (node.type !== "VariableDeclarator" || !node.init || node.init.type !== "ArrowFunctionExpression" && node.init.type !== "FunctionExpression")) return false;
110
+ return ((_node$id2 = node.id) === null || _node$id2 === void 0 ? void 0 : _node$id2.type) === "Identifier" && node.id.name.startsWith("use") && node.id.name.length > 3 && node.id.name[3] !== void 0 && node.id.name[3] === node.id.name[3].toUpperCase();
111
+ };
112
+ const isUseState = (node) => {
113
+ if (node.type === "Identifier" && node.name === "useState") return true;
114
+ if (node.type === "MemberExpression" && node.object.type === "Identifier" && node.object.name === "React" && node.property.type === "Identifier" && node.property.name === "useState") return true;
115
+ const parent = node.parent;
116
+ return parent.type === "MemberExpression" && parent.object.type === "Identifier" && parent.object.name === "React" && parent.property.type === "Identifier" && parent.property.name === "useState";
117
+ };
118
+ const isUseRef = (node) => {
119
+ if (node.type === "Identifier" && node.name === "useRef") return true;
120
+ const parent = node.parent;
121
+ return parent.type === "MemberExpression" && parent.object.type === "Identifier" && parent.object.name === "React" && parent.property.type === "Identifier" && parent.property.name === "useRef";
122
+ };
123
+ const isUseEffect = (node) => node.type === "CallExpression" && (node.callee.type === "Identifier" && node.callee.name === "useEffect" || node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "React" && node.callee.property.type === "Identifier" && node.callee.property.name === "useEffect");
124
+ const getEffectFn = (context, node) => {
125
+ if (node.type !== "CallExpression") return void 0;
126
+ const effectFn = node.arguments[0];
127
+ if ((effectFn === null || effectFn === void 0 ? void 0 : effectFn.type) === "ArrowFunctionExpression" || (effectFn === null || effectFn === void 0 ? void 0 : effectFn.type) === "FunctionExpression") return effectFn;
128
+ else if ((effectFn === null || effectFn === void 0 ? void 0 : effectFn.type) === "Identifier") {
129
+ var _ref$resolved;
130
+ const ref = getRef(context, effectFn);
131
+ const def = ref === null || ref === void 0 || (_ref$resolved = ref.resolved) === null || _ref$resolved === void 0 ? void 0 : _ref$resolved.defs[0];
132
+ if (!def) return void 0;
133
+ return def.node.init ?? def.node.body;
134
+ }
135
+ };
136
+ const getEffectDeps = (node) => {
137
+ if (node.type !== "CallExpression") return void 0;
138
+ const depsArr = node.arguments[1];
139
+ if ((depsArr === null || depsArr === void 0 ? void 0 : depsArr.type) !== "ArrayExpression") return;
140
+ return depsArr;
141
+ };
142
+ const getEffectCleanup = (context, node) => {
143
+ const effectFn = getEffectFn(context, node);
144
+ if (!effectFn) return void 0;
145
+ if (effectFn.type !== "ArrowFunctionExpression" && effectFn.type !== "FunctionExpression" || effectFn.body.type !== "BlockStatement") return;
146
+ return effectFn.body.body.concat().reverse().find((stmt) => stmt.type === "ReturnStatement" && stmt.argument);
147
+ };
148
+ const getEffect = (context, node) => {
149
+ if (!isUseEffect(node)) return void 0;
150
+ const fn = getEffectFn(context, node);
151
+ if (!fn) return void 0;
152
+ const deps = getEffectDeps(node);
153
+ return {
154
+ fn,
155
+ fnRefs: getDownstreamRefs(context, fn),
156
+ depsRefs: deps ? getDownstreamRefs(context, deps) : void 0,
157
+ cleanup: getEffectCleanup(context, node)
158
+ };
159
+ };
160
+ const isState = (ref) => {
161
+ var _ref$resolved2;
162
+ return ((_ref$resolved2 = ref.resolved) === null || _ref$resolved2 === void 0 ? void 0 : _ref$resolved2.defs.some((def) => {
163
+ var _def$node$init, _def$node$id$elements;
164
+ return def.node.type === "VariableDeclarator" && ((_def$node$init = def.node.init) === null || _def$node$init === void 0 ? void 0 : _def$node$init.type) === "CallExpression" && isUseState(def.node.init.callee) && def.node.id.type === "ArrayPattern" && (def.node.id.elements.length === 1 || def.node.id.elements.length === 2) && ((_def$node$id$elements = def.node.id.elements[0]) === null || _def$node$id$elements === void 0 ? void 0 : _def$node$id$elements.type) === "Identifier" && def.node.id.elements[0].name === ref.identifier.name;
165
+ })) ?? false;
166
+ };
167
+ const isStateSetter = (ref) => {
168
+ var _ref$resolved3;
169
+ return ((_ref$resolved3 = ref.resolved) === null || _ref$resolved3 === void 0 ? void 0 : _ref$resolved3.defs.some((def) => {
170
+ var _def$node$init2, _def$node$id$elements2;
171
+ return def.node.type === "VariableDeclarator" && ((_def$node$init2 = def.node.init) === null || _def$node$init2 === void 0 ? void 0 : _def$node$init2.type) === "CallExpression" && isUseState(def.node.init.callee) && def.node.id.type === "ArrayPattern" && def.node.id.elements.length === 2 && ((_def$node$id$elements2 = def.node.id.elements[1]) === null || _def$node$id$elements2 === void 0 ? void 0 : _def$node$id$elements2.type) === "Identifier" && def.node.id.elements[1].name === ref.identifier.name;
172
+ })) ?? false;
173
+ };
174
+ const isProp = (context, ref) => {
175
+ var _ref$resolved4;
176
+ return ((_ref$resolved4 = ref.resolved) === null || _ref$resolved4 === void 0 ? void 0 : _ref$resolved4.defs.some((def) => {
177
+ const declaringNode = def.node.type === "ArrowFunctionExpression" ? def.node.parent.type === "CallExpression" ? def.node.parent.parent : def.node.parent : def.node;
178
+ return def.type === "Parameter" && (isFunctionalComponent(declaringNode) && !isFunctionalHOC(context, declaringNode) || isCustomHook(declaringNode));
179
+ })) ?? false;
180
+ };
181
+ const isConstant = (ref) => {
182
+ var _ref$resolved5;
183
+ return (((_ref$resolved5 = ref.resolved) === null || _ref$resolved5 === void 0 ? void 0 : _ref$resolved5.defs) ?? []).some((def) => {
184
+ var _def$node$init3, _def$node$init4, _def$node$init5, _def$node$init6;
185
+ return def.node.type === "VariableDeclarator" && ((_def$node$init3 = def.node.init) === null || _def$node$init3 === void 0 ? void 0 : _def$node$init3.type) === "Literal" || ((_def$node$init4 = def.node.init) === null || _def$node$init4 === void 0 ? void 0 : _def$node$init4.type) === "TemplateLiteral" || ((_def$node$init5 = def.node.init) === null || _def$node$init5 === void 0 ? void 0 : _def$node$init5.type) === "ArrayExpression" || ((_def$node$init6 = def.node.init) === null || _def$node$init6 === void 0 ? void 0 : _def$node$init6.type) === "ObjectExpression";
186
+ });
187
+ };
188
+ const isRef = (ref) => {
189
+ var _ref$resolved6;
190
+ return ((_ref$resolved6 = ref.resolved) === null || _ref$resolved6 === void 0 ? void 0 : _ref$resolved6.defs.some((def) => {
191
+ var _def$node$init7;
192
+ return def.node.type === "VariableDeclarator" && ((_def$node$init7 = def.node.init) === null || _def$node$init7 === void 0 ? void 0 : _def$node$init7.type) === "CallExpression" && (def.node.init.callee.type === "Identifier" && def.node.init.callee.name === "useRef" || def.node.init.callee.type === "MemberExpression" && def.node.init.callee.object.type === "Identifier" && def.node.init.callee.object.name === "React" && def.node.init.callee.property.type === "Identifier" && def.node.init.callee.property.name === "useRef");
193
+ })) ?? false;
194
+ };
195
+ const isRefCurrent = (ref) => {
196
+ const parent = ref.identifier.parent;
197
+ return parent.type === "MemberExpression" && parent.property.type === "Identifier" && parent.property.name === "current";
198
+ };
199
+ const isStateCall = (context, ref) => getSynchronousCallChain(context, ref).some((callChainRef) => isStateSetter(callChainRef));
200
+ const isPropCall = (context, ref) => getSynchronousCallChain(context, ref).some((callChainRef) => isProp(context, callChainRef));
201
+ const isRefCall = (context, ref) => getSynchronousCallChain(context, ref).some((callChainRef) => isRefCurrent(callChainRef) || isRef(callChainRef));
202
+ const getStateName = (context, ref) => {
203
+ var _ref;
204
+ const decl = getUseStateDecl(context, ref);
205
+ if (!decl || decl.id.type !== "ArrayPattern") return void 0;
206
+ const elements = decl.id.elements;
207
+ const first = elements[0];
208
+ const second = elements[1];
209
+ return (_ref = ((first === null || first === void 0 ? void 0 : first.type) === "Identifier" ? first : void 0) ?? ((second === null || second === void 0 ? void 0 : second.type) === "Identifier" ? second : void 0)) === null || _ref === void 0 ? void 0 : _ref.name;
210
+ };
211
+ const getUseStateDecl = (context, ref) => {
212
+ var _getUpstreamRefs$find;
213
+ let result = (_getUpstreamRefs$find = getUpstreamRefs(context, ref).find((upRef) => isUseState(upRef.identifier))) === null || _getUpstreamRefs$find === void 0 ? void 0 : _getUpstreamRefs$find.identifier;
214
+ while (result && result.type !== "VariableDeclarator") result = result.parent;
215
+ return result;
216
+ };
217
+ const findEnclosingReactNode = (context, node) => {
218
+ if (!node) return;
219
+ else if (isFunctionalComponent(node) || isFunctionalHOC(context, node) || isCustomHook(node)) return node;
220
+ else return findEnclosingReactNode(context, node.parent);
221
+ };
222
+ const getComponentName = (containingNode) => {
223
+ var _id;
224
+ if (!containingNode) return void 0;
225
+ if (containingNode.type !== "FunctionDeclaration" && containingNode.type !== "VariableDeclarator") return void 0;
226
+ return (_id = containingNode.id) === null || _id === void 0 ? void 0 : _id.name;
227
+ };
228
+ //#endregion
229
+ //#region src/rules/no-adjust-state-on-prop-change.ts
230
+ const rule$8 = {
231
+ meta: {
232
+ type: "suggestion",
233
+ docs: {
234
+ description: "Disallow adjusting state in an effect when a prop changes.",
235
+ url: "https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes"
236
+ },
237
+ schema: [],
238
+ messages: { avoidAdjustingStateWhenAPropChanges: "Avoid adjusting state when a prop changes. Instead, adjust \"{{state}}\" directly during render when {{props}} changes, or refactor your state to avoid this need entirely." }
239
+ },
240
+ create: (context) => ({ CallExpression: (node) => {
241
+ const effect = getEffect(context, node);
242
+ if (!effect || !effect.depsRefs) return;
243
+ const depsPropRefs = effect.depsRefs.flatMap((ref) => getUpstreamRefs(context, ref)).filter((ref) => isProp(context, ref));
244
+ if (depsPropRefs.length === 0) return;
245
+ effect.fnRefs.filter((ref) => isSynchronous(ref.identifier, effect.fn)).filter((ref) => isStateCall(context, ref)).forEach((ref) => {
246
+ const callExpr = getCallExpr(ref);
247
+ if (!callExpr) return;
248
+ if (getArgsUpstreamRefs(context, ref).some((ref) => isProp(context, ref))) return;
249
+ const stateName = getStateName(context, ref);
250
+ if (!stateName) return;
251
+ context.report({
252
+ node: callExpr,
253
+ messageId: "avoidAdjustingStateWhenAPropChanges",
254
+ data: {
255
+ state: stateName,
256
+ props: depsPropRefs.map((ref) => ref.identifier.name).map((n) => `"${n}"`).join(" and ")
257
+ }
258
+ });
259
+ });
260
+ } })
261
+ };
262
+ //#endregion
263
+ //#region src/rules/no-reset-all-state-on-prop-change.ts
264
+ const rule$7 = {
265
+ meta: {
266
+ type: "suggestion",
267
+ docs: {
268
+ description: "Disallow resetting all state in an effect when a prop changes.",
269
+ url: "https://react.dev/learn/you-might-not-need-an-effect#resetting-all-state-when-a-prop-changes"
270
+ },
271
+ schema: [],
272
+ messages: { avoidResettingAllStateWhenAPropChanges: "Avoid resetting all state when a prop changes. Instead, if \"{{prop}}\" is a key, pass it as \"key\" so React will reset the component's state." }
273
+ },
274
+ create: (context) => ({ CallExpression: (node) => {
275
+ const effect = getEffect(context, node);
276
+ if (!effect || !effect.depsRefs) return;
277
+ const containingNode = findEnclosingReactNode(context, node);
278
+ if (containingNode && isCustomHook(containingNode)) return;
279
+ const propUsedToResetAllState = findPropUsedToResetAllState(context, effect.fnRefs, effect.depsRefs, node);
280
+ if (propUsedToResetAllState) context.report({
281
+ node,
282
+ messageId: "avoidResettingAllStateWhenAPropChanges",
283
+ data: { prop: propUsedToResetAllState.identifier.name }
284
+ });
285
+ } })
286
+ };
287
+ const findPropUsedToResetAllState = (context, effectFnRefs, depsRefs, useEffectNode) => {
288
+ const stateSetterRefs = effectFnRefs.filter((ref) => isStateCall(context, ref));
289
+ return stateSetterRefs.length > 0 && stateSetterRefs.every((ref) => isSetStateToInitialValue(context, ref)) && stateSetterRefs.length === countUseStates(context, findEnclosingReactNode(context, useEffectNode)) ? depsRefs.flatMap((ref) => getUpstreamRefs(context, ref)).find((ref) => isProp(context, ref)) : void 0;
290
+ };
291
+ const isSetStateToInitialValue = (context, setterRef) => {
292
+ const callExpr = getCallExpr(setterRef);
293
+ if (!callExpr || callExpr.type !== "CallExpression") return false;
294
+ const setStateToValue = callExpr.arguments[0];
295
+ const useStateDecl = getUseStateDecl(context, setterRef);
296
+ if (!useStateDecl || !useStateDecl.init || useStateDecl.init.type !== "CallExpression") return false;
297
+ const stateInitialValue = useStateDecl.init.arguments[0];
298
+ const isUndefined = (node) => node === void 0 || "name" in node && node.name === "undefined";
299
+ if (isUndefined(setStateToValue) && isUndefined(stateInitialValue)) return true;
300
+ if (setStateToValue === null && stateInitialValue === null) return true;
301
+ else if (setStateToValue && !stateInitialValue || !setStateToValue && stateInitialValue) return false;
302
+ if (!setStateToValue || !stateInitialValue) return false;
303
+ return context.sourceCode.getText(setStateToValue) === context.sourceCode.getText(stateInitialValue);
304
+ };
305
+ const countUseStates = (context, componentNode) => {
306
+ var _componentNode$init;
307
+ if (!componentNode) return 0;
308
+ if (componentNode.type === "VariableDeclarator" && ((_componentNode$init = componentNode.init) === null || _componentNode$init === void 0 ? void 0 : _componentNode$init.type) === "CallExpression") componentNode = componentNode.init.arguments[0];
309
+ return getDownstreamRefs(context, componentNode).filter((ref) => isState(ref)).length;
310
+ };
311
+ //#endregion
312
+ //#region src/index.ts
313
+ const plugin = {
314
+ meta: {
315
+ name: "react-you-might-not-need-an-effect",
316
+ version: "1.0.2"
317
+ },
318
+ rules: {
319
+ "no-derived-state": {
320
+ meta: {
321
+ type: "suggestion",
322
+ docs: {
323
+ description: "Disallow storing derived state in an effect.",
324
+ url: "https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state"
325
+ },
326
+ schema: [],
327
+ messages: { avoidDerivedState: "Avoid storing derived state. Instead, compute \"{{state}}\" directly during render." }
328
+ },
329
+ create: (context) => ({ CallExpression: (node) => {
330
+ const effect = getEffect(context, node);
331
+ if (!effect || effect.cleanup) return;
332
+ effect.fnRefs.filter((ref) => isSynchronous(ref.identifier, effect.fn)).filter((ref) => isStateCall(context, ref)).forEach((ref) => {
333
+ const callExpr = getCallExpr(ref);
334
+ if (!callExpr) return;
335
+ const stateName = getStateName(context, ref);
336
+ if (!stateName) return;
337
+ if (getArgsUpstreamRefs(context, ref).some((ref) => isState(ref) || isProp(context, ref))) context.report({
338
+ node: callExpr,
339
+ messageId: "avoidDerivedState",
340
+ data: { state: stateName }
341
+ });
342
+ });
343
+ } })
344
+ },
345
+ "no-chain-state-updates": {
346
+ meta: {
347
+ type: "suggestion",
348
+ docs: {
349
+ description: "Disallow chaining state changes in an effect.",
350
+ url: "https://react.dev/learn/you-might-not-need-an-effect#chains-of-computations"
351
+ },
352
+ schema: [],
353
+ messages: { avoidChainingStateUpdates: "Avoid chaining state changes. When possible, update \"{{state}}\" along with other relevant state simultaneously." }
354
+ },
355
+ create: (context) => ({ CallExpression: (node) => {
356
+ const effect = getEffect(context, node);
357
+ if (!effect || effect.cleanup || !effect.depsRefs) return;
358
+ const isSomeDepsState = effect.depsRefs.flatMap((ref) => getUpstreamRefs(context, ref)).some((ref) => isState(ref));
359
+ effect.fnRefs.filter((ref) => isSynchronous(ref.identifier, effect.fn)).filter((ref) => isStateCall(context, ref)).forEach((ref) => {
360
+ const callExpr = getCallExpr(ref);
361
+ if (!callExpr) return;
362
+ const isSomeArgsState = getArgsUpstreamRefs(context, ref).some((ref) => isState(ref));
363
+ if (isSomeDepsState && !isSomeArgsState) {
364
+ const stateName = getStateName(context, ref);
365
+ if (!stateName) return;
366
+ context.report({
367
+ node: callExpr,
368
+ messageId: "avoidChainingStateUpdates",
369
+ data: { state: stateName }
370
+ });
371
+ }
372
+ });
373
+ } })
374
+ },
375
+ "no-event-handler": {
376
+ meta: {
377
+ type: "suggestion",
378
+ docs: {
379
+ description: "Disallow using state and an effect as an event handler.",
380
+ url: "https://react.dev/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers"
381
+ },
382
+ schema: [],
383
+ messages: {
384
+ avoidEventHandler: "Avoid using state and effects as an event handler. Instead, call the code that uses \"{{name}}\" directly when the event occurs.",
385
+ avoidPropHandler: "Avoid using props and effects as an event handler. Instead, move the code that uses \"{{name}}\" to the parent component."
386
+ }
387
+ },
388
+ create: (context) => ({ CallExpression: (node) => {
389
+ const effect = getEffect(context, node);
390
+ if (!effect || effect.cleanup) return;
391
+ findDownstreamNodes(context, effect.fn, "IfStatement").filter((ifNode) => ifNode.type === "IfStatement" && !ifNode.alternate).map((ifNode) => ifNode.test).flatMap((ifTestNode) => getDownstreamRefs(context, ifTestNode)).forEach((ifTestRef) => {
392
+ const upstreamRefs = getUpstreamRefs(context, ifTestRef);
393
+ const name = ifTestRef.identifier.name;
394
+ if (upstreamRefs.some((ref) => isState(ref))) context.report({
395
+ node: ifTestRef.identifier,
396
+ messageId: "avoidEventHandler",
397
+ data: { name }
398
+ });
399
+ if (upstreamRefs.some((ref) => isProp(context, ref))) context.report({
400
+ node: ifTestRef.identifier,
401
+ messageId: "avoidPropHandler",
402
+ data: { name }
403
+ });
404
+ });
405
+ } })
406
+ },
407
+ "no-adjust-state-on-prop-change": rule$8,
408
+ "no-reset-all-state-on-prop-change": rule$7,
409
+ "no-pass-live-state-to-parent": {
410
+ meta: {
411
+ type: "suggestion",
412
+ docs: {
413
+ description: "Disallow passing live state to parents in an effect.",
414
+ url: "https://react.dev/learn/you-might-not-need-an-effect#notifying-parent-components-about-state-changes"
415
+ },
416
+ schema: [],
417
+ messages: {
418
+ avoidPassingLiveStateToParentInComponent: "Avoid passing live state to parents in an effect. Instead, lift \"{{state}}\" to the parent and pass it down to {{name}} as a prop.",
419
+ avoidPassingLiveStateToParentInHook: "Avoid passing live state to parents in an effect. Instead, return \"{{state}}\" from {{name}}."
420
+ }
421
+ },
422
+ create: (context) => ({ CallExpression: (node) => {
423
+ const effect = getEffect(context, node);
424
+ if (!effect) return;
425
+ effect.fnRefs.filter((ref) => isSynchronous(ref.identifier, effect.fn)).filter((ref) => isPropCall(context, ref)).forEach((ref) => {
426
+ const callExpr = getCallExpr(ref);
427
+ if (!callExpr) return;
428
+ const stateRefs = getArgsUpstreamRefs(context, ref).filter((r) => isState(r));
429
+ if (stateRefs.length === 0) return;
430
+ const containingNode = findEnclosingReactNode(context, node);
431
+ const isInCustomHook = containingNode && isCustomHook(containingNode);
432
+ context.report({
433
+ node: callExpr,
434
+ messageId: isInCustomHook ? "avoidPassingLiveStateToParentInHook" : "avoidPassingLiveStateToParentInComponent",
435
+ data: {
436
+ state: stateRefs.map((r) => r.identifier.name).map((n) => `"${n}"`).join(" and "),
437
+ name: (() => {
438
+ const n = getComponentName(containingNode);
439
+ return n ? `"${n}"` : isInCustomHook ? "this custom hook" : "this component";
440
+ })()
441
+ }
442
+ });
443
+ });
444
+ } })
445
+ },
446
+ "no-pass-data-to-parent": {
447
+ meta: {
448
+ type: "suggestion",
449
+ docs: {
450
+ description: "Disallow passing data to parents in an effect.",
451
+ url: "https://react.dev/learn/you-might-not-need-an-effect#passing-data-to-the-parent"
452
+ },
453
+ schema: [],
454
+ messages: {
455
+ avoidPassingDataToParentInComponent: "Avoid passing data to parents in an effect. Instead, fetch \"{{data}}\" in the parent and pass it down to {{name}} as a prop.",
456
+ avoidPassingDataToParentInHook: "Avoid passing data to parents in an effect. Instead, return \"{{data}}\" from {{name}}."
457
+ }
458
+ },
459
+ create: (context) => ({ CallExpression: (node) => {
460
+ const effect = getEffect(context, node);
461
+ if (!effect || effect.cleanup) return;
462
+ effect.fnRefs.filter((ref) => isSynchronous(ref.identifier, effect.fn)).filter((ref) => isPropCall(context, ref)).filter((ref) => !isRefCall(context, ref)).forEach((ref) => {
463
+ const callExpr = getCallExpr(ref);
464
+ if (!callExpr) return;
465
+ const dataArgs = getArgsUpstreamRefs(context, ref).filter((ref) => getUpstreamRefs(context, ref).length === 1).filter((ref) => !isUseState(ref.identifier) && !isProp(context, ref) && !isUseRef(ref.identifier) && !isRefCurrent(ref) && !isConstant(ref));
466
+ if (dataArgs.length === 0) return;
467
+ const containingNode = findEnclosingReactNode(context, node);
468
+ const isInCustomHook = containingNode && isCustomHook(containingNode);
469
+ context.report({
470
+ node: callExpr,
471
+ messageId: isInCustomHook ? "avoidPassingDataToParentInHook" : "avoidPassingDataToParentInComponent",
472
+ data: {
473
+ data: dataArgs.map((r) => r.identifier.name).map((n) => `"${n}"`).join(" and "),
474
+ name: (() => {
475
+ const n = getComponentName(containingNode);
476
+ return n ? `"${n}"` : isInCustomHook ? "this custom hook" : "this component";
477
+ })()
478
+ }
479
+ });
480
+ });
481
+ } })
482
+ },
483
+ "no-external-store-subscription": {
484
+ meta: {
485
+ type: "suggestion",
486
+ docs: {
487
+ description: "Disallow subscribing to an external store in an effect.",
488
+ url: "https://react.dev/learn/you-might-not-need-an-effect#subscribing-to-an-external-store"
489
+ },
490
+ schema: [],
491
+ messages: { avoidExternalStoreSubscription: "Avoid using an effect to subscribe to an external store. Instead, use \"useSyncExternalStore\" to manage \"{{state}}\"." }
492
+ },
493
+ create: (context) => ({ CallExpression: (node) => {
494
+ const effect = getEffect(context, node);
495
+ if (!effect || !effect.cleanup) return;
496
+ const bodySetters = effect.fnRefs.filter((ref) => isSynchronous(ref.identifier, effect.fn)).filter((ref) => isStateCall(context, ref));
497
+ if (bodySetters.length === 0) return;
498
+ const cleanupArg = effect.cleanup.argument;
499
+ if (!cleanupArg) return;
500
+ const cleanupRefs = getDownstreamRefs(context, cleanupArg);
501
+ findDownstreamNodes(context, cleanupArg, "CallExpression").forEach((callExpr) => {
502
+ if (callExpr.type !== "CallExpression") return;
503
+ for (const arg of callExpr.arguments) {
504
+ if (!arg) continue;
505
+ cleanupRefs.push(...getDownstreamRefs(context, arg));
506
+ }
507
+ });
508
+ const cleanupVars = /* @__PURE__ */ new Set();
509
+ for (const ref of cleanupRefs) for (const upRef of getUpstreamRefs(context, ref)) if (upRef.resolved) cleanupVars.add(upRef.resolved);
510
+ for (const ref of bodySetters) {
511
+ if (!getUpstreamRefs(context, ref).some((upRef) => upRef.resolved && cleanupVars.has(upRef.resolved))) continue;
512
+ const callExpr = getCallExpr(ref);
513
+ if (!callExpr) continue;
514
+ const stateName = getStateName(context, ref);
515
+ if (!stateName) continue;
516
+ context.report({
517
+ node: callExpr,
518
+ messageId: "avoidExternalStoreSubscription",
519
+ data: { state: stateName }
520
+ });
521
+ }
522
+ } })
523
+ },
524
+ "no-initialize-state": {
525
+ meta: {
526
+ type: "suggestion",
527
+ docs: {
528
+ description: "Disallow initializing state in an effect.",
529
+ url: "https://tkdodo.eu/blog/avoiding-hydration-mismatches-with-use-sync-external-store"
530
+ },
531
+ schema: [],
532
+ messages: { avoidInitializingState: "Avoid initializing state in an effect. Instead, initialize \"{{state}}\"'s \"useState()\" with \"{{arguments}}\". For SSR hydration, prefer \"useSyncExternalStore\"." }
533
+ },
534
+ create: (context) => ({ CallExpression: (node) => {
535
+ const effect = getEffect(context, node);
536
+ if (!effect || !effect.depsRefs) return;
537
+ if (!(effect.depsRefs.filter((ref) => !isStateSetter(ref)).length === 0)) return;
538
+ effect.fnRefs.filter((ref) => isSynchronous(ref.identifier, effect.fn)).filter((ref) => isStateCall(context, ref)).forEach((ref) => {
539
+ const callExpr = getCallExpr(ref);
540
+ if (!callExpr) return;
541
+ const stateName = getStateName(context, ref);
542
+ if (!stateName) return;
543
+ let argumentText = "undefined";
544
+ if (callExpr && callExpr.type === "CallExpression" && callExpr.arguments[0]) argumentText = context.sourceCode.getText(callExpr.arguments[0]);
545
+ context.report({
546
+ node: callExpr,
547
+ messageId: "avoidInitializingState",
548
+ data: {
549
+ state: stateName,
550
+ arguments: argumentText
551
+ }
552
+ });
553
+ });
554
+ } })
555
+ }
556
+ },
557
+ configs: {}
558
+ };
559
+ export { plugin as default };