eslint-plugin-effector 0.16.0 → 0.18.0

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.
Files changed (53) hide show
  1. package/README.md +24 -37
  2. package/dist/index.cjs +1488 -0
  3. package/dist/index.d.cts +164 -0
  4. package/dist/index.d.mts +165 -0
  5. package/dist/index.mjs +1465 -0
  6. package/package.json +73 -17
  7. package/.nvmrc +0 -1
  8. package/config/future.js +0 -7
  9. package/config/patronum.js +0 -5
  10. package/config/react.js +0 -7
  11. package/config/recommended.js +0 -15
  12. package/config/scope.js +0 -6
  13. package/index.js +0 -31
  14. package/rules/enforce-effect-naming-convention/enforce-effect-naming-convention.js +0 -143
  15. package/rules/enforce-gate-naming-convention/enforce-gate-naming-convention.js +0 -122
  16. package/rules/enforce-store-naming-convention/enforce-store-naming-convention.js +0 -205
  17. package/rules/keep-options-order/config.js +0 -3
  18. package/rules/keep-options-order/keep-options-order.js +0 -107
  19. package/rules/mandatory-scope-binding/mandatory-scope-binding.js +0 -81
  20. package/rules/no-ambiguity-target/no-ambiguity-target.js +0 -74
  21. package/rules/no-duplicate-clock-or-source-array-values/no-duplicate-clock-or-source-array-values.js +0 -124
  22. package/rules/no-duplicate-on/no-duplicate-on.js +0 -137
  23. package/rules/no-forward/no-forward.js +0 -73
  24. package/rules/no-getState/no-getState.js +0 -50
  25. package/rules/no-guard/no-guard.js +0 -78
  26. package/rules/no-patronum-debug/no-patronum-debug.js +0 -133
  27. package/rules/no-unnecessary-combination/no-unnecessary-combination.js +0 -88
  28. package/rules/no-unnecessary-duplication/no-unnecessary-duplication.js +0 -115
  29. package/rules/no-useless-methods/no-useless-methods.js +0 -93
  30. package/rules/no-watch/no-watch.js +0 -61
  31. package/rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping.js +0 -111
  32. package/rules/prefer-useUnit/prefer-useUnit.js +0 -56
  33. package/rules/require-pickup-in-persist/require-pickup-in-persist.js +0 -47
  34. package/rules/strict-effect-handlers/strict-effect-handlers.js +0 -76
  35. package/utils/are-nodes-same-in-text.js +0 -22
  36. package/utils/builders.js +0 -19
  37. package/utils/create-link-to-rule.js +0 -5
  38. package/utils/extract-config.js +0 -26
  39. package/utils/extract-imported-from.js +0 -18
  40. package/utils/get-corrected-store-name.js +0 -45
  41. package/utils/get-nested-object-name.js +0 -18
  42. package/utils/get-store-name-convention.js +0 -6
  43. package/utils/is.js +0 -39
  44. package/utils/method.js +0 -23
  45. package/utils/naming.js +0 -47
  46. package/utils/node-is-type.js +0 -5
  47. package/utils/node-type-is.js +0 -106
  48. package/utils/react.js +0 -214
  49. package/utils/read-example.js +0 -63
  50. package/utils/replace-by-sample.js +0 -98
  51. package/utils/traverse-nested-object-node.js +0 -9
  52. package/utils/traverse-parent-by-type.js +0 -15
  53. package/utils/validate-store-name-convention.js +0 -13
package/dist/index.mjs ADDED
@@ -0,0 +1,1465 @@
1
+ import { ASTUtils, AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils";
2
+ import { getContextualType, typeMatchesSpecifier } from "@typescript-eslint/type-utils";
3
+ import { isExpression } from "typescript";
4
+ import esquery from "esquery";
5
+ //#region package.json
6
+ var name = "eslint-plugin-effector";
7
+ var version = "0.18.0";
8
+ //#endregion
9
+ //#region src/shared/create.ts
10
+ const createRule = ESLintUtils.RuleCreator((name) => `https://eslint.effector.dev/rules/${name}`);
11
+ //#endregion
12
+ //#region src/shared/is.ts
13
+ const check = (symbol, types, from) => {
14
+ const name = symbol.getName();
15
+ const declarations = symbol.declarations ?? [];
16
+ return types.includes(name) && declarations.map((decl) => decl.getSourceFile().fileName).some((fname) => fname.includes("node_modules") && fname.includes(from));
17
+ };
18
+ const isType = {
19
+ store: (type, program) => typeMatchesSpecifier(type, {
20
+ from: "package",
21
+ package: "effector",
22
+ name: ["Store", "StoreWritable"]
23
+ }, program),
24
+ event: (type, program) => typeMatchesSpecifier(type, {
25
+ from: "package",
26
+ package: "effector",
27
+ name: ["Event", "EventCallable"]
28
+ }, program),
29
+ effect: (type, program) => typeMatchesSpecifier(type, {
30
+ from: "package",
31
+ package: "effector",
32
+ name: "Effect"
33
+ }, program),
34
+ unit: (type, program) => {
35
+ return typeMatchesSpecifier(type, {
36
+ from: "package",
37
+ package: "effector",
38
+ name: [
39
+ "Store",
40
+ "StoreWritable",
41
+ "Event",
42
+ "EventCallable",
43
+ "Effect",
44
+ "Domain"
45
+ ]
46
+ }, program);
47
+ },
48
+ domain: (type, program) => typeMatchesSpecifier(type, {
49
+ from: "package",
50
+ package: "effector",
51
+ name: "Domain"
52
+ }, program),
53
+ gate: (type) => {
54
+ const symbol = type.getSymbol() ?? type.aliasSymbol;
55
+ return symbol ? check(symbol, ["Gate"], "effector") : false;
56
+ },
57
+ jsx: (type, program) => {
58
+ return typeMatchesSpecifier(type, {
59
+ from: "package",
60
+ package: "react",
61
+ name: [
62
+ "Element",
63
+ "ReactNode",
64
+ "ReactElement"
65
+ ]
66
+ }, program);
67
+ },
68
+ component: (type, program) => {
69
+ return typeMatchesSpecifier(type, {
70
+ from: "package",
71
+ package: "react",
72
+ name: [
73
+ "FC",
74
+ "FunctionComponent",
75
+ "ComponentType",
76
+ "ComponentClass",
77
+ "ForwardRefRenderFunction"
78
+ ]
79
+ }, program);
80
+ }
81
+ };
82
+ //#endregion
83
+ //#region src/rules/enforce-effect-naming-convention/enforce-effect-naming-convention.ts
84
+ var enforce_effect_naming_convention_default = createRule({
85
+ name: "enforce-effect-naming-convention",
86
+ meta: {
87
+ type: "problem",
88
+ docs: { description: "Enforce Fx as a suffix for any Effector Effect." },
89
+ messages: {
90
+ invalid: "Effect `{{ current }}` should be named with suffix, rename it to `{{ fixed }}`",
91
+ rename: "Rename \"{{ current }}\" to \"{{ fixed }}\""
92
+ },
93
+ schema: [],
94
+ hasSuggestions: true
95
+ },
96
+ defaultOptions: [],
97
+ create: (context) => {
98
+ const services = ESLintUtils.getParserServices(context);
99
+ return { [`VariableDeclarator[id.name!=${FxRegex}]`]: (node) => {
100
+ const type = services.getTypeAtLocation(node);
101
+ if (!isType.effect(type, services.program)) return;
102
+ const current = node.id.name;
103
+ const fixed = current + "Fx";
104
+ const data = {
105
+ current,
106
+ fixed
107
+ };
108
+ const suggestion = {
109
+ messageId: "rename",
110
+ data: {
111
+ current,
112
+ fixed
113
+ },
114
+ fix: (fixer) => fixer.replaceText(node.id, fixed)
115
+ };
116
+ context.report({
117
+ node: node.id,
118
+ messageId: "invalid",
119
+ data,
120
+ suggest: [suggestion]
121
+ });
122
+ } };
123
+ }
124
+ });
125
+ const FxRegex = /Fx$/;
126
+ //#endregion
127
+ //#region src/rules/enforce-gate-naming-convention/enforce-gate-naming-convention.ts
128
+ var enforce_gate_naming_convention_default = createRule({
129
+ name: "enforce-gate-naming-convention",
130
+ meta: {
131
+ type: "problem",
132
+ docs: { description: "Enforce a Gate is named capitalized like a React Component" },
133
+ messages: {
134
+ invalid: "Gate \"{{ current }}\" should be named with first capital letter, rename it to \"{{ fixed }}\"",
135
+ rename: "Rename \"{{ current }}\" to \"{{ fixed }}\""
136
+ },
137
+ schema: [],
138
+ hasSuggestions: true
139
+ },
140
+ defaultOptions: [],
141
+ create: (context) => {
142
+ const services = ESLintUtils.getParserServices(context);
143
+ return { [`VariableDeclarator[id.name=${GateRegex}]`]: (node) => {
144
+ const type = services.getTypeAtLocation(node);
145
+ if (!isType.gate(type)) return;
146
+ const current = node.id.name;
147
+ const fixed = current[0].toUpperCase() + current.slice(1);
148
+ const data = {
149
+ current,
150
+ fixed
151
+ };
152
+ const suggestion = {
153
+ messageId: "rename",
154
+ data: {
155
+ current,
156
+ fixed
157
+ },
158
+ fix: (fixer) => fixer.replaceText(node.id, fixed)
159
+ };
160
+ context.report({
161
+ node: node.id,
162
+ messageId: "invalid",
163
+ data,
164
+ suggest: [suggestion]
165
+ });
166
+ } };
167
+ }
168
+ });
169
+ const GateRegex = /^[^A-Z]/;
170
+ //#endregion
171
+ //#region src/rules/enforce-store-naming-convention/enforce-store-naming-convention.ts
172
+ var enforce_store_naming_convention_default = createRule({
173
+ name: "enforce-store-naming-convention",
174
+ meta: {
175
+ type: "problem",
176
+ docs: { description: "Enforce $ as a prefix/postfix for any Effector `Store`" },
177
+ messages: {
178
+ invalid: "Store \"{{ current }}\" should be named with a `$` {{ convention }}, rename it to \"{{ fixed }}\"",
179
+ rename: "Rename \"{{ current }}\" to \"{{ fixed }}\""
180
+ },
181
+ schema: [{
182
+ type: "object",
183
+ properties: { mode: {
184
+ type: "string",
185
+ enum: ["prefix", "postfix"]
186
+ } }
187
+ }],
188
+ hasSuggestions: true
189
+ },
190
+ defaultOptions: [{ mode: "prefix" }],
191
+ create: (context, [options]) => {
192
+ const services = ESLintUtils.getParserServices(context);
193
+ return { [`VariableDeclarator[id.name=${options.mode === "prefix" ? PrefixRegex : PostfixRegex}]`]: (node) => {
194
+ const type = services.getTypeAtLocation(node);
195
+ if (!isType.store(type, services.program)) return;
196
+ const current = node.id.name;
197
+ const trimmed = current.replaceAll(options.mode === "prefix" ? /\$+$/g : /^\$+/g, "");
198
+ const fixed = options.mode === "prefix" ? `$${trimmed}` : `${trimmed}$`;
199
+ const data = {
200
+ current,
201
+ convention: options.mode,
202
+ fixed
203
+ };
204
+ const suggestion = {
205
+ messageId: "rename",
206
+ data: {
207
+ current,
208
+ fixed
209
+ },
210
+ fix: (fixer) => fixer.replaceText(node.id, fixed)
211
+ };
212
+ context.report({
213
+ node: node.id,
214
+ messageId: "invalid",
215
+ data,
216
+ suggest: [suggestion]
217
+ });
218
+ } };
219
+ }
220
+ });
221
+ const PrefixRegex = /^[^$]/;
222
+ const PostfixRegex = /[^$]$/;
223
+ //#endregion
224
+ //#region src/shared/package.ts
225
+ const PACKAGE_NAME$1 = {
226
+ core: /^effector(?:\u002Fcompat)?$/,
227
+ react: /^effector-react$/,
228
+ storage: /^@?effector-storage(\u002F[\w-]+)*$/
229
+ };
230
+ //#endregion
231
+ //#region src/rules/keep-options-order/keep-options-order.ts
232
+ var keep_options_order_default = createRule({
233
+ name: "keep-options-order",
234
+ meta: {
235
+ type: "problem",
236
+ docs: { description: "Enforce options order for Effector methods" },
237
+ messages: {
238
+ invalidOrder: `Order of options should be \`{{ correctOrder }}\`, but found \`{{ currentOrder }}\`.`,
239
+ changeOrder: "Sort options to follow the recommended order."
240
+ },
241
+ schema: [],
242
+ hasSuggestions: true
243
+ },
244
+ defaultOptions: [],
245
+ create: (context) => {
246
+ const source = context.sourceCode;
247
+ const imports = /* @__PURE__ */ new Set();
248
+ return {
249
+ [`${`ImportDeclaration[source.value=${PACKAGE_NAME$1.core}]`} > ${selector$11.method}`]: (node) => imports.add(node.local.name),
250
+ [`CallExpression${selector$11.call}:has(${selector$11.argument})`]: (node) => {
251
+ if (!imports.has(node.callee.name)) return;
252
+ const [config] = node.arguments;
253
+ if (config.properties.some((prop) => prop.type === AST_NODE_TYPES.SpreadElement || prop.key.type !== AST_NODE_TYPES.Identifier)) return;
254
+ const properties = config.properties;
255
+ const current = properties.map((prop) => prop.key.name);
256
+ if (isCorrectOrder(current)) return;
257
+ const correctOrder = TRUE_ORDER.filter((item) => current.includes(item));
258
+ const othersOrder = current.filter((item) => !TRUE_ORDER.includes(item));
259
+ const order = [...correctOrder, ...othersOrder];
260
+ const snippets = properties.toSorted((a, b) => order.indexOf(a.key.name) - order.indexOf(b.key.name)).map((prop) => source.getText(prop));
261
+ const suggestion = {
262
+ messageId: "changeOrder",
263
+ fix: (fixer) => [fixer.replaceText(config, `{ ${snippets.join(", ")} }`)]
264
+ };
265
+ const data = {
266
+ correctOrder: correctOrder.join(" -> "),
267
+ currentOrder: current.join(" -> ")
268
+ };
269
+ context.report({
270
+ node: config,
271
+ messageId: "invalidOrder",
272
+ data,
273
+ suggest: [suggestion]
274
+ });
275
+ }
276
+ };
277
+ }
278
+ });
279
+ const TRUE_ORDER = [
280
+ "clock",
281
+ "source",
282
+ "filter",
283
+ "fn",
284
+ "target",
285
+ "greedy",
286
+ "batch",
287
+ "name"
288
+ ];
289
+ const selector$11 = {
290
+ method: `ImportSpecifier[imported.name=/(sample|guard)/]`,
291
+ call: `[callee.type="Identifier"][arguments.length=1]`,
292
+ argument: `ObjectExpression.arguments`
293
+ };
294
+ const isCorrectOrder = (current) => {
295
+ let seen = -1;
296
+ for (const item of current) {
297
+ const index = TRUE_ORDER.indexOf(item);
298
+ const placement = index === -1 ? Infinity : index;
299
+ if (placement <= seen) return false;
300
+ seen = placement;
301
+ }
302
+ return true;
303
+ };
304
+ //#endregion
305
+ //#region src/shared/name.ts
306
+ function functionToName(node) {
307
+ if (node.id) return node.id;
308
+ if (node.parent.type === AST_NODE_TYPES.VariableDeclarator && node.parent.id.type === AST_NODE_TYPES.Identifier) return node.parent.id;
309
+ if (node.parent.type === AST_NODE_TYPES.AssignmentExpression && node.parent.left.type === AST_NODE_TYPES.Identifier) return node.parent.left;
310
+ if (node.parent.type === AST_NODE_TYPES.Property && node.parent.key.type === AST_NODE_TYPES.Identifier) return node.parent.key;
311
+ if (node.parent.type === AST_NODE_TYPES.AssignmentPattern && node.parent.left.type === AST_NODE_TYPES.Identifier) return node.parent.left;
312
+ return null;
313
+ }
314
+ const nameOf = { function: functionToName };
315
+ //#endregion
316
+ //#region src/rules/mandatory-scope-binding/mandatory-scope-binding.ts
317
+ var mandatory_scope_binding_default = createRule({
318
+ name: "mandatory-scope-binding",
319
+ meta: {
320
+ type: "problem",
321
+ docs: { description: "Forbid `Event` and `Effect` usage without `useUnit` in React components." },
322
+ messages: { useUnitNeeded: "\"{{ name }}\" must be wrapped with `useUnit` from `effector-react` before usage inside React components." },
323
+ schema: []
324
+ },
325
+ defaultOptions: [],
326
+ create: (context) => {
327
+ const services = ESLintUtils.getParserServices(context);
328
+ const checker = services.program.getTypeChecker();
329
+ const stack = {
330
+ render: [],
331
+ hook: []
332
+ };
333
+ return {
334
+ [`FunctionDeclaration, FunctionExpression, ArrowFunctionExpression`]: (node) => {
335
+ if (stack.render.at(-1) ?? false) return void stack.render.push(true);
336
+ const name = nameOf.function(node);
337
+ if (name && UseRegex$1.test(name.name)) return void stack.render.push(true);
338
+ const tsnode = services.esTreeNodeToTSNodeMap.get(node);
339
+ const signature = checker.getSignatureFromDeclaration(tsnode);
340
+ const returnType = signature ? checker.getReturnTypeOfSignature(signature) : checker.getVoidType();
341
+ if (returnType.isUnion() ? returnType.types.some((type) => isType.jsx(type, services.program)) : isType.jsx(returnType, services.program)) return void stack.render.push(true);
342
+ const inferred = isExpression(tsnode) && getContextualType(checker, tsnode) || checker.getUnknownType();
343
+ if (inferred.isUnion() ? inferred.types.some((type) => isType.component(type, services.program)) : isType.component(inferred, services.program)) return void stack.render.push(true);
344
+ stack.render.push(false);
345
+ },
346
+ [`:matches(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression):exit`]: () => void stack.render.pop(),
347
+ "ClassDeclaration": () => void stack.render.push(false),
348
+ "ClassDeclaration:exit": () => void stack.render.pop(),
349
+ "CallExpression": (node) => {
350
+ const isHook = typeMatchesSpecifier(services.getTypeAtLocation(node.callee), {
351
+ from: "package",
352
+ package: "effector-react",
353
+ name: [
354
+ "useStore",
355
+ "useStoreMap",
356
+ "useList",
357
+ "useEvent",
358
+ "useUnit"
359
+ ]
360
+ }, services.program);
361
+ stack.hook.push(isHook);
362
+ },
363
+ "Identifier": (node) => {
364
+ if (!(stack.render.at(-1) ?? false)) return;
365
+ if (stack.hook.at(-1) ?? false) return;
366
+ const type = services.getTypeAtLocation(node);
367
+ if (!isType.event(type, services.program) && !isType.effect(type, services.program)) return;
368
+ context.report({
369
+ node,
370
+ messageId: "useUnitNeeded",
371
+ data: { name: node.name }
372
+ });
373
+ }
374
+ };
375
+ }
376
+ });
377
+ const UseRegex$1 = /^use[A-Z0-9].*$/;
378
+ //#endregion
379
+ //#region src/shared/locate.ts
380
+ const property = (key, node) => node.properties.find((prop) => prop.type == AST_NODE_TYPES.Property && prop.key.type === AST_NODE_TYPES.Identifier && prop.key.name === key);
381
+ const locate = { property };
382
+ //#endregion
383
+ //#region src/rules/no-ambiguity-target/no-ambiguity-target.ts
384
+ var no_ambiguity_target_default = createRule({
385
+ name: "no-ambiguity-target",
386
+ meta: {
387
+ type: "problem",
388
+ docs: { description: "Forbid ambiguous target in `sample` and `guard`." },
389
+ messages: { ambiguous: "Method `{{ method }}` both specifies `target` option and assigns the result to a variable. Consider removing one of them." },
390
+ schema: []
391
+ },
392
+ defaultOptions: [],
393
+ create: (context) => {
394
+ const imports = /* @__PURE__ */ new Set();
395
+ const importSelector = `ImportDeclaration[source.value=${PACKAGE_NAME$1.core}]`;
396
+ const usageStack = [];
397
+ return {
398
+ "ReturnStatement": () => usageStack.push(true),
399
+ "ReturnStatement:exit": () => usageStack.pop(),
400
+ "VariableDeclarator": () => usageStack.push(true),
401
+ "VariableDeclarator:exit": () => usageStack.pop(),
402
+ "ObjectExpression": () => usageStack.push(true),
403
+ "ObjectExpression:exit": () => usageStack.pop(),
404
+ "BlockStatement": () => usageStack.push(false),
405
+ "BlockStatement:exit": () => usageStack.pop(),
406
+ [`${importSelector} > ${selector$10.method}`]: (node) => imports.add(node.local.name),
407
+ [`CallExpression[callee.type="Identifier"]`]: (node) => {
408
+ if (!imports.has(node.callee.name)) return;
409
+ if (!(usageStack.at(-1) ?? false)) return;
410
+ const [config] = node.arguments;
411
+ if (config?.type !== AST_NODE_TYPES.ObjectExpression) return;
412
+ if (!locate.property("target", config)) return;
413
+ context.report({
414
+ node,
415
+ messageId: "ambiguous",
416
+ data: { method: node.callee.name }
417
+ });
418
+ }
419
+ };
420
+ }
421
+ });
422
+ const selector$10 = { method: `ImportSpecifier[imported.name=/(sample|guard)/]` };
423
+ //#endregion
424
+ //#region src/rules/no-domain-unit-creators/no-domain-unit-creators.ts
425
+ var no_domain_unit_creators_default = createRule({
426
+ name: "no-domain-unit-creators",
427
+ meta: {
428
+ type: "suggestion",
429
+ docs: { description: "Disallow using Domain methods to create units." },
430
+ messages: { avoid: "Avoid using `.{{ method }}` on a Domain instance. Use a standard factory unit creator `{{ factory }}` with a `domain` option instead." },
431
+ schema: []
432
+ },
433
+ defaultOptions: [],
434
+ create: (context) => {
435
+ const services = ESLintUtils.getParserServices(context);
436
+ return { [`CallExpression:has(> ${selector$9.member})`]: (node) => {
437
+ const name = node.callee.property.name;
438
+ if (!METHODS.has(name)) return;
439
+ const type = services.getTypeAtLocation(node.callee.object);
440
+ if (!isType.domain(type, services.program)) return;
441
+ const factory = ALIAS_MAP.get(name) ?? name;
442
+ context.report({
443
+ node,
444
+ messageId: "avoid",
445
+ data: {
446
+ method: name,
447
+ factory
448
+ }
449
+ });
450
+ } };
451
+ }
452
+ });
453
+ const ALIAS_MAP = (/* @__PURE__ */ new Map()).set("event", "createEvent").set("store", "createStore").set("effect", "createEffect").set("domain", "createDomain");
454
+ const METHODS = new Set([...ALIAS_MAP.values(), ...ALIAS_MAP.keys()]);
455
+ const selector$9 = { member: `MemberExpression.callee[property.type="Identifier"]` };
456
+ //#endregion
457
+ //#region src/rules/no-duplicate-clock-or-source-array-values/no-duplicate-clock-or-source-array-values.ts
458
+ var no_duplicate_clock_or_source_array_values_default = createRule({
459
+ name: "no-duplicate-clock-or-source-array-values",
460
+ meta: {
461
+ type: "problem",
462
+ docs: { description: "Forbid providing duplicate units in `clock` and `source` arrays in `sample` and `guard`." },
463
+ messages: {
464
+ duplicate: "`{{ field }}` contains a duplicate unit `{{ unit }}`.",
465
+ remove: "Remove duplicate unit `{{ unit }}`."
466
+ },
467
+ schema: [],
468
+ hasSuggestions: true
469
+ },
470
+ defaultOptions: [],
471
+ create: (context) => {
472
+ const imports = /* @__PURE__ */ new Set();
473
+ const importSelector = `ImportDeclaration[source.value=${PACKAGE_NAME$1.core}]`;
474
+ const analyze = (node, field) => {
475
+ const seen = /* @__PURE__ */ new Map();
476
+ const entries = node.elements.filter((item) => item !== null).filter((item) => item.type !== AST_NODE_TYPES.SpreadElement);
477
+ for (const entry of entries) {
478
+ const root = traverseToRoot$1(entry);
479
+ if (!root) continue;
480
+ const name = [root.node.name, ...root.path].join(".");
481
+ if (seen.has(name)) report(entry, name, field);
482
+ else seen.set(name, entry);
483
+ }
484
+ };
485
+ const report = (node, name, field) => {
486
+ const data = {
487
+ field,
488
+ unit: name
489
+ };
490
+ const suggestion = {
491
+ messageId: "remove",
492
+ data: { unit: name },
493
+ fix: function* (fixer) {
494
+ yield fixer.remove(node);
495
+ const before = context.sourceCode.getTokenBefore(node);
496
+ if (before?.value === ",") yield fixer.remove(before);
497
+ }
498
+ };
499
+ context.report({
500
+ node,
501
+ messageId: "duplicate",
502
+ data,
503
+ suggest: [suggestion]
504
+ });
505
+ };
506
+ return {
507
+ [`${importSelector} > ${selector$8.method}`]: (node) => imports.add(node.local.name),
508
+ [`CallExpression${selector$8.call}:has(${selector$8.argument})`]: (node) => {
509
+ if (!imports.has(node.callee.name)) return;
510
+ const [config] = node.arguments;
511
+ const clock = locate.property("clock", config);
512
+ const source = locate.property("source", config);
513
+ if (clock?.value?.type === AST_NODE_TYPES.ArrayExpression) analyze(clock.value, "clock");
514
+ if (source?.value?.type === AST_NODE_TYPES.ArrayExpression) analyze(source.value, "source");
515
+ }
516
+ };
517
+ }
518
+ });
519
+ const selector$8 = {
520
+ method: `ImportSpecifier[imported.name=/(sample|guard)/]`,
521
+ call: `[callee.type="Identifier"][arguments.length=1]`,
522
+ argument: `ObjectExpression.arguments`
523
+ };
524
+ function traverseToRoot$1(node, path = []) {
525
+ if (node.type === AST_NODE_TYPES.Identifier) return {
526
+ node,
527
+ path
528
+ };
529
+ if (node.type === AST_NODE_TYPES.MemberExpression && node.property.type === AST_NODE_TYPES.Identifier) return traverseToRoot$1(node.object, [node.property.name, ...path]);
530
+ return null;
531
+ }
532
+ //#endregion
533
+ //#region src/rules/no-duplicate-on/no-duplicate-on.ts
534
+ var no_duplicate_on_default = createRule({
535
+ name: "no-duplicate-on",
536
+ meta: {
537
+ type: "problem",
538
+ docs: { description: "Forbid duplicate `.on` calls on Stores." },
539
+ messages: { duplicate: "Method `.on` is called on store `{{ store }}` more than once for `{{ unit }}`." },
540
+ schema: []
541
+ },
542
+ defaultOptions: [],
543
+ create: (context) => {
544
+ const services = ESLintUtils.getParserServices(context);
545
+ const map = /* @__PURE__ */ new Map();
546
+ return { [`CallExpression[callee.property.name="on"]`]: (node) => {
547
+ const type = services.getTypeAtLocation(node.callee.object);
548
+ if (!isType.store(type, services.program)) return;
549
+ const arg = node.arguments[0];
550
+ if (!arg || arg.type === AST_NODE_TYPES.SpreadElement) return;
551
+ const units = arg.type === AST_NODE_TYPES.ArrayExpression ? arg.elements.filter((item) => item !== null && item.type !== AST_NODE_TYPES.SpreadElement) : [arg];
552
+ const scope = context.sourceCode.getScope(node);
553
+ const store = identify("store", node.callee.object, scope);
554
+ if (!store) return;
555
+ const set = map.get(store.id) ?? /* @__PURE__ */ new Set();
556
+ for (const unit of units) {
557
+ const instance = identify("unit", unit, scope);
558
+ if (!instance) continue;
559
+ if (set.has(instance.id)) {
560
+ const data = {
561
+ store: store.name,
562
+ unit: instance.name
563
+ };
564
+ context.report({
565
+ messageId: "duplicate",
566
+ node: unit,
567
+ data
568
+ });
569
+ } else set.add(instance.id);
570
+ }
571
+ map.set(store.id, set);
572
+ } };
573
+ }
574
+ });
575
+ function traverseToRoot(node, path = []) {
576
+ if (node.type === AST_NODE_TYPES.Identifier) return {
577
+ node,
578
+ path
579
+ };
580
+ if (node.type === AST_NODE_TYPES.MemberExpression && node.property.type === AST_NODE_TYPES.Identifier) return traverseToRoot(node.object, [node.property.name, ...path]);
581
+ return null;
582
+ }
583
+ const STORE_METHODS = ["on", "reset"];
584
+ function traverseStoreToRoot(node, path = []) {
585
+ if (node.type === AST_NODE_TYPES.Identifier) return {
586
+ node,
587
+ path
588
+ };
589
+ if (node.type === AST_NODE_TYPES.MemberExpression && node.property.type === AST_NODE_TYPES.Identifier) return traverseStoreToRoot(node.object, [node.property.name, ...path]);
590
+ if (node.type === AST_NODE_TYPES.CallExpression && node.callee.type === AST_NODE_TYPES.MemberExpression) {
591
+ if (node.callee.property.type === AST_NODE_TYPES.Identifier && STORE_METHODS.includes(node.callee.property.name)) return traverseStoreToRoot(node.callee.object, path);
592
+ }
593
+ return null;
594
+ }
595
+ function raiseStoreToVariable(node) {
596
+ let current = node;
597
+ while (current.parent) {
598
+ if (current.parent.type === AST_NODE_TYPES.VariableDeclarator) return current.parent;
599
+ if (current.parent.type !== AST_NODE_TYPES.MemberExpression || current.parent.object !== current) return null;
600
+ if (current.parent.property.type !== AST_NODE_TYPES.Identifier || !STORE_METHODS.includes(current.parent.property.name)) return null;
601
+ const grandparent = current.parent.parent;
602
+ if (grandparent?.type !== AST_NODE_TYPES.CallExpression || grandparent.callee !== current.parent) return null;
603
+ current = current.parent.parent;
604
+ }
605
+ return null;
606
+ }
607
+ function findSuitableRoot(type, node) {
608
+ if (type === "unit") return traverseToRoot(node);
609
+ const root = traverseStoreToRoot(node);
610
+ if (root) return root;
611
+ const declarator = raiseStoreToVariable(node);
612
+ if (declarator && declarator.id.type === AST_NODE_TYPES.Identifier) return {
613
+ node: declarator.id,
614
+ path: []
615
+ };
616
+ return null;
617
+ }
618
+ function identify(type, node, scope) {
619
+ const root = findSuitableRoot(type, node);
620
+ if (!root) return null;
621
+ const variable = ASTUtils.findVariable(scope, root.node);
622
+ if (!variable) return null;
623
+ return {
624
+ id: `${variable.$id}+${root.path.join(".")}`,
625
+ name: [variable.name, ...root.path].join(".")
626
+ };
627
+ }
628
+ //#endregion
629
+ //#region src/rules/no-forward/no-forward.ts
630
+ var no_forward_default = createRule({
631
+ name: "no-forward",
632
+ meta: {
633
+ type: "problem",
634
+ docs: { description: "Prefer `sample` over `forward`." },
635
+ messages: {
636
+ noForward: "Use `sample` operator instead of `forward` as a more universal approach.",
637
+ replaceWithSample: "Replace `forward` with `sample`."
638
+ },
639
+ hasSuggestions: true,
640
+ schema: []
641
+ },
642
+ defaultOptions: [],
643
+ create: (context) => {
644
+ let sample;
645
+ const forwards = /* @__PURE__ */ new Map();
646
+ const source = context.sourceCode;
647
+ const visitorKeys = source.visitorKeys;
648
+ const importSelector = `ImportDeclaration[source.value=${PACKAGE_NAME$1.core}]`;
649
+ return {
650
+ [`${importSelector} > ${selector$7.forward}`]: (node) => forwards.set(node.local.name, node),
651
+ [`${importSelector} > ${selector$7.sample}`]: (node) => sample = node.local.name,
652
+ [`CallExpression${selector$7.call}:has(${selector$7.argument})`]: (node) => {
653
+ if (!forwards.has(node.callee.name)) return;
654
+ const config = {};
655
+ const arg = node.arguments[0];
656
+ config.clock = locate.property("from", arg)?.value;
657
+ config.target = locate.property("to", arg)?.value;
658
+ if (config.target) {
659
+ const [call] = esquery.match(config.target, query$2.prepend, { visitorKeys }).map((node) => node).filter((node) => node === config.target);
660
+ if (call) [config.target, config.fn] = [call.callee.object, call.arguments[0]];
661
+ }
662
+ if (config.clock && !config.fn) {
663
+ const [call] = esquery.match(config.clock, query$2.map, { visitorKeys }).map((node) => node).filter((node) => node === config.clock);
664
+ if (call) [config.clock, config.fn] = [call.callee.object, call.arguments[0]];
665
+ }
666
+ const code = [
667
+ "clock",
668
+ "fn",
669
+ "target"
670
+ ].filter((key) => config[key] !== void 0).map((key) => `${key}: ${source.getText(config[key])}`).join(", ");
671
+ context.report({
672
+ messageId: "noForward",
673
+ node: node.callee,
674
+ suggest: [{
675
+ messageId: "replaceWithSample",
676
+ fix: function* (fixer) {
677
+ const fn = sample ?? "sample";
678
+ yield fixer.replaceText(node, `${fn}({ ${code} })`);
679
+ if (!sample) yield fixer.insertTextAfter(forwards.get(node.callee.name), `, sample`);
680
+ }
681
+ }]
682
+ });
683
+ }
684
+ };
685
+ }
686
+ });
687
+ const selector$7 = {
688
+ forward: `ImportSpecifier[imported.name="forward"]`,
689
+ sample: `ImportSpecifier[imported.name="sample"]`,
690
+ call: `[callee.type="Identifier"][arguments.length=1]`,
691
+ argument: `ObjectExpression.arguments`
692
+ };
693
+ const query$2 = {
694
+ map: esquery.parse("CallExpression[arguments.length=1]:has(> :first-child:expression.arguments):has(> MemberExpression.callee:has(Identifier.property[name='map']))"),
695
+ prepend: esquery.parse("CallExpression[arguments.length=1]:has(> :first-child:expression.arguments):has(> MemberExpression.callee:has(Identifier.property[name='prepend']))")
696
+ };
697
+ //#endregion
698
+ //#region src/rules/no-getState/no-getState.ts
699
+ var no_getState_default = createRule({
700
+ name: "no-getState",
701
+ meta: {
702
+ type: "problem",
703
+ docs: { description: "Forbid `.getState` calls on Effector stores." },
704
+ messages: {
705
+ named: "Method `.getState` used on store `{{ name }}` can lead to race conditions. Replace with with `sample` or `attach`.",
706
+ anonymous: "Method `.getState` used on store can lead to race conditions. Replace with with `sample` or `attach`."
707
+ },
708
+ schema: []
709
+ },
710
+ defaultOptions: [],
711
+ create: (context) => {
712
+ const services = ESLintUtils.getParserServices(context);
713
+ return { [`CallExpression[callee.type="MemberExpression"][callee.property.name="getState"]`]: (node) => {
714
+ const type = services.getTypeAtLocation(node.callee.object);
715
+ if (!isType.store(type, services.program)) return;
716
+ const name = toName$1(node.callee.object);
717
+ if (name) context.report({
718
+ node,
719
+ messageId: "named",
720
+ data: { name }
721
+ });
722
+ else context.report({
723
+ node,
724
+ messageId: "anonymous"
725
+ });
726
+ } };
727
+ }
728
+ });
729
+ const toName$1 = (node) => {
730
+ if (node.type === AST_NODE_TYPES.Identifier) return node.name;
731
+ if (node.type === AST_NODE_TYPES.MemberExpression && !node.computed) return node.property.name;
732
+ return null;
733
+ };
734
+ //#endregion
735
+ //#region src/rules/no-guard/no-guard.ts
736
+ var no_guard_default = createRule({
737
+ name: "no-guard",
738
+ meta: {
739
+ type: "problem",
740
+ docs: { description: "Prefer `sample` over `guard`." },
741
+ messages: {
742
+ noGuard: "Use `sample` operator instead of `guard` as a more universal approach.",
743
+ replaceWithSample: "Replace `guard` with `sample`."
744
+ },
745
+ hasSuggestions: true,
746
+ schema: []
747
+ },
748
+ defaultOptions: [],
749
+ create: (context) => {
750
+ let sample;
751
+ const guards = /* @__PURE__ */ new Map();
752
+ const source = context.sourceCode;
753
+ const visitorKeys = source.visitorKeys;
754
+ const importSelector = `ImportDeclaration[source.value=${PACKAGE_NAME$1.core}]`;
755
+ return {
756
+ [`${importSelector} > ${selector$6.guard}`]: (node) => guards.set(node.local.name, node),
757
+ [`${importSelector} > ${selector$6.sample}`]: (node) => sample = node.local.name,
758
+ [`CallExpression${selector$6.call}`]: (node) => {
759
+ if (!guards.has(node.callee.name)) return;
760
+ const config = {};
761
+ if (node.arguments.length === 1 && node.arguments[0].type === AST_NODE_TYPES.ObjectExpression) {
762
+ const [arg] = node.arguments;
763
+ for (const key of [
764
+ "clock",
765
+ "source",
766
+ "filter",
767
+ "target"
768
+ ]) config[key] = locate.property(key, arg)?.value;
769
+ } else if (node.arguments.length === 2 && node.arguments[1].type === AST_NODE_TYPES.ObjectExpression) {
770
+ const [clock, arg] = node.arguments;
771
+ config.clock = clock;
772
+ for (const key of [
773
+ "source",
774
+ "filter",
775
+ "target"
776
+ ]) config[key] = locate.property(key, arg)?.value;
777
+ } else return;
778
+ if (config.target) {
779
+ const [call] = esquery.match(config.target, query$1.prepend, { visitorKeys }).map((node) => node).filter((node) => node === config.target);
780
+ if (call) [config.target, config.fn] = [call.callee.object, call.arguments[0]];
781
+ }
782
+ const code = [
783
+ "clock",
784
+ "source",
785
+ "filter",
786
+ "fn",
787
+ "target"
788
+ ].filter((key) => config[key] !== void 0).map((key) => `${key}: ${source.getText(config[key])}`).join(", ");
789
+ context.report({
790
+ messageId: "noGuard",
791
+ node: node.callee,
792
+ suggest: [{
793
+ messageId: "replaceWithSample",
794
+ fix: function* (fixer) {
795
+ const fn = sample ?? "sample";
796
+ yield fixer.replaceText(node, `${fn}({ ${code} })`);
797
+ if (!sample) yield fixer.insertTextAfter(guards.get(node.callee.name), `, sample`);
798
+ }
799
+ }]
800
+ });
801
+ }
802
+ };
803
+ }
804
+ });
805
+ const selector$6 = {
806
+ guard: `ImportSpecifier[imported.name="guard"]`,
807
+ sample: `ImportSpecifier[imported.name="sample"]`,
808
+ call: `[callee.type="Identifier"]`
809
+ };
810
+ const query$1 = { prepend: esquery.parse("CallExpression[arguments.length=1]:has(:first-child:expression.arguments):has(> MemberExpression.callee:has(Identifier.property[name='prepend']))") };
811
+ //#endregion
812
+ //#region src/rules/no-patronum-debug/no-patronum-debug.ts
813
+ var no_patronum_debug_default = createRule({
814
+ name: "no-patronum-debug",
815
+ meta: {
816
+ type: "problem",
817
+ docs: { description: "Disallow the use of `patronum` `debug`." },
818
+ messages: {
819
+ unexpected: "Unexpected `debug` call.",
820
+ remove: "Remove this `debug` call."
821
+ },
822
+ schema: [],
823
+ hasSuggestions: true
824
+ },
825
+ defaultOptions: [],
826
+ create: (context) => {
827
+ const debugs = /* @__PURE__ */ new Set();
828
+ return {
829
+ [`${`ImportDeclaration[source.value=${PACKAGE_NAME}]`} > ${selector$5.debug}`]: (node) => debugs.add(node.local.name),
830
+ [`CallExpression:matches(${selector$5.call})`]: (node) => {
831
+ const name = toName(node);
832
+ if (!debugs.has(name)) return;
833
+ context.report({
834
+ messageId: "unexpected",
835
+ node: node.callee,
836
+ suggest: [{
837
+ messageId: "remove",
838
+ fix: (fixer) => {
839
+ if (node.parent.type === AST_NODE_TYPES.ExpressionStatement) return fixer.remove(node.parent);
840
+ else return fixer.replaceText(node, "undefined");
841
+ }
842
+ }]
843
+ });
844
+ }
845
+ };
846
+ }
847
+ });
848
+ const PACKAGE_NAME = /^patronum(?:\u002Fdebug)?$/;
849
+ const selector$5 = {
850
+ debug: `ImportSpecifier[imported.name="debug"]`,
851
+ call: `[callee.type=Identifier], [callee.object.type=Identifier]`
852
+ };
853
+ const toName = (node) => {
854
+ switch (node.callee.type) {
855
+ case AST_NODE_TYPES.Identifier: return node.callee.name;
856
+ case AST_NODE_TYPES.MemberExpression: return node.callee.object.name;
857
+ }
858
+ };
859
+ //#endregion
860
+ //#region src/rules/no-units-spawn-in-render/no-units-spawn-in-render.ts
861
+ const EFFECTOR_FACTORIES = new Set([
862
+ "createStore",
863
+ "createEvent",
864
+ "createEffect",
865
+ "createDomain",
866
+ "createApi",
867
+ "restore"
868
+ ]);
869
+ const EFFECTOR_OPERATORS = new Set([
870
+ "sample",
871
+ "guard",
872
+ "forward",
873
+ "merge",
874
+ "split",
875
+ "combine",
876
+ "attach"
877
+ ]);
878
+ const REACT_HOOKS_SPEC = {
879
+ from: "package",
880
+ package: "react",
881
+ name: [
882
+ "useState",
883
+ "useEffect",
884
+ "useLayoutEffect",
885
+ "useCallback",
886
+ "useMemo",
887
+ "useRef",
888
+ "useReducer",
889
+ "useImperativeHandle",
890
+ "useDebugValue",
891
+ "useDeferredValue",
892
+ "useTransition",
893
+ "useId",
894
+ "useSyncExternalStore",
895
+ "useInsertionEffect",
896
+ "useContext"
897
+ ]
898
+ };
899
+ const EFFECTOR_FACTORY_SPEC = {
900
+ from: "package",
901
+ package: "effector",
902
+ name: [...EFFECTOR_FACTORIES]
903
+ };
904
+ const EFFECTOR_OPERATOR_SPEC = {
905
+ from: "package",
906
+ package: "effector",
907
+ name: [...EFFECTOR_OPERATORS]
908
+ };
909
+ const EFFECTOR_FACTORIO_SHAPE = [
910
+ "useModel",
911
+ "createModel",
912
+ "Provider",
913
+ "@@unitShape"
914
+ ];
915
+ var no_units_spawn_in_render_default = createRule({
916
+ name: "no-units-spawn-in-render",
917
+ meta: {
918
+ type: "problem",
919
+ docs: { description: "Forbid creating Effector units or calling operators inside React components or hooks." },
920
+ messages: {
921
+ noFactoryInRender: "Creating Effector units with \"{{ name }}\" inside React component or hook is forbidden, since it may cause memory leaks and other bugs.",
922
+ noOperatorInRender: "Using Effector operator \"{{ name }}\" inside React component or hook is forbidden, since it may cause memory leaks and other bugs.",
923
+ noCustomFactoryInRender: "Creating Effector units with \"{{ name }}\" inside React component or hook is forbidden, since it may cause memory leaks and other bugs. If this is a false positive, add \"{{ name }}\" to the allowlist in the detectCustomFactories option."
924
+ },
925
+ schema: [{
926
+ type: "object",
927
+ properties: { detectCustomFactories: { oneOf: [{ type: "boolean" }, {
928
+ type: "object",
929
+ properties: { allowlist: {
930
+ type: "array",
931
+ items: { type: "string" },
932
+ uniqueItems: true
933
+ } },
934
+ required: ["allowlist"],
935
+ additionalProperties: false
936
+ }] } },
937
+ additionalProperties: false
938
+ }]
939
+ },
940
+ defaultOptions: [{ detectCustomFactories: true }],
941
+ create: (context, [options]) => {
942
+ const services = ESLintUtils.getParserServices(context);
943
+ const checker = services.program.getTypeChecker();
944
+ const { detectCustomFactories } = options;
945
+ const allowlist = typeof detectCustomFactories === "object" ? new Set(detectCustomFactories.allowlist) : void 0;
946
+ const stack = { render: [] };
947
+ const effectorImports = /* @__PURE__ */ new Map();
948
+ return {
949
+ [`${`ImportDeclaration[source.value=${PACKAGE_NAME$1.core}]`} > ImportSpecifier[imported.type="Identifier"]`]: (node) => {
950
+ const imported = node.imported.name;
951
+ const local = node.local.name;
952
+ if (EFFECTOR_FACTORIES.has(imported)) effectorImports.set(local, "factory");
953
+ else if (EFFECTOR_OPERATORS.has(imported)) effectorImports.set(local, "operator");
954
+ },
955
+ [`FunctionDeclaration, FunctionExpression, ArrowFunctionExpression`]: (node) => {
956
+ if (stack.render.at(-1) ?? false) return void stack.render.push(true);
957
+ const name = nameOf.function(node);
958
+ if (name && UseRegex.test(name.name)) return void stack.render.push(true);
959
+ const tsnode = services.esTreeNodeToTSNodeMap.get(node);
960
+ const signature = checker.getSignatureFromDeclaration(tsnode);
961
+ const returnType = signature ? checker.getReturnTypeOfSignature(signature) : checker.getVoidType();
962
+ if (returnType.isUnion() ? returnType.types.some((type) => isType.jsx(type, services.program)) : isType.jsx(returnType, services.program)) return void stack.render.push(true);
963
+ const inferred = isExpression(tsnode) && getContextualType(checker, tsnode) || checker.getUnknownType();
964
+ if (inferred.isUnion() ? inferred.types.some((type) => isType.component(type, services.program)) : isType.component(inferred, services.program)) return void stack.render.push(true);
965
+ stack.render.push(false);
966
+ },
967
+ [`:matches(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression):exit`]: () => void stack.render.pop(),
968
+ "ClassDeclaration": () => void stack.render.push(false),
969
+ "ClassDeclaration:exit": () => void stack.render.pop(),
970
+ "CallExpression": (node) => {
971
+ if (!(stack.render.at(-1) ?? false)) return;
972
+ const calleeName = getCalleeName(node.callee);
973
+ switch (calleeName ? effectorImports.get(calleeName) : void 0) {
974
+ case "factory": return context.report({
975
+ node,
976
+ messageId: "noFactoryInRender",
977
+ data: { name: calleeName }
978
+ });
979
+ case "operator": return context.report({
980
+ node,
981
+ messageId: "noOperatorInRender",
982
+ data: { name: calleeName }
983
+ });
984
+ }
985
+ if (detectCustomFactories === false) return;
986
+ const returnType = services.getTypeAtLocation(node);
987
+ if (!hasEffectorUnitInType({
988
+ node: services.esTreeNodeToTSNodeMap.get(node),
989
+ checker,
990
+ program: services.program
991
+ }, returnType)) return;
992
+ const calleeType = services.getTypeAtLocation(node.callee);
993
+ const displayName = calleeName ?? "<expression>";
994
+ if (typeMatchesSpecifier(calleeType, REACT_HOOKS_SPEC, services.program)) return;
995
+ if (isEffectorFactorioHook(node.callee, services.getTypeAtLocation)) return;
996
+ if (typeMatchesSpecifier(calleeType, EFFECTOR_FACTORY_SPEC, services.program)) return context.report({
997
+ node,
998
+ messageId: "noFactoryInRender",
999
+ data: { name: displayName }
1000
+ });
1001
+ if (typeMatchesSpecifier(calleeType, EFFECTOR_OPERATOR_SPEC, services.program)) return context.report({
1002
+ node,
1003
+ messageId: "noOperatorInRender",
1004
+ data: { name: displayName }
1005
+ });
1006
+ if (allowlist && calleeName && allowlist.has(calleeName)) return;
1007
+ context.report({
1008
+ node,
1009
+ messageId: "noCustomFactoryInRender",
1010
+ data: { name: displayName }
1011
+ });
1012
+ }
1013
+ };
1014
+ }
1015
+ });
1016
+ const UseRegex = /^use[A-Z0-9].*$/;
1017
+ function getCalleeName(callee) {
1018
+ if (callee.type === AST_NODE_TYPES.Identifier) return callee.name;
1019
+ if (callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier) return callee.property.name;
1020
+ else return null;
1021
+ }
1022
+ function hasEffectorUnitInType(ctx, type, depth = 3) {
1023
+ if (isType.unit(type, ctx.program)) return true;
1024
+ if (depth <= 0) return false;
1025
+ if (type.isUnion()) return type.types.some((type) => hasEffectorUnitInType(ctx, type, depth));
1026
+ for (const property of type.getProperties()) if (hasEffectorUnitInType(ctx, ctx.checker.getTypeOfSymbolAtLocation(property, ctx.node), depth - 1)) return true;
1027
+ return false;
1028
+ }
1029
+ function isEffectorFactorioHook(callee, getTypeAtLocation) {
1030
+ if (callee.type !== AST_NODE_TYPES.MemberExpression) return false;
1031
+ const objectType = getTypeAtLocation(callee.object);
1032
+ const propertyNames = new Set(objectType.getProperties().map((p) => p.getName()));
1033
+ return EFFECTOR_FACTORIO_SHAPE.every((name) => propertyNames.has(name));
1034
+ }
1035
+ //#endregion
1036
+ //#region src/rules/no-unnecessary-combination/no-unnecessary-combination.ts
1037
+ var no_unnecessary_combination_default = createRule({
1038
+ name: "no-unnecessary-combination",
1039
+ meta: {
1040
+ type: "suggestion",
1041
+ docs: { description: "Forbid unnecessary combinations in `clock` and `source`." },
1042
+ messages: { unnecessary: "{{ method }} is used under the hood of {{ property }} in {{ operator }}, you can omit it." },
1043
+ schema: []
1044
+ },
1045
+ defaultOptions: [],
1046
+ create: (context) => {
1047
+ const services = ESLintUtils.getParserServices(context);
1048
+ const operators = /* @__PURE__ */ new Set();
1049
+ const combinators = /* @__PURE__ */ new Map();
1050
+ const importSelector = `ImportDeclaration[source.value=${PACKAGE_NAME$1.core}]`;
1051
+ return {
1052
+ [`${importSelector} > ${selector$4.operator}`]: (node) => operators.add(node.local.name),
1053
+ [`${importSelector} > ${selector$4.combinator}`]: (node) => combinators.set(node.local.name, node.imported.name),
1054
+ [`CallExpression${selector$4.call}:has(${selector$4.argument})`]: (node) => {
1055
+ if (!operators.has(node.callee.name)) return;
1056
+ const [config] = node.arguments;
1057
+ const clock = locate.property("clock", config)?.value;
1058
+ const source = locate.property("source", config)?.value;
1059
+ if (clock?.type === AST_NODE_TYPES.CallExpression && clock.callee.type === AST_NODE_TYPES.Identifier) {
1060
+ if (combinators.get(clock.callee.name) === "merge") {
1061
+ const data = {
1062
+ method: clock.callee.name,
1063
+ property: "clock",
1064
+ operator: node.callee.name
1065
+ };
1066
+ context.report({
1067
+ node: clock,
1068
+ messageId: "unnecessary",
1069
+ data
1070
+ });
1071
+ }
1072
+ }
1073
+ if (source?.type === AST_NODE_TYPES.CallExpression && source.callee.type === AST_NODE_TYPES.Identifier) {
1074
+ const method = combinators.get(source.callee.name);
1075
+ if (!method) return;
1076
+ if (method === "combine" && source.arguments.length > 1 && isFunction(source.arguments.at(-1), services)) return;
1077
+ const data = {
1078
+ method: source.callee.name,
1079
+ property: "source",
1080
+ operator: node.callee.name
1081
+ };
1082
+ context.report({
1083
+ node: source,
1084
+ messageId: "unnecessary",
1085
+ data
1086
+ });
1087
+ }
1088
+ }
1089
+ };
1090
+ }
1091
+ });
1092
+ const selector$4 = {
1093
+ operator: `ImportSpecifier[imported.name=/(sample|guard)/]`,
1094
+ combinator: `ImportSpecifier[imported.name=/(combine|merge)/]`,
1095
+ call: `[callee.type="Identifier"][arguments.length=1]`,
1096
+ argument: `ObjectExpression.arguments`
1097
+ };
1098
+ function isFunction(node, services) {
1099
+ if (node.type === AST_NODE_TYPES.ArrowFunctionExpression) return true;
1100
+ else if (node.type === AST_NODE_TYPES.FunctionExpression) return true;
1101
+ else if (node.type === AST_NODE_TYPES.Identifier) {
1102
+ const checker = services.program.getTypeChecker();
1103
+ const tsnode = services.esTreeNodeToTSNodeMap.get(node);
1104
+ return checker.getTypeAtLocation(tsnode).getCallSignatures().length > 0;
1105
+ } else return false;
1106
+ }
1107
+ //#endregion
1108
+ //#region src/rules/no-unnecessary-duplication/no-unnecessary-duplication.ts
1109
+ var no_unnecessary_duplication_default = createRule({
1110
+ name: "no-unnecessary-duplication",
1111
+ meta: {
1112
+ type: "problem",
1113
+ docs: { description: "Forbid duplicate `source` and `clock` in `sample` and `guard`." },
1114
+ messages: {
1115
+ duplicate: "Method `{{ method }}` has the same value for `source` and `clock`. Consider using only one of them.",
1116
+ removeClock: "Remove the `clock`",
1117
+ removeSource: "Remove the `source`"
1118
+ },
1119
+ schema: [],
1120
+ hasSuggestions: true
1121
+ },
1122
+ defaultOptions: [],
1123
+ create: (context) => {
1124
+ const imports = /* @__PURE__ */ new Set();
1125
+ return {
1126
+ [`${`ImportDeclaration[source.value=${PACKAGE_NAME$1.core}]`} > ${selector$3.method}`]: (node) => imports.add(node.local.name),
1127
+ [`CallExpression${selector$3.call}:has(${selector$3.argument})`]: (node) => {
1128
+ if (!imports.has(node.callee.name)) return;
1129
+ const [config] = node.arguments;
1130
+ const source = locate.property("source", config)?.value;
1131
+ if (!source) return;
1132
+ const clock = locate.property("clock", config)?.value;
1133
+ if (!clock) return;
1134
+ if (!compare(clock, source)) return;
1135
+ const suggestions = [{
1136
+ messageId: "removeClock",
1137
+ fix: function* (fixer) {
1138
+ yield fixer.remove(clock.parent);
1139
+ const after = context.sourceCode.getTokenAfter(clock.parent);
1140
+ if (after?.value === ",") yield fixer.remove(after);
1141
+ }
1142
+ }, {
1143
+ messageId: "removeSource",
1144
+ fix: function* (fixer) {
1145
+ yield fixer.remove(source.parent);
1146
+ const after = context.sourceCode.getTokenAfter(source.parent);
1147
+ if (after?.value === ",") yield fixer.remove(after);
1148
+ }
1149
+ }];
1150
+ const data = { method: node.callee.name };
1151
+ context.report({
1152
+ node: config,
1153
+ messageId: "duplicate",
1154
+ data,
1155
+ suggest: suggestions
1156
+ });
1157
+ }
1158
+ };
1159
+ }
1160
+ });
1161
+ const selector$3 = {
1162
+ method: `ImportSpecifier[imported.name=/(sample|guard)/]`,
1163
+ call: `[callee.type="Identifier"][arguments.length=1]`,
1164
+ argument: `ObjectExpression.arguments`
1165
+ };
1166
+ function compare(clock, source, limit = 5) {
1167
+ if (limit <= 0) return false;
1168
+ if (clock.type === AST_NODE_TYPES.Identifier) return source.type === AST_NODE_TYPES.Identifier && clock.name === source.name;
1169
+ if (clock.type === AST_NODE_TYPES.ArrayExpression) {
1170
+ if (clock.elements.length !== 1) return false;
1171
+ let a, b;
1172
+ if (source.type === AST_NODE_TYPES.ArrayExpression) if (source.elements.length !== 1) return false;
1173
+ else [a, b] = [clock.elements[0], source.elements[0]];
1174
+ else [a, b] = [clock.elements[0], source];
1175
+ return a.type === AST_NODE_TYPES.Identifier && b.type === AST_NODE_TYPES.Identifier && a.name === b.name;
1176
+ }
1177
+ if (clock.type === AST_NODE_TYPES.MemberExpression) {
1178
+ if (source.type !== AST_NODE_TYPES.MemberExpression) return false;
1179
+ if (clock.computed || source.computed) return false;
1180
+ if (clock.property.name !== source.property.name) return false;
1181
+ return compare(clock.object, source.object, limit - 1);
1182
+ }
1183
+ return false;
1184
+ }
1185
+ //#endregion
1186
+ //#region src/rules/no-useless-methods/no-useless-methods.ts
1187
+ var no_useless_methods_default = createRule({
1188
+ name: "no-useless-methods",
1189
+ meta: {
1190
+ type: "problem",
1191
+ docs: { description: "Forbid useless calls of `sample` and `guard`." },
1192
+ messages: { uselessMethod: "Method `{{ method }}` does nothing in this case. You should assign the result to variable or pass `target` to it." },
1193
+ schema: []
1194
+ },
1195
+ defaultOptions: [],
1196
+ create: (context) => {
1197
+ const imports = /* @__PURE__ */ new Set();
1198
+ const source = context.sourceCode;
1199
+ const visitorKeys = source.visitorKeys;
1200
+ const importSelector = `ImportDeclaration[source.value=${PACKAGE_NAME$1.core}]`;
1201
+ const usageStack = [];
1202
+ return {
1203
+ "ReturnStatement": () => usageStack.push(true),
1204
+ "ReturnStatement:exit": () => usageStack.pop(),
1205
+ "VariableDeclarator": () => usageStack.push(true),
1206
+ "VariableDeclarator:exit": () => usageStack.pop(),
1207
+ "ObjectExpression": () => usageStack.push(true),
1208
+ "ObjectExpression:exit": () => usageStack.pop(),
1209
+ "BlockStatement": () => usageStack.push(false),
1210
+ "BlockStatement:exit": () => usageStack.pop(),
1211
+ [`${importSelector} > ${selector$2.method}`]: (node) => imports.add(node.local.name),
1212
+ [`CallExpression[callee.type="Identifier"]`]: (node) => {
1213
+ if (!imports.has(node.callee.name)) return;
1214
+ if (usageStack.at(-1) ?? false) return;
1215
+ if (node.parent.type === AST_NODE_TYPES.CallExpression) return;
1216
+ const [config] = node.arguments;
1217
+ if (config?.type === AST_NODE_TYPES.ObjectExpression) {
1218
+ if (locate.property("target", config)?.value) return;
1219
+ }
1220
+ const grandparent = node.parent.parent;
1221
+ if (grandparent) {
1222
+ const ancestry = source.getAncestors(grandparent);
1223
+ if (esquery.matches(grandparent, query.watch, ancestry, { visitorKeys })) return;
1224
+ }
1225
+ const method = node.callee.name;
1226
+ context.report({
1227
+ node,
1228
+ messageId: "uselessMethod",
1229
+ data: { method }
1230
+ });
1231
+ }
1232
+ };
1233
+ }
1234
+ });
1235
+ const selector$2 = { method: `ImportSpecifier[imported.name=/(sample|guard)/]` };
1236
+ const query = { watch: esquery.parse("CallExpression:has(> MemberExpression.callee[property.name=watch]:has(> CallExpression.object))") };
1237
+ //#endregion
1238
+ //#region src/rules/no-watch/no-watch.ts
1239
+ var no_watch_default = createRule({
1240
+ name: "no-watch",
1241
+ meta: {
1242
+ type: "suggestion",
1243
+ docs: { description: "Restrict usage of `.watch` on any Effector Unit." },
1244
+ messages: { restricted: "Using `.watch` method leads to imperative code. Replace it with an operator `sample` or use the `target` parameter of `sample` operator." },
1245
+ schema: []
1246
+ },
1247
+ defaultOptions: [],
1248
+ create: (context) => {
1249
+ const services = ESLintUtils.getParserServices(context);
1250
+ return { [`CallExpression[callee.type="MemberExpression"][callee.property.name="watch"]`]: (node) => {
1251
+ const type = services.getTypeAtLocation(node.callee.object);
1252
+ if (!isType.unit(type, services.program)) return;
1253
+ context.report({
1254
+ node,
1255
+ messageId: "restricted"
1256
+ });
1257
+ } };
1258
+ }
1259
+ });
1260
+ //#endregion
1261
+ //#region src/rules/prefer-useUnit/prefer-useUnit.ts
1262
+ var prefer_useUnit_default = createRule({
1263
+ name: "prefer-useUnit",
1264
+ meta: {
1265
+ type: "suggestion",
1266
+ docs: { description: "Prefer `useUnit` over deprecated `useStore` and `useEvent` hooks." },
1267
+ messages: { useUseUnit: "`{{ name }}` should be replaced with `useUnit`." },
1268
+ schema: []
1269
+ },
1270
+ defaultOptions: [],
1271
+ create: (context) => {
1272
+ const imports = /* @__PURE__ */ new Map();
1273
+ const importSelector = `ImportDeclaration[source.value=${PACKAGE_NAME$1.react}]`;
1274
+ return {
1275
+ [`${importSelector} > ${selector$1.useStore}`]: (node) => void imports.set(node.local.name, "useStore"),
1276
+ [`${importSelector} > ${selector$1.useEvent}`]: (node) => void imports.set(node.local.name, "useEvent"),
1277
+ [`CallExpression[callee.type="Identifier"]`]: (node) => {
1278
+ const hook = imports.get(node.callee.name);
1279
+ if (!hook) return;
1280
+ context.report({
1281
+ node,
1282
+ messageId: "useUseUnit",
1283
+ data: { name: hook }
1284
+ });
1285
+ }
1286
+ };
1287
+ }
1288
+ });
1289
+ const selector$1 = {
1290
+ useStore: `ImportSpecifier[imported.name=useStore]`,
1291
+ useEvent: `ImportSpecifier[imported.name=useEvent]`
1292
+ };
1293
+ //#endregion
1294
+ //#region src/rules/require-pickup-in-persist/require-pickup-in-persist.ts
1295
+ var require_pickup_in_persist_default = createRule({
1296
+ name: "require-pickup-in-persist",
1297
+ meta: {
1298
+ type: "problem",
1299
+ docs: { description: "Require every `persist` call of `effector-storage` to use `pickup`." },
1300
+ messages: { missing: "This `persist` call does not specify a `pickup` event that is required for scoped usage of `effector-storage`." },
1301
+ schema: []
1302
+ },
1303
+ defaultOptions: [],
1304
+ create: (context) => {
1305
+ const imports = /* @__PURE__ */ new Set();
1306
+ return {
1307
+ [`${`ImportDeclaration[source.value=${PACKAGE_NAME$1.storage}]`} > ${selector.persist}`]: (node) => imports.add(node.local.name),
1308
+ [`CallExpression${selector.call}${selector.config}`]: (node) => {
1309
+ if (!imports.has(node.callee.name)) return;
1310
+ if (node.arguments[0].properties.filter((prop) => prop.type === AST_NODE_TYPES.Property).map((prop) => prop.key).filter((key) => key.type === AST_NODE_TYPES.Identifier).some((key) => key.name === "pickup")) return;
1311
+ context.report({
1312
+ node,
1313
+ messageId: "missing"
1314
+ });
1315
+ }
1316
+ };
1317
+ }
1318
+ });
1319
+ const selector = {
1320
+ persist: `ImportSpecifier[imported.name="persist"]`,
1321
+ call: `[callee.type="Identifier"]`,
1322
+ config: `[arguments.length=1][arguments.0.type="ObjectExpression"]`
1323
+ };
1324
+ //#endregion
1325
+ //#region src/rules/strict-effect-handlers/strict-effect-handlers.ts
1326
+ var strict_effect_handlers_default = createRule({
1327
+ name: "strict-effect-handlers",
1328
+ meta: {
1329
+ type: "problem",
1330
+ docs: { description: "Forbid mixing calls to both regular async functions and Effects in the same function." },
1331
+ messages: { mixed: "This function can lead to losing Scope in Effector Fork API." },
1332
+ schema: []
1333
+ },
1334
+ defaultOptions: [],
1335
+ create: (context) => {
1336
+ const services = ESLintUtils.getParserServices(context);
1337
+ const stack = [];
1338
+ const track = (node) => {
1339
+ const current = stack.at(-1);
1340
+ if (!current) return;
1341
+ const callee = node.argument.callee;
1342
+ const type = services.getTypeAtLocation(callee);
1343
+ if (isType.effect(type, services.program)) return current.effect = true;
1344
+ else return current.regular = true;
1345
+ };
1346
+ const enter = () => {
1347
+ stack.push({
1348
+ effect: false,
1349
+ regular: false
1350
+ });
1351
+ };
1352
+ const exit = (node) => {
1353
+ const scope = stack.pop();
1354
+ if (!scope) return;
1355
+ if (scope.effect && scope.regular) context.report({
1356
+ node,
1357
+ messageId: "mixed"
1358
+ });
1359
+ };
1360
+ return {
1361
+ "ArrowFunctionExpression": enter,
1362
+ "ArrowFunctionExpression:exit": exit,
1363
+ "FunctionExpression": enter,
1364
+ "FunctionExpression:exit": exit,
1365
+ "FunctionDeclaration": enter,
1366
+ "FunctionDeclaration:exit": exit,
1367
+ "AwaitExpression:matches([argument.type='CallExpression'], [argument.type='NewExpression'])": track
1368
+ };
1369
+ }
1370
+ });
1371
+ const ruleset = {
1372
+ recommended: {
1373
+ "effector/enforce-effect-naming-convention": "error",
1374
+ "effector/enforce-store-naming-convention": "error",
1375
+ "effector/keep-options-order": "warn",
1376
+ "effector/no-ambiguity-target": "warn",
1377
+ "effector/no-duplicate-on": "error",
1378
+ "effector/no-forward": "error",
1379
+ "effector/no-getState": "error",
1380
+ "effector/no-guard": "error",
1381
+ "effector/no-unnecessary-combination": "warn",
1382
+ "effector/no-unnecessary-duplication": "warn",
1383
+ "effector/no-useless-methods": "error",
1384
+ "effector/no-watch": "warn"
1385
+ },
1386
+ patronum: { "effector/no-patronum-debug": "warn" },
1387
+ scope: {
1388
+ "effector/require-pickup-in-persist": "error",
1389
+ "effector/strict-effect-handlers": "error"
1390
+ },
1391
+ react: {
1392
+ "effector/enforce-gate-naming-convention": "error",
1393
+ "effector/mandatory-scope-binding": "error",
1394
+ "effector/no-units-spawn-in-render": "error",
1395
+ "effector/prefer-useUnit": "error"
1396
+ },
1397
+ future: { "effector/no-domain-unit-creators": "warn" }
1398
+ };
1399
+ //#endregion
1400
+ //#region src/index.ts
1401
+ const base = {
1402
+ meta: {
1403
+ name,
1404
+ version,
1405
+ namespace: "effector"
1406
+ },
1407
+ rules: {
1408
+ "enforce-effect-naming-convention": enforce_effect_naming_convention_default,
1409
+ "enforce-gate-naming-convention": enforce_gate_naming_convention_default,
1410
+ "enforce-store-naming-convention": enforce_store_naming_convention_default,
1411
+ "keep-options-order": keep_options_order_default,
1412
+ "mandatory-scope-binding": mandatory_scope_binding_default,
1413
+ "no-ambiguity-target": no_ambiguity_target_default,
1414
+ "no-domain-unit-creators": no_domain_unit_creators_default,
1415
+ "no-duplicate-clock-or-source-array-values": no_duplicate_clock_or_source_array_values_default,
1416
+ "no-duplicate-on": no_duplicate_on_default,
1417
+ "no-forward": no_forward_default,
1418
+ "no-getState": no_getState_default,
1419
+ "no-guard": no_guard_default,
1420
+ "no-patronum-debug": no_patronum_debug_default,
1421
+ "no-units-spawn-in-render": no_units_spawn_in_render_default,
1422
+ "no-unnecessary-combination": no_unnecessary_combination_default,
1423
+ "no-unnecessary-duplication": no_unnecessary_duplication_default,
1424
+ "no-useless-methods": no_useless_methods_default,
1425
+ "no-watch": no_watch_default,
1426
+ "prefer-useUnit": prefer_useUnit_default,
1427
+ "require-pickup-in-persist": require_pickup_in_persist_default,
1428
+ "strict-effect-handlers": strict_effect_handlers_default
1429
+ }
1430
+ };
1431
+ const legacyConfigs = {
1432
+ recommended: { rules: ruleset.recommended },
1433
+ scope: { rules: ruleset.scope },
1434
+ react: { rules: ruleset.react },
1435
+ future: { rules: ruleset.future },
1436
+ patronum: { rules: ruleset.patronum }
1437
+ };
1438
+ const self = base;
1439
+ const flatConfigs = {
1440
+ recommended: {
1441
+ plugins: { effector: self },
1442
+ rules: ruleset.recommended
1443
+ },
1444
+ scope: {
1445
+ plugins: { effector: self },
1446
+ rules: ruleset.scope
1447
+ },
1448
+ react: {
1449
+ plugins: { effector: self },
1450
+ rules: ruleset.react
1451
+ },
1452
+ future: {
1453
+ plugins: { effector: self },
1454
+ rules: ruleset.future
1455
+ },
1456
+ patronum: {
1457
+ plugins: { effector: self },
1458
+ rules: ruleset.patronum
1459
+ }
1460
+ };
1461
+ const plugin = base;
1462
+ plugin.configs = legacyConfigs;
1463
+ plugin.flatConfigs = flatConfigs;
1464
+ //#endregion
1465
+ export { plugin as default };