eslint-plugin-effector 0.17.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.
package/dist/index.mjs CHANGED
@@ -2,13 +2,18 @@ import { ASTUtils, AST_NODE_TYPES, ESLintUtils } from "@typescript-eslint/utils"
2
2
  import { getContextualType, typeMatchesSpecifier } from "@typescript-eslint/type-utils";
3
3
  import { isExpression } from "typescript";
4
4
  import esquery from "esquery";
5
+ //#region package.json
5
6
  var name = "eslint-plugin-effector";
6
- var version = "0.17.0";
7
- const createRule = ESLintUtils.RuleCreator((name$1) => `https://eslint.effector.dev/rules/${name$1}`);
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
8
13
  const check = (symbol, types, from) => {
9
- const name$1 = symbol.getName();
14
+ const name = symbol.getName();
10
15
  const declarations = symbol.declarations ?? [];
11
- return types.includes(name$1) && declarations.map((decl) => decl.getSourceFile().fileName).some((fname) => fname.includes("node_modules") && fname.includes(from));
16
+ return types.includes(name) && declarations.map((decl) => decl.getSourceFile().fileName).some((fname) => fname.includes("node_modules") && fname.includes(from));
12
17
  };
13
18
  const isType = {
14
19
  store: (type, program) => typeMatchesSpecifier(type, {
@@ -35,7 +40,8 @@ const isType = {
35
40
  "StoreWritable",
36
41
  "Event",
37
42
  "EventCallable",
38
- "Effect"
43
+ "Effect",
44
+ "Domain"
39
45
  ]
40
46
  }, program);
41
47
  },
@@ -73,6 +79,8 @@ const isType = {
73
79
  }, program);
74
80
  }
75
81
  };
82
+ //#endregion
83
+ //#region src/rules/enforce-effect-naming-convention/enforce-effect-naming-convention.ts
76
84
  var enforce_effect_naming_convention_default = createRule({
77
85
  name: "enforce-effect-naming-convention",
78
86
  meta: {
@@ -115,6 +123,8 @@ var enforce_effect_naming_convention_default = createRule({
115
123
  }
116
124
  });
117
125
  const FxRegex = /Fx$/;
126
+ //#endregion
127
+ //#region src/rules/enforce-gate-naming-convention/enforce-gate-naming-convention.ts
118
128
  var enforce_gate_naming_convention_default = createRule({
119
129
  name: "enforce-gate-naming-convention",
120
130
  meta: {
@@ -157,6 +167,8 @@ var enforce_gate_naming_convention_default = createRule({
157
167
  }
158
168
  });
159
169
  const GateRegex = /^[^A-Z]/;
170
+ //#endregion
171
+ //#region src/rules/enforce-store-naming-convention/enforce-store-naming-convention.ts
160
172
  var enforce_store_naming_convention_default = createRule({
161
173
  name: "enforce-store-naming-convention",
162
174
  meta: {
@@ -208,11 +220,15 @@ var enforce_store_naming_convention_default = createRule({
208
220
  });
209
221
  const PrefixRegex = /^[^$]/;
210
222
  const PostfixRegex = /[^$]$/;
223
+ //#endregion
224
+ //#region src/shared/package.ts
211
225
  const PACKAGE_NAME$1 = {
212
226
  core: /^effector(?:\u002Fcompat)?$/,
213
227
  react: /^effector-react$/,
214
228
  storage: /^@?effector-storage(\u002F[\w-]+)*$/
215
229
  };
230
+ //#endregion
231
+ //#region src/rules/keep-options-order/keep-options-order.ts
216
232
  var keep_options_order_default = createRule({
217
233
  name: "keep-options-order",
218
234
  meta: {
@@ -285,6 +301,8 @@ const isCorrectOrder = (current) => {
285
301
  }
286
302
  return true;
287
303
  };
304
+ //#endregion
305
+ //#region src/shared/name.ts
288
306
  function functionToName(node) {
289
307
  if (node.id) return node.id;
290
308
  if (node.parent.type === AST_NODE_TYPES.VariableDeclarator && node.parent.id.type === AST_NODE_TYPES.Identifier) return node.parent.id;
@@ -294,6 +312,8 @@ function functionToName(node) {
294
312
  return null;
295
313
  }
296
314
  const nameOf = { function: functionToName };
315
+ //#endregion
316
+ //#region src/rules/mandatory-scope-binding/mandatory-scope-binding.ts
297
317
  var mandatory_scope_binding_default = createRule({
298
318
  name: "mandatory-scope-binding",
299
319
  meta: {
@@ -313,14 +333,14 @@ var mandatory_scope_binding_default = createRule({
313
333
  return {
314
334
  [`FunctionDeclaration, FunctionExpression, ArrowFunctionExpression`]: (node) => {
315
335
  if (stack.render.at(-1) ?? false) return void stack.render.push(true);
316
- const name$1 = nameOf.function(node);
317
- if (name$1 && UseRegex.test(name$1.name)) 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);
318
338
  const tsnode = services.esTreeNodeToTSNodeMap.get(node);
319
339
  const signature = checker.getSignatureFromDeclaration(tsnode);
320
340
  const returnType = signature ? checker.getReturnTypeOfSignature(signature) : checker.getVoidType();
321
- if (isType.jsx(returnType, services.program)) return void stack.render.push(true);
322
- const inferred = isExpression(tsnode) ? getContextualType(checker, tsnode) : void 0;
323
- if (inferred ? isType.component(inferred, services.program) : false) return void stack.render.push(true);
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);
324
344
  stack.render.push(false);
325
345
  },
326
346
  [`:matches(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression):exit`]: () => void stack.render.pop(),
@@ -354,9 +374,13 @@ var mandatory_scope_binding_default = createRule({
354
374
  };
355
375
  }
356
376
  });
357
- const UseRegex = /^use[A-Z0-9].*$/;
377
+ const UseRegex$1 = /^use[A-Z0-9].*$/;
378
+ //#endregion
379
+ //#region src/shared/locate.ts
358
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);
359
381
  const locate = { property };
382
+ //#endregion
383
+ //#region src/rules/no-ambiguity-target/no-ambiguity-target.ts
360
384
  var no_ambiguity_target_default = createRule({
361
385
  name: "no-ambiguity-target",
362
386
  meta: {
@@ -396,6 +420,8 @@ var no_ambiguity_target_default = createRule({
396
420
  }
397
421
  });
398
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
399
425
  var no_domain_unit_creators_default = createRule({
400
426
  name: "no-domain-unit-creators",
401
427
  meta: {
@@ -408,16 +434,16 @@ var no_domain_unit_creators_default = createRule({
408
434
  create: (context) => {
409
435
  const services = ESLintUtils.getParserServices(context);
410
436
  return { [`CallExpression:has(> ${selector$9.member})`]: (node) => {
411
- const name$1 = node.callee.property.name;
412
- if (!METHODS.has(name$1)) return;
437
+ const name = node.callee.property.name;
438
+ if (!METHODS.has(name)) return;
413
439
  const type = services.getTypeAtLocation(node.callee.object);
414
440
  if (!isType.domain(type, services.program)) return;
415
- const factory = ALIAS_MAP.get(name$1) ?? name$1;
441
+ const factory = ALIAS_MAP.get(name) ?? name;
416
442
  context.report({
417
443
  node,
418
444
  messageId: "avoid",
419
445
  data: {
420
- method: name$1,
446
+ method: name,
421
447
  factory
422
448
  }
423
449
  });
@@ -427,6 +453,8 @@ var no_domain_unit_creators_default = createRule({
427
453
  const ALIAS_MAP = (/* @__PURE__ */ new Map()).set("event", "createEvent").set("store", "createStore").set("effect", "createEffect").set("domain", "createDomain");
428
454
  const METHODS = new Set([...ALIAS_MAP.values(), ...ALIAS_MAP.keys()]);
429
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
430
458
  var no_duplicate_clock_or_source_array_values_default = createRule({
431
459
  name: "no-duplicate-clock-or-source-array-values",
432
460
  meta: {
@@ -449,19 +477,19 @@ var no_duplicate_clock_or_source_array_values_default = createRule({
449
477
  for (const entry of entries) {
450
478
  const root = traverseToRoot$1(entry);
451
479
  if (!root) continue;
452
- const name$1 = [root.node.name, ...root.path].join(".");
453
- if (seen.has(name$1)) report(entry, name$1, field);
454
- else seen.set(name$1, entry);
480
+ const name = [root.node.name, ...root.path].join(".");
481
+ if (seen.has(name)) report(entry, name, field);
482
+ else seen.set(name, entry);
455
483
  }
456
484
  };
457
- const report = (node, name$1, field) => {
485
+ const report = (node, name, field) => {
458
486
  const data = {
459
487
  field,
460
- unit: name$1
488
+ unit: name
461
489
  };
462
490
  const suggestion = {
463
491
  messageId: "remove",
464
- data: { unit: name$1 },
492
+ data: { unit: name },
465
493
  fix: function* (fixer) {
466
494
  yield fixer.remove(node);
467
495
  const before = context.sourceCode.getTokenBefore(node);
@@ -501,6 +529,8 @@ function traverseToRoot$1(node, path = []) {
501
529
  if (node.type === AST_NODE_TYPES.MemberExpression && node.property.type === AST_NODE_TYPES.Identifier) return traverseToRoot$1(node.object, [node.property.name, ...path]);
502
530
  return null;
503
531
  }
532
+ //#endregion
533
+ //#region src/rules/no-duplicate-on/no-duplicate-on.ts
504
534
  var no_duplicate_on_default = createRule({
505
535
  name: "no-duplicate-on",
506
536
  meta: {
@@ -519,12 +549,12 @@ var no_duplicate_on_default = createRule({
519
549
  const arg = node.arguments[0];
520
550
  if (!arg || arg.type === AST_NODE_TYPES.SpreadElement) return;
521
551
  const units = arg.type === AST_NODE_TYPES.ArrayExpression ? arg.elements.filter((item) => item !== null && item.type !== AST_NODE_TYPES.SpreadElement) : [arg];
522
- const scope$1 = context.sourceCode.getScope(node);
523
- const store = identify("store", node.callee.object, scope$1);
552
+ const scope = context.sourceCode.getScope(node);
553
+ const store = identify("store", node.callee.object, scope);
524
554
  if (!store) return;
525
555
  const set = map.get(store.id) ?? /* @__PURE__ */ new Set();
526
556
  for (const unit of units) {
527
- const instance = identify("unit", unit, scope$1);
557
+ const instance = identify("unit", unit, scope);
528
558
  if (!instance) continue;
529
559
  if (set.has(instance.id)) {
530
560
  const data = {
@@ -585,16 +615,18 @@ function findSuitableRoot(type, node) {
585
615
  };
586
616
  return null;
587
617
  }
588
- function identify(type, node, scope$1) {
618
+ function identify(type, node, scope) {
589
619
  const root = findSuitableRoot(type, node);
590
620
  if (!root) return null;
591
- const variable = ASTUtils.findVariable(scope$1, root.node);
621
+ const variable = ASTUtils.findVariable(scope, root.node);
592
622
  if (!variable) return null;
593
623
  return {
594
624
  id: `${variable.$id}+${root.path.join(".")}`,
595
625
  name: [variable.name, ...root.path].join(".")
596
626
  };
597
627
  }
628
+ //#endregion
629
+ //#region src/rules/no-forward/no-forward.ts
598
630
  var no_forward_default = createRule({
599
631
  name: "no-forward",
600
632
  meta: {
@@ -624,11 +656,11 @@ var no_forward_default = createRule({
624
656
  config.clock = locate.property("from", arg)?.value;
625
657
  config.target = locate.property("to", arg)?.value;
626
658
  if (config.target) {
627
- const [call] = esquery.match(config.target, query$2.prepend, { visitorKeys }).map((node$1) => node$1).filter((node$1) => node$1 === config.target);
659
+ const [call] = esquery.match(config.target, query$2.prepend, { visitorKeys }).map((node) => node).filter((node) => node === config.target);
628
660
  if (call) [config.target, config.fn] = [call.callee.object, call.arguments[0]];
629
661
  }
630
662
  if (config.clock && !config.fn) {
631
- const [call] = esquery.match(config.clock, query$2.map, { visitorKeys }).map((node$1) => node$1).filter((node$1) => node$1 === config.clock);
663
+ const [call] = esquery.match(config.clock, query$2.map, { visitorKeys }).map((node) => node).filter((node) => node === config.clock);
632
664
  if (call) [config.clock, config.fn] = [call.callee.object, call.arguments[0]];
633
665
  }
634
666
  const code = [
@@ -662,6 +694,8 @@ const query$2 = {
662
694
  map: esquery.parse("CallExpression[arguments.length=1]:has(> :first-child:expression.arguments):has(> MemberExpression.callee:has(Identifier.property[name='map']))"),
663
695
  prepend: esquery.parse("CallExpression[arguments.length=1]:has(> :first-child:expression.arguments):has(> MemberExpression.callee:has(Identifier.property[name='prepend']))")
664
696
  };
697
+ //#endregion
698
+ //#region src/rules/no-getState/no-getState.ts
665
699
  var no_getState_default = createRule({
666
700
  name: "no-getState",
667
701
  meta: {
@@ -679,11 +713,11 @@ var no_getState_default = createRule({
679
713
  return { [`CallExpression[callee.type="MemberExpression"][callee.property.name="getState"]`]: (node) => {
680
714
  const type = services.getTypeAtLocation(node.callee.object);
681
715
  if (!isType.store(type, services.program)) return;
682
- const name$1 = toName$1(node.callee.object);
683
- if (name$1) context.report({
716
+ const name = toName$1(node.callee.object);
717
+ if (name) context.report({
684
718
  node,
685
719
  messageId: "named",
686
- data: { name: name$1 }
720
+ data: { name }
687
721
  });
688
722
  else context.report({
689
723
  node,
@@ -697,6 +731,8 @@ const toName$1 = (node) => {
697
731
  if (node.type === AST_NODE_TYPES.MemberExpression && !node.computed) return node.property.name;
698
732
  return null;
699
733
  };
734
+ //#endregion
735
+ //#region src/rules/no-guard/no-guard.ts
700
736
  var no_guard_default = createRule({
701
737
  name: "no-guard",
702
738
  meta: {
@@ -740,7 +776,7 @@ var no_guard_default = createRule({
740
776
  ]) config[key] = locate.property(key, arg)?.value;
741
777
  } else return;
742
778
  if (config.target) {
743
- const [call] = esquery.match(config.target, query$1.prepend, { visitorKeys }).map((node$1) => node$1).filter((node$1) => node$1 === config.target);
779
+ const [call] = esquery.match(config.target, query$1.prepend, { visitorKeys }).map((node) => node).filter((node) => node === config.target);
744
780
  if (call) [config.target, config.fn] = [call.callee.object, call.arguments[0]];
745
781
  }
746
782
  const code = [
@@ -772,6 +808,8 @@ const selector$6 = {
772
808
  call: `[callee.type="Identifier"]`
773
809
  };
774
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
775
813
  var no_patronum_debug_default = createRule({
776
814
  name: "no-patronum-debug",
777
815
  meta: {
@@ -790,8 +828,8 @@ var no_patronum_debug_default = createRule({
790
828
  return {
791
829
  [`${`ImportDeclaration[source.value=${PACKAGE_NAME}]`} > ${selector$5.debug}`]: (node) => debugs.add(node.local.name),
792
830
  [`CallExpression:matches(${selector$5.call})`]: (node) => {
793
- const name$1 = toName(node);
794
- if (!debugs.has(name$1)) return;
831
+ const name = toName(node);
832
+ if (!debugs.has(name)) return;
795
833
  context.report({
796
834
  messageId: "unexpected",
797
835
  node: node.callee,
@@ -818,6 +856,184 @@ const toName = (node) => {
818
856
  case AST_NODE_TYPES.MemberExpression: return node.callee.object.name;
819
857
  }
820
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
821
1037
  var no_unnecessary_combination_default = createRule({
822
1038
  name: "no-unnecessary-combination",
823
1039
  meta: {
@@ -888,6 +1104,8 @@ function isFunction(node, services) {
888
1104
  return checker.getTypeAtLocation(tsnode).getCallSignatures().length > 0;
889
1105
  } else return false;
890
1106
  }
1107
+ //#endregion
1108
+ //#region src/rules/no-unnecessary-duplication/no-unnecessary-duplication.ts
891
1109
  var no_unnecessary_duplication_default = createRule({
892
1110
  name: "no-unnecessary-duplication",
893
1111
  meta: {
@@ -964,6 +1182,8 @@ function compare(clock, source, limit = 5) {
964
1182
  }
965
1183
  return false;
966
1184
  }
1185
+ //#endregion
1186
+ //#region src/rules/no-useless-methods/no-useless-methods.ts
967
1187
  var no_useless_methods_default = createRule({
968
1188
  name: "no-useless-methods",
969
1189
  meta: {
@@ -1014,6 +1234,8 @@ var no_useless_methods_default = createRule({
1014
1234
  });
1015
1235
  const selector$2 = { method: `ImportSpecifier[imported.name=/(sample|guard)/]` };
1016
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
1017
1239
  var no_watch_default = createRule({
1018
1240
  name: "no-watch",
1019
1241
  meta: {
@@ -1035,6 +1257,8 @@ var no_watch_default = createRule({
1035
1257
  } };
1036
1258
  }
1037
1259
  });
1260
+ //#endregion
1261
+ //#region src/rules/prefer-useUnit/prefer-useUnit.ts
1038
1262
  var prefer_useUnit_default = createRule({
1039
1263
  name: "prefer-useUnit",
1040
1264
  meta: {
@@ -1066,6 +1290,8 @@ const selector$1 = {
1066
1290
  useStore: `ImportSpecifier[imported.name=useStore]`,
1067
1291
  useEvent: `ImportSpecifier[imported.name=useEvent]`
1068
1292
  };
1293
+ //#endregion
1294
+ //#region src/rules/require-pickup-in-persist/require-pickup-in-persist.ts
1069
1295
  var require_pickup_in_persist_default = createRule({
1070
1296
  name: "require-pickup-in-persist",
1071
1297
  meta: {
@@ -1095,6 +1321,8 @@ const selector = {
1095
1321
  call: `[callee.type="Identifier"]`,
1096
1322
  config: `[arguments.length=1][arguments.0.type="ObjectExpression"]`
1097
1323
  };
1324
+ //#endregion
1325
+ //#region src/rules/strict-effect-handlers/strict-effect-handlers.ts
1098
1326
  var strict_effect_handlers_default = createRule({
1099
1327
  name: "strict-effect-handlers",
1100
1328
  meta: {
@@ -1122,9 +1350,9 @@ var strict_effect_handlers_default = createRule({
1122
1350
  });
1123
1351
  };
1124
1352
  const exit = (node) => {
1125
- const scope$1 = stack.pop();
1126
- if (!scope$1) return;
1127
- if (scope$1.effect && scope$1.regular) context.report({
1353
+ const scope = stack.pop();
1354
+ if (!scope) return;
1355
+ if (scope.effect && scope.regular) context.report({
1128
1356
  node,
1129
1357
  messageId: "mixed"
1130
1358
  });
@@ -1163,10 +1391,13 @@ const ruleset = {
1163
1391
  react: {
1164
1392
  "effector/enforce-gate-naming-convention": "error",
1165
1393
  "effector/mandatory-scope-binding": "error",
1394
+ "effector/no-units-spawn-in-render": "error",
1166
1395
  "effector/prefer-useUnit": "error"
1167
1396
  },
1168
1397
  future: { "effector/no-domain-unit-creators": "warn" }
1169
1398
  };
1399
+ //#endregion
1400
+ //#region src/index.ts
1170
1401
  const base = {
1171
1402
  meta: {
1172
1403
  name,
@@ -1187,6 +1418,7 @@ const base = {
1187
1418
  "no-getState": no_getState_default,
1188
1419
  "no-guard": no_guard_default,
1189
1420
  "no-patronum-debug": no_patronum_debug_default,
1421
+ "no-units-spawn-in-render": no_units_spawn_in_render_default,
1190
1422
  "no-unnecessary-combination": no_unnecessary_combination_default,
1191
1423
  "no-unnecessary-duplication": no_unnecessary_duplication_default,
1192
1424
  "no-useless-methods": no_useless_methods_default,
@@ -1229,5 +1461,5 @@ const flatConfigs = {
1229
1461
  const plugin = base;
1230
1462
  plugin.configs = legacyConfigs;
1231
1463
  plugin.flatConfigs = flatConfigs;
1232
- var src_default = plugin;
1233
- export { src_default as default };
1464
+ //#endregion
1465
+ export { plugin as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-effector",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Enforcing best practices for Effector",
5
5
  "keywords": [
6
6
  "eslint",
@@ -42,35 +42,37 @@
42
42
  "dist"
43
43
  ],
44
44
  "dependencies": {
45
- "@typescript-eslint/type-utils": "^8.52.0",
46
- "@typescript-eslint/utils": "^8.52.0",
45
+ "@typescript-eslint/type-utils": "^8.58.0",
46
+ "@typescript-eslint/utils": "^8.58.0",
47
47
  "esquery": "^1.7.0"
48
48
  },
49
49
  "devDependencies": {
50
- "@eslint/js": "^9.39.2",
50
+ "@changesets/cli": "^2.30.0",
51
+ "@eslint/js": "^10.0.1",
51
52
  "@types/esquery": "^1.5.4",
52
53
  "@types/estree": "^1.0.8",
53
- "@types/node": "^25.0.6",
54
- "@types/react": "^19.2.8",
55
- "@typescript-eslint/rule-tester": "^8.52.0",
56
- "@vitest/coverage-v8": "^4.0.16",
54
+ "@types/node": "^25.5.2",
55
+ "@types/react": "^19.2.14",
56
+ "@typescript-eslint/rule-tester": "^8.58.0",
57
+ "@vitest/coverage-v8": "^4.1.3",
57
58
  "effector": "^23.4.4",
59
+ "effector-factorio": "^1.3.0",
58
60
  "effector-react": "^23.3.0",
59
- "eslint": "^9.39.2",
61
+ "eslint": "^10.2.0",
60
62
  "eslint-config-prettier": "^10.1.8",
61
63
  "eslint-import-resolver-typescript": "^4.4.4",
62
- "eslint-plugin-import-x": "^4.16.1",
63
- "eslint-plugin-prettier": "^5.5.4",
64
- "prettier": "^3.7.4",
64
+ "eslint-plugin-import-x": "^4.16.2",
65
+ "eslint-plugin-prettier": "^5.5.5",
66
+ "prettier": "^3.8.1",
65
67
  "prettier-plugin-embed": "^0.5.1",
66
- "tsdown": "0.19.0",
67
- "typescript": "^5.9.3",
68
- "typescript-eslint": "^8.52.0",
68
+ "tsdown": "0.21.7",
69
+ "typescript": "^6.0.2",
70
+ "typescript-eslint": "^8.58.0",
69
71
  "vitepress": "2.0.0-alpha.15",
70
- "vitest": "^4.0.16"
72
+ "vitest": "^4.1.3"
71
73
  },
72
74
  "peerDependencies": {
73
- "eslint": "^8.57.0 || ^9.0.0",
75
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
74
76
  "typescript": ">=5.0.0"
75
77
  },
76
78
  "engines": {