eslint-plugin-effector 0.4.2 → 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 +20 -3
- package/config/react.js +5 -0
- package/config/recommended.js +1 -0
- package/config/scope.js +5 -0
- package/index.js +5 -0
- package/package.json +4 -1
- package/rules/enforce-effect-naming-convention/enforce-effect-naming-convention.js +16 -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 +25 -25
- package/rules/enforce-store-naming-convention/enforce-store-naming-convention.md +11 -3
- package/rules/no-ambiguity-target/no-ambiguity-target.js +6 -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 +9 -36
- package/rules/no-unnecessary-combination/no-unnecessary-combination.js +6 -4
- package/rules/no-unnecessary-combination/no-unnecessary-combination.md +15 -4
- package/rules/no-unnecessary-duplication/no-unnecessary-duplication.js +6 -5
- package/rules/no-useless-methods/no-useless-methods.js +6 -4
- package/rules/no-watch/no-watch.js +6 -12
- package/rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping.js +6 -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 +1 -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/utils/extract-imported-from-effector.js +0 -8
- package/utils/is-store-name-valid.js +0 -22
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Add `effector` to the plugins section of your `.eslintrc` configuration file. Yo
|
|
|
25
25
|
```json
|
|
26
26
|
{
|
|
27
27
|
"plugins": ["effector"],
|
|
28
|
-
"extends": ["plugin:effector/recommended"]
|
|
28
|
+
"extends": ["plugin:effector/recommended", "plugin:effector/scope"]
|
|
29
29
|
}
|
|
30
30
|
```
|
|
31
31
|
|
|
@@ -39,7 +39,21 @@ To configure individual rules:
|
|
|
39
39
|
}
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
### Available presets
|
|
43
|
+
|
|
44
|
+
#### plugin:effector/recommended
|
|
45
|
+
|
|
46
|
+
This preset is recommended for most projects.
|
|
47
|
+
|
|
48
|
+
#### plugin:effector/scope
|
|
49
|
+
|
|
50
|
+
This preset is recommended for projects that use [Fork API](https://effector.dev/docs/api/effector/scope). You can read more about Fork API in [an article](https://dev.to/effector/the-best-part-of-effector-4c27).
|
|
51
|
+
|
|
52
|
+
#### plugin:effector/react
|
|
53
|
+
|
|
54
|
+
This preset is recommended for projects that use [React](https://reactjs.org) with Effector.
|
|
55
|
+
|
|
56
|
+
### Supported rules
|
|
43
57
|
|
|
44
58
|
- [effector/enforce-store-naming-convention](rules/enforce-store-naming-convention/enforce-store-naming-convention.md)
|
|
45
59
|
- [effector/enforce-effect-naming-convention](rules/enforce-effect-naming-convention/enforce-effect-naming-convention.md)
|
|
@@ -50,6 +64,9 @@ To configure individual rules:
|
|
|
50
64
|
- [effector/prefer-sample-over-forward-with-mapping](rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping.md)
|
|
51
65
|
- [effector/no-watch](rules/no-watch/no-watch.md)
|
|
52
66
|
- [effector/no-unnecessary-combination](rules/no-unnecessary-combination/no-unnecessary-combination.md)
|
|
67
|
+
- [effector/no-duplicate-on](rules/no-duplicate-on/no-duplicate-on.md)
|
|
68
|
+
- [effector/strict-effect-handlers](rules/strict-effect-handlers/strict-effect-handlers.md)
|
|
69
|
+
- [effector/enforce-gate-naming-convention](rules/enforce-gate-naming-convention/enforce-gate-naming-convention.md)
|
|
53
70
|
|
|
54
71
|
## Maintenance
|
|
55
72
|
|
|
@@ -57,7 +74,7 @@ To configure individual rules:
|
|
|
57
74
|
|
|
58
75
|
1. Bump `version` in [package.json](package.json)
|
|
59
76
|
2. Fill [CHANGELOG.md](CHANGELOG.md)
|
|
60
|
-
3. Commit changes by `git
|
|
77
|
+
3. Commit changes by `git commit -m "Release X.X.X"`
|
|
61
78
|
4. Create git tag for release by `git tag -a vX.X.X -m "vX.X.X"`
|
|
62
79
|
5. Push changes to remote by `git push --follow-tags`
|
|
63
80
|
6. Release package to registry by `yarn clean-publish`
|
package/config/react.js
ADDED
package/config/recommended.js
CHANGED
package/config/scope.js
ADDED
package/index.js
CHANGED
|
@@ -9,8 +9,13 @@ module.exports = {
|
|
|
9
9
|
"no-ambiguity-target": require("./rules/no-ambiguity-target/no-ambiguity-target"),
|
|
10
10
|
"no-watch": require("./rules/no-watch/no-watch"),
|
|
11
11
|
"no-unnecessary-combination": require("./rules/no-unnecessary-combination/no-unnecessary-combination"),
|
|
12
|
+
"no-duplicate-on": require("./rules/no-duplicate-on/no-duplicate-on"),
|
|
13
|
+
"strict-effect-handlers": require("./rules/strict-effect-handlers/strict-effect-handlers"),
|
|
14
|
+
"enforce-gate-naming-convention": require("./rules/enforce-gate-naming-convention/enforce-gate-naming-convention"),
|
|
12
15
|
},
|
|
13
16
|
configs: {
|
|
14
17
|
recommended: require("./config/recommended"),
|
|
18
|
+
scope: require("./config/scope"),
|
|
19
|
+
react: require("./config/react"),
|
|
15
20
|
},
|
|
16
21
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-effector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Enforcing best practices for Effector",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|
|
@@ -24,5 +24,8 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"prettier": "^2.3.2"
|
|
27
|
+
},
|
|
28
|
+
"nano-staged": {
|
|
29
|
+
"*.{js,ts,md}": "prettier --write"
|
|
27
30
|
}
|
|
28
31
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
const {
|
|
2
|
-
extractImportedFromEffector,
|
|
3
|
-
} = require("../../utils/extract-imported-from-effector");
|
|
1
|
+
const { extractImportedFrom } = require("../../utils/extract-imported-from");
|
|
4
2
|
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
3
|
+
const { nodeTypeIs } = require("../../utils/node-type-is");
|
|
4
|
+
const { namingOf } = require("../../utils/naming");
|
|
5
5
|
|
|
6
6
|
module.exports = {
|
|
7
7
|
meta: {
|
|
@@ -27,13 +27,10 @@ module.exports = {
|
|
|
27
27
|
if (parserServices.hasFullTypeInformation) {
|
|
28
28
|
return {
|
|
29
29
|
VariableDeclarator(node) {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const isEffectorEffect =
|
|
35
|
-
type?.symbol?.escapedName === "Effect" &&
|
|
36
|
-
type?.symbol?.parent?.escapedName?.includes("effector");
|
|
30
|
+
const isEffectorEffect = nodeTypeIs.effect({
|
|
31
|
+
node,
|
|
32
|
+
context,
|
|
33
|
+
});
|
|
37
34
|
|
|
38
35
|
if (!isEffectorEffect) {
|
|
39
36
|
return;
|
|
@@ -41,11 +38,9 @@ module.exports = {
|
|
|
41
38
|
|
|
42
39
|
const effectName = node.id.name;
|
|
43
40
|
|
|
44
|
-
if (effectName
|
|
45
|
-
|
|
41
|
+
if (namingOf.effect.isInvalid({ name: effectName })) {
|
|
42
|
+
reportEffectNameConventionViolation({ context, node, effectName });
|
|
46
43
|
}
|
|
47
|
-
|
|
48
|
-
reportEffectNameConventionViolation({ context, node, effectName });
|
|
49
44
|
},
|
|
50
45
|
};
|
|
51
46
|
}
|
|
@@ -54,7 +49,11 @@ module.exports = {
|
|
|
54
49
|
const importedFromEffector = new Map();
|
|
55
50
|
return {
|
|
56
51
|
ImportDeclaration(node) {
|
|
57
|
-
|
|
52
|
+
extractImportedFrom({
|
|
53
|
+
importMap: importedFromEffector,
|
|
54
|
+
node,
|
|
55
|
+
packageName: "effector",
|
|
56
|
+
});
|
|
58
57
|
},
|
|
59
58
|
CallExpression(node) {
|
|
60
59
|
// Effect creation with method
|
|
@@ -77,7 +76,7 @@ module.exports = {
|
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
const effectName = node.parent.id.name;
|
|
80
|
-
if (
|
|
79
|
+
if (namingOf.effect.isValid({ name: effectName })) {
|
|
81
80
|
continue;
|
|
82
81
|
}
|
|
83
82
|
|
|
@@ -101,7 +100,7 @@ module.exports = {
|
|
|
101
100
|
}
|
|
102
101
|
|
|
103
102
|
const effectName = node.parent.id.name;
|
|
104
|
-
if (
|
|
103
|
+
if (namingOf.effect.isValid({ name: effectName })) {
|
|
105
104
|
return;
|
|
106
105
|
}
|
|
107
106
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const { extractImportedFrom } = require("../../utils/extract-imported-from");
|
|
2
|
+
const { nodeTypeIs } = require("../../utils/node-type-is");
|
|
3
|
+
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
4
|
+
const { namingOf } = require("../../utils/naming");
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
meta: {
|
|
8
|
+
type: "problem",
|
|
9
|
+
docs: {
|
|
10
|
+
description: "Enforce first capital letter for gate naming",
|
|
11
|
+
category: "Naming",
|
|
12
|
+
recommended: true,
|
|
13
|
+
url: createLinkToRule("enforce-gate-naming-convention"),
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
invalidName:
|
|
17
|
+
'Gate "{{ gateName }}" should be named with first capital letter, rename it to "{{ correctedGateName }}"',
|
|
18
|
+
renameGate: 'Rename "{{ gateName }}" to "{{ correctedGateName }}"',
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
hasSuggestions: true,
|
|
22
|
+
},
|
|
23
|
+
create(context) {
|
|
24
|
+
const parserServices = context.parserServices;
|
|
25
|
+
// TypeScript-way
|
|
26
|
+
if (parserServices.hasFullTypeInformation) {
|
|
27
|
+
return {
|
|
28
|
+
VariableDeclarator(node) {
|
|
29
|
+
const isEffectorGate = nodeTypeIs.gate({
|
|
30
|
+
node,
|
|
31
|
+
context,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (!isEffectorGate) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const gateName = node.id.name;
|
|
39
|
+
|
|
40
|
+
if (namingOf.gate.isInvalid({ name: gateName })) {
|
|
41
|
+
reportGateNameConventionViolation({ context, node, gateName });
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// JavaScript-way
|
|
48
|
+
const importedFromEffectorReact = new Map();
|
|
49
|
+
return {
|
|
50
|
+
ImportDeclaration(node) {
|
|
51
|
+
extractImportedFrom({
|
|
52
|
+
importMap: importedFromEffectorReact,
|
|
53
|
+
node,
|
|
54
|
+
packageName: "effector-react",
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
CallExpression(node) {
|
|
58
|
+
// Effect creation with method
|
|
59
|
+
const GATE_CREATION_METHODS = ["createGate"];
|
|
60
|
+
for (const method of GATE_CREATION_METHODS) {
|
|
61
|
+
const localMethod = importedFromEffectorReact.get(method);
|
|
62
|
+
if (!localMethod) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const isEffectorGateCreation = node.callee.name === localMethod;
|
|
67
|
+
if (!isEffectorGateCreation) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const resultSavedInVariable =
|
|
72
|
+
node.parent.type === "VariableDeclarator";
|
|
73
|
+
if (!resultSavedInVariable) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const gateName = node.parent.id.name;
|
|
78
|
+
if (namingOf.gate.isValid({ name: gateName })) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
reportGateNameConventionViolation({
|
|
83
|
+
context,
|
|
84
|
+
node: node.parent,
|
|
85
|
+
gateName,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function reportGateNameConventionViolation({ context, node, gateName }) {
|
|
94
|
+
const [firstChar, ...restChars] = gateName.split("");
|
|
95
|
+
const correctedGateName = [firstChar.toUpperCase(), ...restChars].join("");
|
|
96
|
+
|
|
97
|
+
context.report({
|
|
98
|
+
node,
|
|
99
|
+
messageId: "invalidName",
|
|
100
|
+
data: {
|
|
101
|
+
gateName,
|
|
102
|
+
correctedGateName,
|
|
103
|
+
},
|
|
104
|
+
suggest: [
|
|
105
|
+
{
|
|
106
|
+
messageId: "renameGate",
|
|
107
|
+
data: { gateName, correctedGateName },
|
|
108
|
+
fix(fixer) {
|
|
109
|
+
return fixer.replaceTextRange(node.id.range, correctedGateName);
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# effector/enforce-gate-naming-convention
|
|
2
|
+
|
|
3
|
+
Enforcing naming conventions helps keep the codebase consistent, and reduces overhead when thinking about how to name a variable with gate. Every gate is a react-component, so it should be named as regular react-compoent.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
// 👍 nice name
|
|
7
|
+
const MyFavoritePageGate = createGate();
|
|
8
|
+
|
|
9
|
+
// 👎 bad name
|
|
10
|
+
const otherFavoritePageGate = createGate();
|
|
11
|
+
```
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
const {
|
|
2
|
-
|
|
3
|
-
} = require("../../utils/extract-imported-from-effector");
|
|
4
|
-
const { isStoreNameValid } = require("../../utils/is-store-name-valid");
|
|
1
|
+
const { extractImportedFrom } = require("../../utils/extract-imported-from");
|
|
2
|
+
const { namingOf } = require("../../utils/naming");
|
|
5
3
|
const {
|
|
6
4
|
validateStoreNameConvention,
|
|
7
5
|
} = require("../../utils/validate-store-name-convention");
|
|
@@ -12,6 +10,7 @@ const {
|
|
|
12
10
|
getCorrectedStoreName,
|
|
13
11
|
} = require("../../utils/get-corrected-store-name");
|
|
14
12
|
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
13
|
+
const { nodeTypeIs } = require("../../utils/node-type-is");
|
|
15
14
|
|
|
16
15
|
module.exports = {
|
|
17
16
|
meta: {
|
|
@@ -40,13 +39,10 @@ module.exports = {
|
|
|
40
39
|
if (parserServices.hasFullTypeInformation) {
|
|
41
40
|
return {
|
|
42
41
|
VariableDeclarator(node) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const isEffectorStore =
|
|
48
|
-
type?.symbol?.escapedName === "Store" &&
|
|
49
|
-
type?.symbol?.parent?.escapedName?.includes("effector");
|
|
42
|
+
const isEffectorStore = nodeTypeIs.store({
|
|
43
|
+
node,
|
|
44
|
+
context,
|
|
45
|
+
});
|
|
50
46
|
|
|
51
47
|
if (!isEffectorStore) {
|
|
52
48
|
return;
|
|
@@ -54,15 +50,13 @@ module.exports = {
|
|
|
54
50
|
|
|
55
51
|
const storeName = node.id.name;
|
|
56
52
|
|
|
57
|
-
if (
|
|
58
|
-
|
|
53
|
+
if (namingOf.store.isInvalid({ name: storeName, context })) {
|
|
54
|
+
reportStoreNameConventionViolation({
|
|
55
|
+
context,
|
|
56
|
+
node,
|
|
57
|
+
storeName,
|
|
58
|
+
});
|
|
59
59
|
}
|
|
60
|
-
|
|
61
|
-
reportStoreNameConventionViolation({
|
|
62
|
-
context,
|
|
63
|
-
node,
|
|
64
|
-
storeName,
|
|
65
|
-
});
|
|
66
60
|
},
|
|
67
61
|
};
|
|
68
62
|
}
|
|
@@ -71,7 +65,11 @@ module.exports = {
|
|
|
71
65
|
const importedFromEffector = new Map();
|
|
72
66
|
return {
|
|
73
67
|
ImportDeclaration(node) {
|
|
74
|
-
|
|
68
|
+
extractImportedFrom({
|
|
69
|
+
importMap: importedFromEffector,
|
|
70
|
+
node,
|
|
71
|
+
packageName: "effector",
|
|
72
|
+
});
|
|
75
73
|
},
|
|
76
74
|
CallExpression(node) {
|
|
77
75
|
// Store creation with method
|
|
@@ -95,7 +93,7 @@ module.exports = {
|
|
|
95
93
|
|
|
96
94
|
const storeName = node.parent.id.name;
|
|
97
95
|
|
|
98
|
-
if (
|
|
96
|
+
if (namingOf.store.isValid({ name: storeName, context })) {
|
|
99
97
|
continue;
|
|
100
98
|
}
|
|
101
99
|
|
|
@@ -111,7 +109,9 @@ module.exports = {
|
|
|
111
109
|
if (node.callee?.property?.name === "map") {
|
|
112
110
|
const storeNameCreatedFromMap = node.callee?.object?.name;
|
|
113
111
|
|
|
114
|
-
if (
|
|
112
|
+
if (
|
|
113
|
+
namingOf.store.isInvalid({ name: storeNameCreatedFromMap, context })
|
|
114
|
+
) {
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
117
|
|
|
@@ -123,7 +123,7 @@ module.exports = {
|
|
|
123
123
|
|
|
124
124
|
const storeName = node.parent.id.name;
|
|
125
125
|
|
|
126
|
-
if (
|
|
126
|
+
if (namingOf.store.isValid({ name: storeName, context })) {
|
|
127
127
|
return;
|
|
128
128
|
}
|
|
129
129
|
|
|
@@ -148,7 +148,7 @@ module.exports = {
|
|
|
148
148
|
|
|
149
149
|
const storeName = node.parent.id.name;
|
|
150
150
|
|
|
151
|
-
if (
|
|
151
|
+
if (namingOf.store.isValid({ name: storeName, context })) {
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -179,7 +179,7 @@ function reportStoreNameConventionViolation({ context, node, storeName }) {
|
|
|
179
179
|
suggest: [
|
|
180
180
|
{
|
|
181
181
|
messageId: "renameStore",
|
|
182
|
-
data: { storeName },
|
|
182
|
+
data: { storeName, correctedStoreName },
|
|
183
183
|
fix(fixer) {
|
|
184
184
|
return fixer.replaceTextRange(node.id.range, correctedStoreName);
|
|
185
185
|
},
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
Enforcing naming conventions helps keep the codebase consistent, and reduces overhead when thinking about how to name a variable with store. Depending on the configuration your stores should be distinguished by a prefix or a postfix $. Enforces prefix convention by default.
|
|
4
4
|
|
|
5
5
|
## Prefix convention
|
|
6
|
+
|
|
6
7
|
When configured as:
|
|
8
|
+
|
|
7
9
|
```js
|
|
8
10
|
module.exports = {
|
|
9
11
|
rules: {
|
|
@@ -11,7 +13,9 @@ module.exports = {
|
|
|
11
13
|
},
|
|
12
14
|
};
|
|
13
15
|
```
|
|
16
|
+
|
|
14
17
|
Prefix convention will be enforced:
|
|
18
|
+
|
|
15
19
|
```ts
|
|
16
20
|
// 👍 nice name
|
|
17
21
|
const $name = createStore(null);
|
|
@@ -19,9 +23,11 @@ const $name = createStore(null);
|
|
|
19
23
|
// 👎 bad name
|
|
20
24
|
const name = createStore(null);
|
|
21
25
|
```
|
|
26
|
+
|
|
22
27
|
## Postfix convention
|
|
23
28
|
|
|
24
29
|
When configured as:
|
|
30
|
+
|
|
25
31
|
```js
|
|
26
32
|
module.exports = {
|
|
27
33
|
rules: {
|
|
@@ -29,12 +35,14 @@ module.exports = {
|
|
|
29
35
|
},
|
|
30
36
|
settings: {
|
|
31
37
|
effector: {
|
|
32
|
-
storeNameConvention: "postfix"
|
|
33
|
-
}
|
|
34
|
-
}
|
|
38
|
+
storeNameConvention: "postfix",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
35
41
|
};
|
|
36
42
|
```
|
|
43
|
+
|
|
37
44
|
Postfix convention will be enforced:
|
|
45
|
+
|
|
38
46
|
```ts
|
|
39
47
|
// 👍 nice name
|
|
40
48
|
const name$ = createStore(null);
|
|
@@ -1,6 +1,4 @@
|
|
|
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");
|
|
5
3
|
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
6
4
|
|
|
@@ -24,7 +22,11 @@ module.exports = {
|
|
|
24
22
|
|
|
25
23
|
return {
|
|
26
24
|
ImportDeclaration(node) {
|
|
27
|
-
|
|
25
|
+
extractImportedFrom({
|
|
26
|
+
importMap: importedFromEffector,
|
|
27
|
+
node,
|
|
28
|
+
packageName: "effector",
|
|
29
|
+
});
|
|
28
30
|
},
|
|
29
31
|
CallExpression(node) {
|
|
30
32
|
const POSSIBLE_USELESS_METHODS = ["sample", "guard"];
|
|
@@ -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,8 +1,8 @@
|
|
|
1
1
|
const {
|
|
2
2
|
traverseNestedObjectNode,
|
|
3
3
|
} = require("../../utils/traverse-nested-object-node");
|
|
4
|
-
const { isStoreNameValid } = require("../../utils/is-store-name-valid");
|
|
5
4
|
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
5
|
+
const { is } = require("../../utils/is");
|
|
6
6
|
|
|
7
7
|
module.exports = {
|
|
8
8
|
meta: {
|
|
@@ -20,47 +20,20 @@ module.exports = {
|
|
|
20
20
|
schema: [],
|
|
21
21
|
},
|
|
22
22
|
create(context) {
|
|
23
|
-
const { parserServices } = context;
|
|
24
|
-
|
|
25
23
|
return {
|
|
26
|
-
CallExpression(node) {
|
|
27
|
-
const
|
|
28
|
-
if (methodName !== "getState") {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
24
|
+
'CallExpression[callee.property.name="getState"]'(node) {
|
|
25
|
+
const storeNode = traverseNestedObjectNode(node.callee?.object);
|
|
31
26
|
|
|
32
|
-
const
|
|
33
|
-
|
|
27
|
+
const isEffectorStore = is.store({
|
|
28
|
+
context,
|
|
29
|
+
node: storeNode,
|
|
30
|
+
});
|
|
34
31
|
|
|
35
|
-
if (!
|
|
32
|
+
if (!isEffectorStore) {
|
|
36
33
|
return;
|
|
37
34
|
}
|
|
38
35
|
|
|
39
|
-
|
|
40
|
-
if (parserServices.hasFullTypeInformation) {
|
|
41
|
-
const checker = parserServices.program.getTypeChecker();
|
|
42
|
-
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(object);
|
|
43
|
-
const type = checker.getTypeAtLocation(originalNode);
|
|
44
|
-
|
|
45
|
-
const isEffectorStore =
|
|
46
|
-
type?.symbol?.escapedName === "Store" &&
|
|
47
|
-
type?.symbol?.parent?.escapedName?.includes("effector");
|
|
48
|
-
|
|
49
|
-
if (!isEffectorStore) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
reportGetStateCall({ context, node, storeName: objectName });
|
|
54
|
-
}
|
|
55
|
-
// JavaScript-way
|
|
56
|
-
else {
|
|
57
|
-
const isEffectorStore = isStoreNameValid(objectName, context);
|
|
58
|
-
if (!isEffectorStore) {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
reportGetStateCall({ context, node, storeName: objectName });
|
|
63
|
-
}
|
|
36
|
+
reportGetStateCall({ context, node, storeName: storeNode.name });
|
|
64
37
|
},
|
|
65
38
|
};
|
|
66
39
|
},
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
const {
|
|
2
|
-
extractImportedFromEffector,
|
|
3
|
-
} = require("../../utils/extract-imported-from-effector");
|
|
1
|
+
const { extractImportedFrom } = require("../../utils/extract-imported-from");
|
|
4
2
|
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
5
3
|
|
|
6
4
|
module.exports = {
|
|
@@ -24,7 +22,11 @@ module.exports = {
|
|
|
24
22
|
|
|
25
23
|
return {
|
|
26
24
|
ImportDeclaration(node) {
|
|
27
|
-
|
|
25
|
+
extractImportedFrom({
|
|
26
|
+
importMap: importedFromEffector,
|
|
27
|
+
node,
|
|
28
|
+
packageName: "effector",
|
|
29
|
+
});
|
|
28
30
|
},
|
|
29
31
|
CallExpression(node) {
|
|
30
32
|
const METHODS_WITH_POSSIBLE_UNNECESSARY_COMBINATION = [
|
|
@@ -4,11 +4,22 @@ Call of `combine`/`merge` in `clock`/`source` is unnecessary. It can be omitted
|
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
// 👎 can be simplified
|
|
7
|
-
const badEventOne = guard({
|
|
8
|
-
|
|
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
|
+
});
|
|
9
18
|
|
|
10
19
|
// 👍 better
|
|
11
20
|
const goodEventOne = guard({ clock: [$store1, $store2], filter: $filter });
|
|
12
|
-
const goodEventTwo = guard({
|
|
21
|
+
const goodEventTwo = guard({
|
|
22
|
+
clock: { x: $store1, x: $store2 },
|
|
23
|
+
filter: $filter,
|
|
24
|
+
});
|
|
13
25
|
```
|
|
14
|
-
|
|
@@ -1,6 +1,4 @@
|
|
|
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");
|
|
5
3
|
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
6
4
|
|
|
@@ -24,11 +22,14 @@ module.exports = {
|
|
|
24
22
|
},
|
|
25
23
|
create(context) {
|
|
26
24
|
const importedFromEffector = new Map();
|
|
27
|
-
const sourceCode = context.getSourceCode();
|
|
28
25
|
|
|
29
26
|
return {
|
|
30
27
|
ImportDeclaration(node) {
|
|
31
|
-
|
|
28
|
+
extractImportedFrom({
|
|
29
|
+
importMap: importedFromEffector,
|
|
30
|
+
node,
|
|
31
|
+
packageName: "effector",
|
|
32
|
+
});
|
|
32
33
|
},
|
|
33
34
|
CallExpression(node) {
|
|
34
35
|
const METHODS_WITH_POSSIBLE_DUPLCATION = ["sample", "guard"];
|
|
@@ -1,6 +1,4 @@
|
|
|
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");
|
|
5
3
|
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
6
4
|
|
|
@@ -24,7 +22,11 @@ module.exports = {
|
|
|
24
22
|
|
|
25
23
|
return {
|
|
26
24
|
ImportDeclaration(node) {
|
|
27
|
-
|
|
25
|
+
extractImportedFrom({
|
|
26
|
+
importMap: importedFromEffector,
|
|
27
|
+
node,
|
|
28
|
+
packageName: "effector",
|
|
29
|
+
});
|
|
28
30
|
},
|
|
29
31
|
CallExpression(node) {
|
|
30
32
|
const POSSIBLE_USELESS_METHODS = ["sample", "guard"];
|
|
@@ -2,6 +2,7 @@ const {
|
|
|
2
2
|
traverseNestedObjectNode,
|
|
3
3
|
} = require("../../utils/traverse-nested-object-node");
|
|
4
4
|
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
5
|
+
const { nodeTypeIs } = require("../../utils/node-type-is");
|
|
5
6
|
|
|
6
7
|
module.exports = {
|
|
7
8
|
meta: {
|
|
@@ -24,22 +25,15 @@ module.exports = {
|
|
|
24
25
|
// JavaScript-way https://github.com/effector/eslint-plugin/issues/48#issuecomment-931107829
|
|
25
26
|
return {};
|
|
26
27
|
}
|
|
27
|
-
const checker = parserServices.program.getTypeChecker();
|
|
28
28
|
|
|
29
29
|
return {
|
|
30
|
-
CallExpression(node) {
|
|
31
|
-
const methodName = node.callee?.property?.name;
|
|
32
|
-
if (methodName !== "watch") {
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
30
|
+
'CallExpression[callee.property.name="watch"]'(node) {
|
|
36
31
|
const object = traverseNestedObjectNode(node.callee?.object);
|
|
37
|
-
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(object);
|
|
38
|
-
const type = checker.getTypeAtLocation(originalNode);
|
|
39
32
|
|
|
40
|
-
const isEffectorUnit =
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
const isEffectorUnit = nodeTypeIs.unit({
|
|
34
|
+
node: object,
|
|
35
|
+
context,
|
|
36
|
+
});
|
|
43
37
|
|
|
44
38
|
if (!isEffectorUnit) {
|
|
45
39
|
return;
|
package/rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
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");
|
|
@@ -28,7 +26,11 @@ module.exports = {
|
|
|
28
26
|
|
|
29
27
|
return {
|
|
30
28
|
ImportDeclaration(node) {
|
|
31
|
-
|
|
29
|
+
extractImportedFrom({
|
|
30
|
+
importMap: importedFromEffector,
|
|
31
|
+
node,
|
|
32
|
+
packageName: "effector",
|
|
33
|
+
});
|
|
32
34
|
},
|
|
33
35
|
CallExpression(node) {
|
|
34
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
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
const { getStoreNameConvention } = require("./get-store-name-convention");
|
|
2
2
|
|
|
3
3
|
function getCorrectedStoreName(storeName, context) {
|
|
4
|
-
|
|
4
|
+
const storeNameConvention = getStoreNameConvention(context);
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return `${storeName.slice(1)}$`;
|
|
13
|
-
}
|
|
6
|
+
// handle edge case
|
|
7
|
+
if (storeName.startsWith("$") && storeName.endsWith("$")) {
|
|
8
|
+
if (storeNameConvention === "prefix") {
|
|
9
|
+
return `$${storeName.slice(0, -1)}`;
|
|
10
|
+
} else {
|
|
11
|
+
return `${storeName.slice(1)}$`;
|
|
14
12
|
}
|
|
13
|
+
}
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
: `${storeName}$`;
|
|
15
|
+
const correctedStoreName =
|
|
16
|
+
storeNameConvention === "prefix" ? `$${storeName}` : `${storeName}$`;
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
return correctedStoreName;
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
module.exports = { getCorrectedStoreName };
|
|
21
|
+
module.exports = { getCorrectedStoreName };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function getNestedObjectName(node) {
|
|
2
|
+
let root = node;
|
|
3
|
+
let name = "";
|
|
4
|
+
|
|
5
|
+
while (root.type === "MemberExpression") {
|
|
6
|
+
name = `${root.property.name}.${name}`;
|
|
7
|
+
root = root.object;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (root.type === "Identifier") {
|
|
11
|
+
name = `${root.name}.${name}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Remove last dot
|
|
15
|
+
return name.slice(0, -1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { getNestedObjectName };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
function getStoreNameConvention(context) {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
// prefix convention is default
|
|
3
|
+
return context.settings.effector?.storeNameConvention || "prefix";
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
module.exports = { getStoreNameConvention };
|
|
6
|
+
module.exports = { getStoreNameConvention };
|
package/utils/is.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const { nodeTypeIs } = require("./node-type-is");
|
|
2
|
+
const { namingOf } = require("./naming");
|
|
3
|
+
|
|
4
|
+
function isSomething({ isValidNaming, isTypeCorrect }) {
|
|
5
|
+
return ({ node, context }) => {
|
|
6
|
+
if (context.parserServices.hasFullTypeInformation) {
|
|
7
|
+
return isTypeCorrect({ node, context });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return isValidNaming({ name: node?.name ?? node?.id?.name, context });
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const isStore = isSomething({
|
|
15
|
+
isTypeCorrect: nodeTypeIs.store,
|
|
16
|
+
isValidNaming: namingOf.store.isValid,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const isEffect = isSomething({
|
|
20
|
+
isTypeCorrect: nodeTypeIs.effect,
|
|
21
|
+
isValidNaming: namingOf.effect.isValid,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const is = {
|
|
25
|
+
store: (opts) => isStore(opts),
|
|
26
|
+
effect: (opts) => isEffect(opts),
|
|
27
|
+
not: { store: (opts) => !isStore(opts), effect: (opts) => !isEffect(opts) },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
module.exports = { is };
|
package/utils/naming.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const { getStoreNameConvention } = require("./get-store-name-convention");
|
|
2
|
+
|
|
3
|
+
function isEffectNameValid({ name }) {
|
|
4
|
+
return Boolean(name?.endsWith("Fx"));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function isGateNameValid({ name }) {
|
|
8
|
+
const [firstChar] = name.split("");
|
|
9
|
+
|
|
10
|
+
return Boolean(firstChar?.toUpperCase() === firstChar);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isStoreNameValid({ name, context }) {
|
|
14
|
+
const storeNameConvention = getStoreNameConvention(context);
|
|
15
|
+
|
|
16
|
+
// validate edge case
|
|
17
|
+
if (name?.startsWith("$") && name?.endsWith("$")) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (storeNameConvention === "prefix" && name?.startsWith("$")) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (storeNameConvention === "postfix" && name?.endsWith("$")) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const namingOf = {
|
|
33
|
+
effect: {
|
|
34
|
+
isValid: (opts) => isEffectNameValid(opts),
|
|
35
|
+
isInvalid: (opts) => !isEffectNameValid(opts),
|
|
36
|
+
},
|
|
37
|
+
store: {
|
|
38
|
+
isValid: (opts) => isStoreNameValid(opts),
|
|
39
|
+
isInvalid: (opts) => !isStoreNameValid(opts),
|
|
40
|
+
},
|
|
41
|
+
gate: {
|
|
42
|
+
isValid: (opts) => isGateNameValid(opts),
|
|
43
|
+
isInvalid: (opts) => !isGateNameValid(opts),
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
module.exports = { namingOf };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
function hasType({ node, possibleTypes, context, from }) {
|
|
2
|
+
const checker = context.parserServices.program.getTypeChecker();
|
|
3
|
+
const originalNode = context.parserServices.esTreeNodeToTSNodeMap.get(node);
|
|
4
|
+
const type = checker.getTypeAtLocation(
|
|
5
|
+
originalNode?.initializer ?? originalNode
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
const symbol = type?.symbol ?? type?.aliasSymbol;
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
possibleTypes.includes(symbol?.escapedName) &&
|
|
12
|
+
Boolean(symbol?.parent?.escapedName?.includes(from))
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const nodeTypeIs = {
|
|
17
|
+
effect: (opts) =>
|
|
18
|
+
hasType({ ...opts, possibleTypes: ["Effect"], from: "effector" }),
|
|
19
|
+
store: (opts) =>
|
|
20
|
+
hasType({ ...opts, possibleTypes: ["Store"], from: "effector" }),
|
|
21
|
+
event: (opts) =>
|
|
22
|
+
hasType({ ...opts, possibleTypes: ["Event"], from: "effector" }),
|
|
23
|
+
unit: (opts) =>
|
|
24
|
+
hasType({
|
|
25
|
+
...opts,
|
|
26
|
+
possibleTypes: ["Effect", "Store", "Event"],
|
|
27
|
+
from: "effector",
|
|
28
|
+
}),
|
|
29
|
+
gate: (opts) =>
|
|
30
|
+
hasType({ ...opts, possibleTypes: ["Gate"], from: "effector-react" }),
|
|
31
|
+
not: {
|
|
32
|
+
effect: (opts) =>
|
|
33
|
+
!hasType({
|
|
34
|
+
...opts,
|
|
35
|
+
possibleTypes: ["Effect"],
|
|
36
|
+
from: "effector",
|
|
37
|
+
}),
|
|
38
|
+
store: (opts) =>
|
|
39
|
+
!hasType({ ...opts, possibleTypes: ["Store"], from: "effector" }),
|
|
40
|
+
event: (opts) =>
|
|
41
|
+
!hasType({ ...opts, possibleTypes: ["Event"], from: "effector" }),
|
|
42
|
+
unit: (opts) =>
|
|
43
|
+
!hasType({
|
|
44
|
+
...opts,
|
|
45
|
+
possibleTypes: ["Effect", "Store", "Event"],
|
|
46
|
+
from: "effector",
|
|
47
|
+
}),
|
|
48
|
+
gate: (opts) =>
|
|
49
|
+
!hasType({ ...opts, possibleTypes: ["Gate"], from: "effector-react" }),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
nodeTypeIs,
|
|
55
|
+
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
const { getStoreNameConvention } = require("./get-store-name-convention");
|
|
2
2
|
|
|
3
3
|
function validateStoreNameConvention(context) {
|
|
4
|
-
|
|
4
|
+
const storeNameConvention = getStoreNameConvention(context);
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
if (storeNameConvention !== "prefix" && storeNameConvention !== "postfix") {
|
|
7
|
+
throw new Error(
|
|
8
|
+
"Invalid Configuration of effector-plugin-eslint/enforce-store-naming-convention. The value should be equal to prefix or postfix."
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
module.exports = { validateStoreNameConvention };
|
|
13
|
+
module.exports = { validateStoreNameConvention };
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
function extractImportedFromEffector(importedFromEffector, node) {
|
|
2
|
-
if (node.source.value === "effector") {
|
|
3
|
-
for (const s of node.specifiers) {
|
|
4
|
-
importedFromEffector.set(s.imported.name, s.local.name);
|
|
5
|
-
}
|
|
6
|
-
}
|
|
7
|
-
}
|
|
8
|
-
module.exports = { extractImportedFromEffector };
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
const { getStoreNameConvention } = require("./get-store-name-convention");
|
|
2
|
-
|
|
3
|
-
function isStoreNameValid(storeName, context) {
|
|
4
|
-
const storeNameConvention = getStoreNameConvention(context);
|
|
5
|
-
|
|
6
|
-
// validate edge case
|
|
7
|
-
if (storeName?.startsWith("$") && storeName?.endsWith("$")) {
|
|
8
|
-
return false
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
if (storeNameConvention === "prefix" && storeName?.startsWith("$")) {
|
|
12
|
-
return true;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
if (storeNameConvention === "postfix" && storeName?.endsWith("$")) {
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
module.exports = { isStoreNameValid };
|