eslint-plugin-effector 0.3.1 → 0.5.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/README.md +40 -9
- package/config/react.js +5 -0
- package/config/recommended.js +3 -0
- package/config/scope.js +5 -0
- package/index.js +8 -1
- package/package.json +13 -15
- package/rules/enforce-effect-naming-convention/enforce-effect-naming-convention.js +19 -17
- package/rules/enforce-gate-naming-convention/enforce-gate-naming-convention.js +114 -0
- package/rules/enforce-gate-naming-convention/enforce-gate-naming-convention.md +11 -0
- package/rules/enforce-store-naming-convention/enforce-store-naming-convention.js +46 -40
- package/rules/enforce-store-naming-convention/enforce-store-naming-convention.md +12 -4
- package/rules/no-ambiguity-target/no-ambiguity-target.js +8 -4
- package/rules/no-duplicate-on/no-duplicate-on.js +128 -0
- package/rules/no-duplicate-on/no-duplicate-on.md +16 -0
- package/rules/no-getState/no-getState.js +11 -36
- package/rules/no-unnecessary-combination/no-unnecessary-combination.js +96 -0
- package/rules/no-unnecessary-combination/no-unnecessary-combination.md +25 -0
- package/rules/no-unnecessary-duplication/no-unnecessary-duplication.js +9 -5
- package/rules/no-useless-methods/no-useless-methods.js +18 -4
- package/rules/no-watch/no-watch.js +53 -0
- package/rules/no-watch/no-watch.md +42 -0
- package/rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping.js +8 -4
- package/rules/strict-effect-handlers/strict-effect-handlers.js +74 -0
- package/rules/strict-effect-handlers/strict-effect-handlers.md +38 -0
- package/rules/tsconfig.json +2 -1
- package/utils/create-link-to-rule.js +5 -0
- package/utils/extract-imported-from.js +9 -0
- package/utils/get-corrected-store-name.js +12 -14
- package/utils/get-nested-object-name.js +18 -0
- package/utils/get-store-name-convention.js +3 -3
- package/utils/is.js +30 -0
- package/utils/naming.js +47 -0
- package/utils/node-type-is.js +55 -0
- package/utils/validate-store-name-convention.js +7 -7
- package/.github/workflows/ci.yml +0 -43
- package/CHANGELOG.md +0 -28
- package/jest.config.js +0 -7
- package/utils/extract-imported-from-effector.js +0 -8
- package/utils/is-store-name-valid.js +0 -22
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
2
|
+
const { getNestedObjectName } = require("../../utils/get-nested-object-name");
|
|
3
|
+
const { is } = require("../../utils/is");
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Forbids duplicate `.on` calls on store",
|
|
10
|
+
category: "Quality",
|
|
11
|
+
recommended: true,
|
|
12
|
+
url: createLinkToRule("no-duplicate-on"),
|
|
13
|
+
},
|
|
14
|
+
messages: {
|
|
15
|
+
duplicateOn:
|
|
16
|
+
"Method `.on` is called on store `{{ storeName }}` more than once for {{ unitName }}.",
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
const usedOns = new Map();
|
|
22
|
+
|
|
23
|
+
function isEventUsedInStoreOn(scope, storeName, unitName) {
|
|
24
|
+
const usedOnsOnScope = usedOns.get(scope);
|
|
25
|
+
|
|
26
|
+
if (!usedOnsOnScope) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const usedUnits = usedOnsOnScope.get(storeName);
|
|
31
|
+
|
|
32
|
+
if (!usedUnits) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return usedUnits.has(unitName);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function markUnitAsUsedInStoreOn(scope, storeName, unitNames) {
|
|
40
|
+
let usedOnsOnScope = usedOns.get(scope);
|
|
41
|
+
|
|
42
|
+
if (!usedOnsOnScope) {
|
|
43
|
+
usedOnsOnScope = new Map();
|
|
44
|
+
usedOns.set(scope, usedOnsOnScope);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let usedUnits = usedOnsOnScope.get(storeName);
|
|
48
|
+
|
|
49
|
+
if (!usedUnits) {
|
|
50
|
+
usedUnits = new Set();
|
|
51
|
+
usedOnsOnScope.set(storeName, usedUnits);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
usedUnits.add(...unitNames);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
'CallExpression[callee.property.name="on"]'(node) {
|
|
59
|
+
const storeObject = getNestedCallee(node) ?? getAssignedVariable(node);
|
|
60
|
+
const storeName = getStoreName(storeObject);
|
|
61
|
+
|
|
62
|
+
if (!is.store({ context, node: storeObject })) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const triggerObjects = normalizePossibleArrayNode(node.arguments[0]);
|
|
67
|
+
const unitNames = triggerObjects.map(getNestedObjectName);
|
|
68
|
+
|
|
69
|
+
const scope = context.getScope();
|
|
70
|
+
|
|
71
|
+
for (const unitName of unitNames) {
|
|
72
|
+
const unitAlreadyUsed = isEventUsedInStoreOn(
|
|
73
|
+
scope,
|
|
74
|
+
storeName,
|
|
75
|
+
unitName
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (unitAlreadyUsed) {
|
|
79
|
+
context.report({
|
|
80
|
+
node,
|
|
81
|
+
messageId: "duplicateOn",
|
|
82
|
+
data: {
|
|
83
|
+
storeName,
|
|
84
|
+
unitName,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
markUnitAsUsedInStoreOn(scope, storeName, unitNames);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function normalizePossibleArrayNode(node) {
|
|
99
|
+
if (node.type === "ArrayExpression") {
|
|
100
|
+
return node.elements;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return [node];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getNestedCallee(node) {
|
|
107
|
+
const { callee } = node;
|
|
108
|
+
|
|
109
|
+
if (callee.object?.type === "CallExpression") {
|
|
110
|
+
return getNestedCallee(callee.object);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return callee.object;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getAssignedVariable(node) {
|
|
117
|
+
const { parent } = node;
|
|
118
|
+
|
|
119
|
+
if (parent.type === "VariableDeclarator") {
|
|
120
|
+
return parent;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return getAssignedVariable(parent);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getStoreName(node) {
|
|
127
|
+
return node.name ?? node.id.name;
|
|
128
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# effector/no-duplicate-on
|
|
2
|
+
|
|
3
|
+
Disallows duplcates `on`-handlers on particular store.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
const increment = createEvent();
|
|
7
|
+
|
|
8
|
+
// 👍 all explicitly
|
|
9
|
+
const $goodCounter = createStore(0).on(increment, (counter) => counter + 1);
|
|
10
|
+
|
|
11
|
+
// 👎 so, which handler should we choose?
|
|
12
|
+
// it's better to remove one of them
|
|
13
|
+
const $badCounter = createStore(0)
|
|
14
|
+
.on(increment, (counter) => counter + 1)
|
|
15
|
+
.on(increment, (counter) => counter + 2);
|
|
16
|
+
```
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
const {
|
|
2
2
|
traverseNestedObjectNode,
|
|
3
3
|
} = require("../../utils/traverse-nested-object-node");
|
|
4
|
-
const {
|
|
4
|
+
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
5
|
+
const { is } = require("../../utils/is");
|
|
5
6
|
|
|
6
7
|
module.exports = {
|
|
7
8
|
meta: {
|
|
@@ -10,6 +11,7 @@ module.exports = {
|
|
|
10
11
|
description: "Forbids `.getState` calls on any Effector store",
|
|
11
12
|
category: "Quality",
|
|
12
13
|
recommended: true,
|
|
14
|
+
url: createLinkToRule("no-getState"),
|
|
13
15
|
},
|
|
14
16
|
messages: {
|
|
15
17
|
abusiveCall:
|
|
@@ -18,47 +20,20 @@ module.exports = {
|
|
|
18
20
|
schema: [],
|
|
19
21
|
},
|
|
20
22
|
create(context) {
|
|
21
|
-
const { parserServices } = context;
|
|
22
|
-
|
|
23
23
|
return {
|
|
24
|
-
CallExpression(node) {
|
|
25
|
-
const
|
|
26
|
-
if (methodName !== "getState") {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
24
|
+
'CallExpression[callee.property.name="getState"]'(node) {
|
|
25
|
+
const storeNode = traverseNestedObjectNode(node.callee?.object);
|
|
29
26
|
|
|
30
|
-
const
|
|
31
|
-
|
|
27
|
+
const isEffectorStore = is.store({
|
|
28
|
+
context,
|
|
29
|
+
node: storeNode,
|
|
30
|
+
});
|
|
32
31
|
|
|
33
|
-
if (!
|
|
32
|
+
if (!isEffectorStore) {
|
|
34
33
|
return;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
if (parserServices.hasFullTypeInformation) {
|
|
39
|
-
const checker = parserServices.program.getTypeChecker();
|
|
40
|
-
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(object);
|
|
41
|
-
const type = checker.getTypeAtLocation(originalNode);
|
|
42
|
-
|
|
43
|
-
const isEffectorStore =
|
|
44
|
-
type?.symbol?.escapedName === "Store" &&
|
|
45
|
-
type?.symbol?.parent?.escapedName?.includes("effector");
|
|
46
|
-
|
|
47
|
-
if (!isEffectorStore) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
reportGetStateCall({ context, node, storeName: objectName });
|
|
52
|
-
}
|
|
53
|
-
// JavaScript-way
|
|
54
|
-
else {
|
|
55
|
-
const isEffectorStore = isStoreNameValid(objectName, context);
|
|
56
|
-
if (!isEffectorStore) {
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
reportGetStateCall({ context, node, storeName: objectName });
|
|
61
|
-
}
|
|
36
|
+
reportGetStateCall({ context, node, storeName: storeNode.name });
|
|
62
37
|
},
|
|
63
38
|
};
|
|
64
39
|
},
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const { extractImportedFrom } = require("../../utils/extract-imported-from");
|
|
2
|
+
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
meta: {
|
|
6
|
+
type: "problem",
|
|
7
|
+
docs: {
|
|
8
|
+
description:
|
|
9
|
+
"Forbids unnecessary combinations in `clock`, `source` and `forward`",
|
|
10
|
+
category: "Quality",
|
|
11
|
+
recommended: true,
|
|
12
|
+
url: createLinkToRule("no-unnecessary-combination"),
|
|
13
|
+
},
|
|
14
|
+
messages: {
|
|
15
|
+
unnecessaryCombination:
|
|
16
|
+
"Method {{ methodName }} is used under the hood, you can omit it.",
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
const importedFromEffector = new Map();
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
ImportDeclaration(node) {
|
|
25
|
+
extractImportedFrom({
|
|
26
|
+
importMap: importedFromEffector,
|
|
27
|
+
node,
|
|
28
|
+
packageName: "effector",
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
CallExpression(node) {
|
|
32
|
+
const METHODS_WITH_POSSIBLE_UNNECESSARY_COMBINATION = [
|
|
33
|
+
"sample",
|
|
34
|
+
"guard",
|
|
35
|
+
"forward",
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const CONFIG_ARG_PROPERTIES = ["source", "clock", "from"];
|
|
39
|
+
|
|
40
|
+
function toLocalMethod(method) {
|
|
41
|
+
return importedFromEffector.get(method);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const UNNECESSARY_METHODS = {
|
|
45
|
+
source: ["combine", "merge"].map(toLocalMethod).filter(Boolean),
|
|
46
|
+
clock: ["merge"].map(toLocalMethod).filter(Boolean),
|
|
47
|
+
from: ["merge"].map(toLocalMethod).filter(Boolean),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
for (const method of METHODS_WITH_POSSIBLE_UNNECESSARY_COMBINATION) {
|
|
51
|
+
const localMethod = importedFromEffector.get(method);
|
|
52
|
+
if (!localMethod) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const isEffectorMethod = node?.callee?.name === localMethod;
|
|
57
|
+
if (!isEffectorMethod) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const candidates =
|
|
62
|
+
node?.arguments?.[0]?.properties?.filter((n) =>
|
|
63
|
+
CONFIG_ARG_PROPERTIES.includes(n.key.name)
|
|
64
|
+
) ?? [];
|
|
65
|
+
|
|
66
|
+
if (candidates.length === 0) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const candidate of candidates) {
|
|
71
|
+
const candidateName = candidate?.value?.callee?.name;
|
|
72
|
+
const argProp = candidate?.key?.name;
|
|
73
|
+
if (!candidateName || !argProp) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const localUnnecessaryMethods = UNNECESSARY_METHODS[argProp];
|
|
78
|
+
|
|
79
|
+
const UnnecessaryMethodIsEffectorMethod =
|
|
80
|
+
localUnnecessaryMethods.some((m) => m === candidateName);
|
|
81
|
+
|
|
82
|
+
if (!UnnecessaryMethodIsEffectorMethod) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
context.report({
|
|
87
|
+
node: candidate?.value,
|
|
88
|
+
messageId: "unnecessaryCombination",
|
|
89
|
+
data: { methodName: candidateName },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# effector/no-unnecessary-combination
|
|
2
|
+
|
|
3
|
+
Call of `combine`/`merge` in `clock`/`source` is unnecessary. It can be omitted from source code.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
// 👎 can be simplified
|
|
7
|
+
const badEventOne = guard({
|
|
8
|
+
clock: combine($store1, $store2),
|
|
9
|
+
filter: $filter,
|
|
10
|
+
});
|
|
11
|
+
const badEventOne = guard({
|
|
12
|
+
clock: combine($store1, $store2, (store1, store2) => ({
|
|
13
|
+
x: store1,
|
|
14
|
+
y: store2,
|
|
15
|
+
})),
|
|
16
|
+
filter: $filter,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// 👍 better
|
|
20
|
+
const goodEventOne = guard({ clock: [$store1, $store2], filter: $filter });
|
|
21
|
+
const goodEventTwo = guard({
|
|
22
|
+
clock: { x: $store1, x: $store2 },
|
|
23
|
+
filter: $filter,
|
|
24
|
+
});
|
|
25
|
+
```
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
const {
|
|
2
|
-
extractImportedFromEffector,
|
|
3
|
-
} = require("../../utils/extract-imported-from-effector");
|
|
1
|
+
const { extractImportedFrom } = require("../../utils/extract-imported-from");
|
|
4
2
|
const { areNodesSameInText } = require("../../utils/are-nodes-same-in-text");
|
|
3
|
+
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
5
4
|
|
|
6
5
|
module.exports = {
|
|
7
6
|
meta: {
|
|
@@ -10,6 +9,7 @@ module.exports = {
|
|
|
10
9
|
description: "Forbids unnecessary duplication in `clock` and `source`",
|
|
11
10
|
category: "Quality",
|
|
12
11
|
recommended: true,
|
|
12
|
+
url: createLinkToRule("no-unnecessary-duplication"),
|
|
13
13
|
},
|
|
14
14
|
messages: {
|
|
15
15
|
unnecessaryDuplication:
|
|
@@ -18,14 +18,18 @@ module.exports = {
|
|
|
18
18
|
removeSource: "Remove `source`",
|
|
19
19
|
},
|
|
20
20
|
schema: [],
|
|
21
|
+
hasSuggestions: true,
|
|
21
22
|
},
|
|
22
23
|
create(context) {
|
|
23
24
|
const importedFromEffector = new Map();
|
|
24
|
-
const sourceCode = context.getSourceCode();
|
|
25
25
|
|
|
26
26
|
return {
|
|
27
27
|
ImportDeclaration(node) {
|
|
28
|
-
|
|
28
|
+
extractImportedFrom({
|
|
29
|
+
importMap: importedFromEffector,
|
|
30
|
+
node,
|
|
31
|
+
packageName: "effector",
|
|
32
|
+
});
|
|
29
33
|
},
|
|
30
34
|
CallExpression(node) {
|
|
31
35
|
const METHODS_WITH_POSSIBLE_DUPLCATION = ["sample", "guard"];
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
const {
|
|
2
|
-
extractImportedFromEffector,
|
|
3
|
-
} = require("../../utils/extract-imported-from-effector");
|
|
1
|
+
const { extractImportedFrom } = require("../../utils/extract-imported-from");
|
|
4
2
|
const { traverseParentByType } = require("../../utils/traverse-parent-by-type");
|
|
3
|
+
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
5
4
|
|
|
6
5
|
module.exports = {
|
|
7
6
|
meta: {
|
|
@@ -10,6 +9,7 @@ module.exports = {
|
|
|
10
9
|
description: "Forbids useless calls of `sample` and `guard`",
|
|
11
10
|
category: "Quality",
|
|
12
11
|
recommended: true,
|
|
12
|
+
url: createLinkToRule("no-useless-methods"),
|
|
13
13
|
},
|
|
14
14
|
messages: {
|
|
15
15
|
uselessMethod:
|
|
@@ -22,7 +22,11 @@ module.exports = {
|
|
|
22
22
|
|
|
23
23
|
return {
|
|
24
24
|
ImportDeclaration(node) {
|
|
25
|
-
|
|
25
|
+
extractImportedFrom({
|
|
26
|
+
importMap: importedFromEffector,
|
|
27
|
+
node,
|
|
28
|
+
packageName: "effector",
|
|
29
|
+
});
|
|
26
30
|
},
|
|
27
31
|
CallExpression(node) {
|
|
28
32
|
const POSSIBLE_USELESS_METHODS = ["sample", "guard"];
|
|
@@ -68,6 +72,16 @@ module.exports = {
|
|
|
68
72
|
continue;
|
|
69
73
|
}
|
|
70
74
|
|
|
75
|
+
const resultIsWatched = node?.parent?.property?.name === "watch";
|
|
76
|
+
if (resultIsWatched) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const resultIsArgument = node?.parent?.type === "CallExpression";
|
|
81
|
+
if (resultIsArgument) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
71
85
|
context.report({
|
|
72
86
|
node,
|
|
73
87
|
messageId: "uselessMethod",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const {
|
|
2
|
+
traverseNestedObjectNode,
|
|
3
|
+
} = require("../../utils/traverse-nested-object-node");
|
|
4
|
+
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
5
|
+
const { nodeTypeIs } = require("../../utils/node-type-is");
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "suggestion",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Avoid `.watch` calls on any Effector unit or operator",
|
|
12
|
+
category: "Quality",
|
|
13
|
+
recommended: true,
|
|
14
|
+
url: createLinkToRule("no-watch"),
|
|
15
|
+
},
|
|
16
|
+
messages: {
|
|
17
|
+
abusiveCall:
|
|
18
|
+
"Method `.watch` leads to imperative code. Try to replace it with operators (`sample`, `guard`, etc) or use the `target` parameter of the operators.",
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
},
|
|
22
|
+
create(context) {
|
|
23
|
+
const { parserServices } = context;
|
|
24
|
+
if (!parserServices.hasFullTypeInformation) {
|
|
25
|
+
// JavaScript-way https://github.com/effector/eslint-plugin/issues/48#issuecomment-931107829
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
'CallExpression[callee.property.name="watch"]'(node) {
|
|
31
|
+
const object = traverseNestedObjectNode(node.callee?.object);
|
|
32
|
+
|
|
33
|
+
const isEffectorUnit = nodeTypeIs.unit({
|
|
34
|
+
node: object,
|
|
35
|
+
context,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!isEffectorUnit) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
reportWatchCall({ context, node });
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function reportWatchCall({ context, node }) {
|
|
49
|
+
context.report({
|
|
50
|
+
node,
|
|
51
|
+
messageId: "abusiveCall",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# effector/no-watch
|
|
2
|
+
|
|
3
|
+
Method `.watch` leads to imperative code. Try replacing it with operators (`forward`, `sample`, etc) or use the `target` parameter of the operators.
|
|
4
|
+
|
|
5
|
+
> Caution! This rule only works on projects using TypeScript.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
const myFx = createEffect();
|
|
9
|
+
const myEvent = createEvent();
|
|
10
|
+
const $awesome = createStore();
|
|
11
|
+
|
|
12
|
+
// 👍 good solutions
|
|
13
|
+
forward({
|
|
14
|
+
from: myFx.finally,
|
|
15
|
+
to: myEvent,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
guard({
|
|
19
|
+
clock: myEvent,
|
|
20
|
+
filter: Boolean,
|
|
21
|
+
target: myFx,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
sample({
|
|
25
|
+
from: $awesome.updates,
|
|
26
|
+
fn: identity,
|
|
27
|
+
to: myEvent,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// 👎 bad solutions
|
|
31
|
+
myFx.finally.watch(myEvent);
|
|
32
|
+
|
|
33
|
+
myEvent.watch((payload) => {
|
|
34
|
+
if (Boolean(payload)) {
|
|
35
|
+
myFx(payload);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
$awesome.updates.watch((data) => {
|
|
40
|
+
myEvent(identity(data));
|
|
41
|
+
});
|
|
42
|
+
```
|
package/rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
const {
|
|
2
|
-
extractImportedFromEffector,
|
|
3
|
-
} = require("../../utils/extract-imported-from-effector");
|
|
1
|
+
const { extractImportedFrom } = require("../../utils/extract-imported-from");
|
|
4
2
|
const {
|
|
5
3
|
traverseNestedObjectNode,
|
|
6
4
|
} = require("../../utils/traverse-nested-object-node");
|
|
5
|
+
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
7
6
|
|
|
8
7
|
module.exports = {
|
|
9
8
|
meta: {
|
|
@@ -12,6 +11,7 @@ module.exports = {
|
|
|
12
11
|
description: "Prefer `sample` over `forward` with `.map`/`.prepend`",
|
|
13
12
|
category: "Quality",
|
|
14
13
|
recommended: true,
|
|
14
|
+
url: createLinkToRule("prefer-sample-over-forward-with-mapping"),
|
|
15
15
|
},
|
|
16
16
|
messages: {
|
|
17
17
|
overMap:
|
|
@@ -26,7 +26,11 @@ module.exports = {
|
|
|
26
26
|
|
|
27
27
|
return {
|
|
28
28
|
ImportDeclaration(node) {
|
|
29
|
-
|
|
29
|
+
extractImportedFrom({
|
|
30
|
+
importMap: importedFromEffector,
|
|
31
|
+
node,
|
|
32
|
+
packageName: "effector",
|
|
33
|
+
});
|
|
30
34
|
},
|
|
31
35
|
CallExpression(node) {
|
|
32
36
|
const localMethod = importedFromEffector.get("forward");
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
2
|
+
const { is } = require("../../utils/is");
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
meta: {
|
|
6
|
+
type: "problem",
|
|
7
|
+
docs: {
|
|
8
|
+
description:
|
|
9
|
+
"Forbids mix of async functions and effects calls in effect handlers.",
|
|
10
|
+
category: "Quality",
|
|
11
|
+
recommended: true,
|
|
12
|
+
url: createLinkToRule("strict-effect-handlers"),
|
|
13
|
+
},
|
|
14
|
+
messages: {
|
|
15
|
+
mixedCallsInHandler:
|
|
16
|
+
"Handler of effect `{{ effectName }}` can lead to scope loosing in Fork API.",
|
|
17
|
+
mixedCallsInFunction:
|
|
18
|
+
"Function `{{ functionName }}` can lead to scope loosing in Fork API.",
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
},
|
|
22
|
+
create(context) {
|
|
23
|
+
function onEffectHandler(node) {
|
|
24
|
+
if (!node.body?.body) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const calledNodes = node.body.body
|
|
29
|
+
.filter((bodyNode) => bodyNode.expression?.type === "AwaitExpression")
|
|
30
|
+
.map((awaitNode) => ({
|
|
31
|
+
node: awaitNode.expression.argument.callee,
|
|
32
|
+
context,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
const hasEffects = calledNodes.some(is.effect);
|
|
36
|
+
const hasRegularAsyncFunctions = calledNodes.some(is.not.effect);
|
|
37
|
+
|
|
38
|
+
const hasError = hasEffects && hasRegularAsyncFunctions;
|
|
39
|
+
|
|
40
|
+
if (!hasError) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const isEffectHandler = is.effect({
|
|
45
|
+
node: node.parent?.parent,
|
|
46
|
+
context,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (isEffectHandler) {
|
|
50
|
+
const effectName = node.parent?.parent?.id?.name ?? "Unknown";
|
|
51
|
+
|
|
52
|
+
context.report({
|
|
53
|
+
node: node.parent,
|
|
54
|
+
messageId: "mixedCallsInHandler",
|
|
55
|
+
data: { effectName },
|
|
56
|
+
});
|
|
57
|
+
} else {
|
|
58
|
+
const functionName = node.id?.name ?? "Unknown";
|
|
59
|
+
|
|
60
|
+
context.report({
|
|
61
|
+
node,
|
|
62
|
+
messageId: "mixedCallsInFunction",
|
|
63
|
+
data: { functionName },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
ArrowFunctionExpression: onEffectHandler,
|
|
70
|
+
FunctionExpression: onEffectHandler,
|
|
71
|
+
FunctionDeclaration: onEffectHandler,
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# effector/strict-effect-handlers
|
|
2
|
+
|
|
3
|
+
[Related documentation](https://effector.dev/docs/api/effector/scope#imperative-effects-calls-with-scope)
|
|
4
|
+
|
|
5
|
+
When effect calls another effects then it should call only effects, not common async functions and effect calls should have await:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// 👍 effect without inner effects:
|
|
9
|
+
const delayFx = createEffect(async () => {
|
|
10
|
+
await new Promise((rs) => setTimeout(rs, 80));
|
|
11
|
+
});
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
const authUserFx = createEffect();
|
|
16
|
+
const sendMessageFx = createEffect();
|
|
17
|
+
|
|
18
|
+
// 👍 effect with inner effects
|
|
19
|
+
const sendWithAuthFx = createEffect(async () => {
|
|
20
|
+
await authUserFx();
|
|
21
|
+
await delayFx();
|
|
22
|
+
await sendMessageFx();
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// 👎 effect with inner effects and common async functions
|
|
28
|
+
|
|
29
|
+
const sendWithAuthFx = createEffect(async () => {
|
|
30
|
+
await authUserFx();
|
|
31
|
+
//WRONG! wrap that in effect
|
|
32
|
+
await new Promise((rs) => setTimeout(rs, 80));
|
|
33
|
+
//context lost
|
|
34
|
+
await sendMessageFx();
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
So, any effect might either call another effects or perform some async computations but not both.
|
package/rules/tsconfig.json
CHANGED