eslint-plugin-effector 0.2.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.3.0
4
+
5
+ - Add new rule: `no-useless-methods` ([PR #41](https://github.com/effector/eslint-plugin/pull/41))
6
+ - Add new rule: `no-ambiguity-target` ([PR #42](https://github.com/effector/eslint-plugin/pull/42))
7
+ - Add possibility to configure store's naming convention — suffix of prefix ([PR #37](https://github.com/effector/eslint-plugin/pull/37) by @ilyaryabchinski)
8
+
3
9
  ## v0.2.0
4
10
 
5
11
  - Add tests against Effector 22
package/README.md CHANGED
@@ -45,4 +45,6 @@ To configure individual rules:
45
45
  - [effector/enforce-effect-naming-convention](/rules/enforce-effect-naming-convention/enforce-effect-naming-convention.md)
46
46
  - [effector/no-getState](/rules/no-getState/no-getState.md)
47
47
  - [effector/no-unnecessary-duplication](/rules/no-unnecessary-duplication/no-unnecessary-duplication.md)
48
+ - [effector/no-useless-methods](/rules/no-useless-methods/no-useless-methods.md)
49
+ - [effector/no-ambiguity-target](/rules/no-ambiguity-target/no-ambiguity-target.md)
48
50
  - [effector/prefer-sample-over-forward-with-mapping](/rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping.md)
@@ -3,7 +3,9 @@ module.exports = {
3
3
  "effector/enforce-store-naming-convention": "error",
4
4
  "effector/enforce-effect-naming-convention": "error",
5
5
  "effector/no-getState": "error",
6
+ "effector/no-useless-methods": "error",
6
7
  "effector/no-unnecessary-duplication": "warn",
7
8
  "effector/prefer-sample-over-forward-with-mapping": "warn",
9
+ "effector/no-ambiguity-target": "warn",
8
10
  },
9
11
  };
package/index.js CHANGED
@@ -5,8 +5,10 @@ module.exports = {
5
5
  "no-getState": require("./rules/no-getState/no-getState"),
6
6
  "no-unnecessary-duplication": require("./rules/no-unnecessary-duplication/no-unnecessary-duplication"),
7
7
  "prefer-sample-over-forward-with-mapping": require("./rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping"),
8
+ "no-useless-methods": require("./rules/no-useless-methods/no-useless-methods"),
9
+ "no-ambiguity-target": require("./rules/no-ambiguity-target/no-ambiguity-target"),
8
10
  },
9
11
  configs: {
10
12
  recommended: require("./config/recommended"),
11
- },
13
+ }
12
14
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-effector",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Enforcing best practices for Effector",
5
5
  "main": "index.js",
6
6
  "author": "Igor Kamyshev <igor@kamyshev.me>",
@@ -1,25 +1,32 @@
1
1
  const {
2
2
  extractImportedFromEffector,
3
3
  } = require("../../utils/extract-imported-from-effector");
4
+ const { isStoreNameValid } = require("../../utils/is-store-name-valid");
5
+ const { validateStoreNameConvention } = require("../../utils/validate-store-name-convention");
6
+ const { getStoreNameConvention } = require("../../utils/get-store-name-convention");
7
+ const { getCorrectedStoreName } = require("../../utils/get-corrected-store-name");
4
8
 
5
9
  module.exports = {
6
10
  meta: {
7
11
  type: "problem",
8
12
  docs: {
9
13
  description:
10
- "Enforce $ as a prefix for any store created by Effector methods",
14
+ "Enforce $ as a prefix or postfix for any store created by Effector methods",
11
15
  category: "Naming",
12
16
  recommended: true,
13
17
  },
14
18
  messages: {
15
19
  invalidName:
16
- 'Store "{{ storeName }}" should be named with prefix, rename it to "${{ storeName }}"',
17
- renameStore: 'Rename "{{ storeName }}" to "${{ storeName }}"',
20
+ 'Store "{{ storeName }}" should be named with {{ storeNameConvention }}, rename it to "{{ correctedStoreName }}"',
21
+ renameStore: 'Rename "{{ storeName }}" to "{{ correctedStoreName }}"',
18
22
  },
19
23
  schema: [],
20
24
  },
21
25
  create(context) {
22
- const parserServices = context.parserServices;
26
+ const { parserServices } = context;
27
+
28
+ validateStoreNameConvention(context);
29
+
23
30
  // TypeScript-way
24
31
  if (parserServices.hasFullTypeInformation) {
25
32
  return {
@@ -29,8 +36,8 @@ module.exports = {
29
36
  const type = checker.getTypeAtLocation(originalNode.initializer);
30
37
 
31
38
  const isEffectorStore =
32
- type?.symbol?.escapedName === "Store" &&
33
- type?.symbol?.parent?.escapedName?.includes("effector");
39
+ type?.symbol?.escapedName === "Store" &&
40
+ type?.symbol?.parent?.escapedName?.includes("effector");
34
41
 
35
42
  if (!isEffectorStore) {
36
43
  return;
@@ -38,11 +45,15 @@ module.exports = {
38
45
 
39
46
  const storeName = node.id.name;
40
47
 
41
- if (storeName?.startsWith("$")) {
48
+ if (isStoreNameValid(storeName, context)) {
42
49
  return;
43
50
  }
44
51
 
45
- reportStoreNameConventionViolation({ context, node, storeName });
52
+ reportStoreNameConventionViolation({
53
+ context,
54
+ node,
55
+ storeName
56
+ });
46
57
  },
47
58
  };
48
59
  }
@@ -68,47 +79,50 @@ module.exports = {
68
79
  }
69
80
 
70
81
  const resultSavedInVariable =
71
- node.parent.type === "VariableDeclarator";
82
+ node.parent.type === "VariableDeclarator";
72
83
  if (!resultSavedInVariable) {
73
84
  continue;
74
85
  }
75
86
 
76
87
  const storeName = node.parent.id.name;
77
- if (storeName.startsWith("$")) {
88
+
89
+ if (isStoreNameValid(storeName, context)) {
78
90
  continue;
79
91
  }
80
92
 
81
93
  reportStoreNameConventionViolation({
82
94
  context,
83
95
  node: node.parent,
84
- storeName,
96
+ storeName
85
97
  });
86
98
  return;
87
99
  }
88
100
 
89
101
  // Store creation with .map
90
102
  if (node.callee?.property?.name === "map") {
91
- const objectIsEffectorStore =
92
- node.callee?.object?.name?.startsWith?.("$");
93
- if (!objectIsEffectorStore) {
103
+ const storeNameCreatedFromMap = node.callee?.object?.name;
104
+
105
+ if (!isStoreNameValid(storeNameCreatedFromMap, context)) {
94
106
  return;
95
107
  }
96
108
 
97
109
  const resultSavedInVariable =
98
- node.parent.type === "VariableDeclarator";
110
+ node.parent.type === "VariableDeclarator";
99
111
  if (!resultSavedInVariable) {
100
112
  return;
101
113
  }
102
114
 
103
115
  const storeName = node.parent.id.name;
104
- if (storeName.startsWith("$")) {
116
+
117
+ if (isStoreNameValid(storeName, context)) {
105
118
  return;
106
119
  }
107
120
 
121
+
108
122
  reportStoreNameConventionViolation({
109
123
  context,
110
124
  node: node.parent,
111
- storeName,
125
+ storeName
112
126
  });
113
127
  return;
114
128
  }
@@ -116,23 +130,24 @@ module.exports = {
116
130
  // Store creation in domain
117
131
  const STORE_IN_DOMAIN_CREATION_METHODS = ["createStore", "store"];
118
132
  if (
119
- STORE_IN_DOMAIN_CREATION_METHODS.includes(node.callee?.property?.name)
133
+ STORE_IN_DOMAIN_CREATION_METHODS.includes(node.callee?.property?.name)
120
134
  ) {
121
135
  const resultSavedInVariable =
122
- node.parent.type === "VariableDeclarator";
136
+ node.parent.type === "VariableDeclarator";
123
137
  if (!resultSavedInVariable) {
124
138
  return;
125
139
  }
126
140
 
127
141
  const storeName = node.parent.id.name;
128
- if (storeName.startsWith("$")) {
142
+
143
+ if (isStoreNameValid(storeName, context)) {
129
144
  return;
130
145
  }
131
146
 
132
147
  reportStoreNameConventionViolation({
133
148
  context,
134
149
  node: node.parent,
135
- storeName,
150
+ storeName
136
151
  });
137
152
  return;
138
153
  }
@@ -142,20 +157,27 @@ module.exports = {
142
157
  };
143
158
 
144
159
  function reportStoreNameConventionViolation({ context, node, storeName }) {
160
+
161
+ const storeNameConvention = getStoreNameConvention(context);
162
+ const correctedStoreName = getCorrectedStoreName(storeName, context);
163
+
145
164
  context.report({
146
165
  node,
147
166
  messageId: "invalidName",
148
167
  data: {
149
168
  storeName,
169
+ correctedStoreName,
170
+ storeNameConvention
150
171
  },
151
172
  suggest: [
152
173
  {
153
174
  messageId: "renameStore",
154
175
  data: { storeName },
155
176
  fix(fixer) {
156
- return fixer.insertTextBeforeRange(node.range, "$");
177
+ return fixer.replaceTextRange(node.id.range, correctedStoreName);
157
178
  },
158
179
  },
159
180
  ],
160
181
  });
161
182
  }
183
+
@@ -1,7 +1,17 @@
1
1
  # effector/enforce-store-naming-convention
2
2
 
3
- Enforcing naming conventions helps keep the codebase consistent, and reduces overhead when thinking about how to name a variable with store. Your stores should be distingueshed by a prefix $. For example, `$name` is a store,`name` is not.
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
+ ## Prefix convention
6
+ When configured as:
7
+ ```js
8
+ module.exports = {
9
+ rules: {
10
+ "effector/enforce-store-naming-convention": "error",
11
+ },
12
+ };
13
+ ```
14
+ Prefix convention will be enforced:
5
15
  ```ts
6
16
  // 👍 nice name
7
17
  const $name = createStore(null);
@@ -9,3 +19,26 @@ const $name = createStore(null);
9
19
  // 👎 bad name
10
20
  const name = createStrore(null);
11
21
  ```
22
+ ## Postfix convention
23
+
24
+ When configured as:
25
+ ```js
26
+ module.exports = {
27
+ rules: {
28
+ "effector/enforce-store-naming-convention": "error",
29
+ },
30
+ settings: {
31
+ effector: {
32
+ storeNameConvention: "postfix"
33
+ }
34
+ }
35
+ };
36
+ ```
37
+ Postfix convention will be enforced:
38
+ ```ts
39
+ // 👍 nice name
40
+ const name$ = createStore(null);
41
+
42
+ // 👎 bad name
43
+ const name = createStrore(null);
44
+ ```
@@ -0,0 +1,71 @@
1
+ const {
2
+ extractImportedFromEffector,
3
+ } = require("../../utils/extract-imported-from-effector");
4
+ const { traverseParentByType } = require("../../utils/traverse-parent-by-type");
5
+
6
+ module.exports = {
7
+ meta: {
8
+ type: "problem",
9
+ docs: {
10
+ description: "Forbids ambiguity targets in `sample` and `guard`",
11
+ category: "Quality",
12
+ recommended: true,
13
+ },
14
+ messages: {
15
+ ambiguityTarget:
16
+ "Method `{{ methodName }}` returns `target` and assigns the result to a variable. Consider removing one of them.",
17
+ },
18
+ schema: [],
19
+ },
20
+ create(context) {
21
+ const importedFromEffector = new Map();
22
+
23
+ return {
24
+ ImportDeclaration(node) {
25
+ extractImportedFromEffector(importedFromEffector, node);
26
+ },
27
+ CallExpression(node) {
28
+ const POSSIBLE_USELESS_METHODS = ["sample", "guard"];
29
+ for (const method of POSSIBLE_USELESS_METHODS) {
30
+ const localMethod = importedFromEffector.get(method);
31
+ if (!localMethod) {
32
+ continue;
33
+ }
34
+
35
+ const isEffectorMethod = node?.callee?.name === localMethod;
36
+ if (!isEffectorMethod) {
37
+ continue;
38
+ }
39
+
40
+ const configHasTarget = node?.arguments?.[0]?.properties?.some(
41
+ (prop) => prop?.key.name === "target"
42
+ );
43
+ if (!configHasTarget) {
44
+ continue;
45
+ }
46
+
47
+ const resultAssignedInVariable = traverseParentByType(
48
+ node,
49
+ "VariableDeclarator"
50
+ );
51
+ const resultPartOfChain = traverseParentByType(
52
+ node,
53
+ "ObjectExpression"
54
+ );
55
+
56
+ if (resultAssignedInVariable || resultPartOfChain) {
57
+ context.report({
58
+ node,
59
+ messageId: "ambiguityTarget",
60
+ data: {
61
+ methodName: node?.callee?.name,
62
+ },
63
+ });
64
+
65
+ continue;
66
+ }
67
+ }
68
+ },
69
+ };
70
+ },
71
+ };
@@ -0,0 +1,12 @@
1
+ # effector/no-ambiguity-target
2
+
3
+ Call of `gaurd`/`sample` with `target` and variable assignment is ambiguity. One of them should be omitted from source code.
4
+
5
+ ```ts
6
+ // 👎 should be rewritten
7
+ const result = guard({ clock: trigger, filter: Boolean, target });
8
+
9
+ // 👍 makes sense
10
+ guard({ clock: trigger, filter: Boolean, target });
11
+ const result = target;
12
+ ```
@@ -1,6 +1,7 @@
1
1
  const {
2
2
  traverseNestedObjectNode,
3
3
  } = require("../../utils/traverse-nested-object-node");
4
+ const { isStoreNameValid } = require("../../utils/is-store-name-valid");
4
5
 
5
6
  module.exports = {
6
7
  meta: {
@@ -17,7 +18,7 @@ module.exports = {
17
18
  schema: [],
18
19
  },
19
20
  create(context) {
20
- const parserServices = context.parserServices;
21
+ const { parserServices } = context;
21
22
 
22
23
  return {
23
24
  CallExpression(node) {
@@ -51,7 +52,7 @@ module.exports = {
51
52
  }
52
53
  // JavaScript-way
53
54
  else {
54
- const isEffectorStore = objectName.startsWith("$");
55
+ const isEffectorStore = isStoreNameValid(objectName, context);
55
56
  if (!isEffectorStore) {
56
57
  return;
57
58
  }
@@ -0,0 +1,82 @@
1
+ const {
2
+ extractImportedFromEffector,
3
+ } = require("../../utils/extract-imported-from-effector");
4
+ const { traverseParentByType } = require("../../utils/traverse-parent-by-type");
5
+
6
+ module.exports = {
7
+ meta: {
8
+ type: "problem",
9
+ docs: {
10
+ description: "Forbids useless calls of `sample` and `guard`",
11
+ category: "Quality",
12
+ recommended: true,
13
+ },
14
+ messages: {
15
+ uselessMethod:
16
+ "Method `{{ methodName }}` does nothing in this case. You should assign the result to variable or pass `target` to it.",
17
+ },
18
+ schema: [],
19
+ },
20
+ create(context) {
21
+ const importedFromEffector = new Map();
22
+
23
+ return {
24
+ ImportDeclaration(node) {
25
+ extractImportedFromEffector(importedFromEffector, node);
26
+ },
27
+ CallExpression(node) {
28
+ const POSSIBLE_USELESS_METHODS = ["sample", "guard"];
29
+ for (const method of POSSIBLE_USELESS_METHODS) {
30
+ const localMethod = importedFromEffector.get(method);
31
+ if (!localMethod) {
32
+ continue;
33
+ }
34
+
35
+ const isEffectorMethod = node?.callee?.name === localMethod;
36
+ if (!isEffectorMethod) {
37
+ continue;
38
+ }
39
+
40
+ const resultAssignedInVariable = traverseParentByType(
41
+ node,
42
+ "VariableDeclarator"
43
+ );
44
+ if (resultAssignedInVariable) {
45
+ continue;
46
+ }
47
+
48
+ const resultReturnedFromFactory = traverseParentByType(
49
+ node,
50
+ "ReturnStatement"
51
+ );
52
+ if (resultReturnedFromFactory) {
53
+ continue;
54
+ }
55
+
56
+ const resultPartOfChain = traverseParentByType(
57
+ node,
58
+ "ObjectExpression"
59
+ );
60
+ if (resultPartOfChain) {
61
+ continue;
62
+ }
63
+
64
+ const configHasTarget = node?.arguments?.[0]?.properties?.some(
65
+ (prop) => prop?.key.name === "target"
66
+ );
67
+ if (configHasTarget) {
68
+ continue;
69
+ }
70
+
71
+ context.report({
72
+ node,
73
+ messageId: "uselessMethod",
74
+ data: {
75
+ methodName: node?.callee?.name,
76
+ },
77
+ });
78
+ }
79
+ },
80
+ };
81
+ },
82
+ };
@@ -0,0 +1,14 @@
1
+ # effector/no-useless-methods
2
+
3
+ Call of `gaurd`/`sample` without `target` or variable assignment is useless. It can be omitted from source code.
4
+
5
+ ```ts
6
+ // 👎 can be omitted
7
+ guard({ clock: trigger, filter: Boolean });
8
+
9
+ // 👍 makes sense
10
+ const target1 = guard({ clock: trigger, filter: Boolean });
11
+
12
+ // 👍 make sense too
13
+ guard({ clock: trigger, filter: Boolean, target: target2 });
14
+ ```
@@ -8,20 +8,20 @@ const eventTwo = createEvent();
8
8
 
9
9
  // 👎 looks weird
10
10
  forward({
11
- from: eventOne.map((items) => item.length),
11
+ from: eventOne.map((items) => items.length),
12
12
  to: eventTwo,
13
13
  });
14
14
 
15
15
  // 👎 weird too
16
16
  forward({
17
17
  from: eventOne,
18
- to: eventTwo.prepend((items) => item.length),
18
+ to: eventTwo.prepend((items) => items.length),
19
19
  });
20
20
 
21
21
  // 👍 better
22
22
  sample({
23
23
  source: eventOne,
24
- fn: (items) => item.length,
24
+ fn: (items) => items.length,
25
25
  target: eventTwo,
26
26
  });
27
27
  ```
@@ -0,0 +1,23 @@
1
+ const { getStoreNameConvention } = require("./get-store-name-convention");
2
+
3
+ function getCorrectedStoreName(storeName, context) {
4
+ const storeNameConvention = getStoreNameConvention(context);
5
+
6
+ // handle edge case
7
+ if (storeName.startsWith("$") && storeName.endsWith("$")) {
8
+
9
+ if (storeNameConvention === "prefix") {
10
+ return `$${storeName.slice(0, -1)}`;
11
+ } else {
12
+ return `${storeName.slice(1)}$`;
13
+ }
14
+ }
15
+
16
+ const correctedStoreName = storeNameConvention === "prefix"
17
+ ? `$${storeName}`
18
+ : `${storeName}$`;
19
+
20
+ return correctedStoreName;
21
+ }
22
+
23
+ module.exports = { getCorrectedStoreName };
@@ -0,0 +1,6 @@
1
+ function getStoreNameConvention(context) {
2
+ // prefix convention is default
3
+ return context.settings.effector?.storeNameConvention || "prefix";
4
+ }
5
+
6
+ module.exports = { getStoreNameConvention };
@@ -0,0 +1,22 @@
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 };
@@ -0,0 +1,13 @@
1
+ function traverseParentByType(node, type) {
2
+ if (!node) {
3
+ return null;
4
+ }
5
+
6
+ if (node.type === type) {
7
+ return node;
8
+ }
9
+
10
+ return traverseParentByType(node.parent, type);
11
+ }
12
+
13
+ module.exports = { traverseParentByType };
@@ -0,0 +1,13 @@
1
+ const { getStoreNameConvention } = require("./get-store-name-convention");
2
+
3
+ function validateStoreNameConvention(context) {
4
+ const storeNameConvention = getStoreNameConvention(context);
5
+
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
+ }
12
+
13
+ module.exports = { validateStoreNameConvention };