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.cjs +298 -69
- package/dist/index.d.cts +67 -80
- package/dist/index.d.mts +67 -80
- package/dist/index.mjs +271 -39
- package/package.json +19 -17
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.
|
|
7
|
-
|
|
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
|
|
14
|
+
const name = symbol.getName();
|
|
10
15
|
const declarations = symbol.declarations ?? [];
|
|
11
|
-
return types.includes(name
|
|
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
|
|
317
|
-
if (name
|
|
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)
|
|
323
|
-
if (inferred ? isType.component(
|
|
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
|
|
412
|
-
if (!METHODS.has(name
|
|
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
|
|
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
|
|
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
|
|
453
|
-
if (seen.has(name
|
|
454
|
-
else seen.set(name
|
|
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
|
|
485
|
+
const report = (node, name, field) => {
|
|
458
486
|
const data = {
|
|
459
487
|
field,
|
|
460
|
-
unit: name
|
|
488
|
+
unit: name
|
|
461
489
|
};
|
|
462
490
|
const suggestion = {
|
|
463
491
|
messageId: "remove",
|
|
464
|
-
data: { unit: name
|
|
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
|
|
523
|
-
const store = identify("store", node.callee.object, scope
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
683
|
-
if (name
|
|
716
|
+
const name = toName$1(node.callee.object);
|
|
717
|
+
if (name) context.report({
|
|
684
718
|
node,
|
|
685
719
|
messageId: "named",
|
|
686
|
-
data: { name
|
|
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
|
|
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
|
|
794
|
-
if (!debugs.has(name
|
|
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
|
|
1126
|
-
if (!scope
|
|
1127
|
-
if (scope
|
|
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
|
-
|
|
1233
|
-
export {
|
|
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.
|
|
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.
|
|
46
|
-
"@typescript-eslint/utils": "^8.
|
|
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
|
-
"@
|
|
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.
|
|
54
|
-
"@types/react": "^19.2.
|
|
55
|
-
"@typescript-eslint/rule-tester": "^8.
|
|
56
|
-
"@vitest/coverage-v8": "^4.
|
|
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": "^
|
|
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.
|
|
63
|
-
"eslint-plugin-prettier": "^5.5.
|
|
64
|
-
"prettier": "^3.
|
|
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.
|
|
67
|
-
"typescript": "^
|
|
68
|
-
"typescript-eslint": "^8.
|
|
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.
|
|
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": {
|