eslint-plugin-effector 0.1.2 → 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.
Files changed (42) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +5 -2
  4. package/config/recommended.js +3 -0
  5. package/index.js +4 -1
  6. package/package.json +9 -3
  7. package/rules/enforce-effect-naming-convention/enforce-effect-naming-convention.js +1 -1
  8. package/rules/enforce-effect-naming-convention/enforce-effect-naming-convention.md +9 -1
  9. package/rules/enforce-store-naming-convention/enforce-store-naming-convention.js +44 -21
  10. package/rules/enforce-store-naming-convention/enforce-store-naming-convention.md +43 -2
  11. package/rules/no-ambiguity-target/no-ambiguity-target.js +71 -0
  12. package/rules/no-ambiguity-target/no-ambiguity-target.md +12 -0
  13. package/rules/no-getState/no-getState.js +6 -13
  14. package/rules/no-getState/no-getState.md +18 -1
  15. package/rules/no-unnecessary-duplication/no-unnecessary-duplication.js +7 -2
  16. package/rules/no-unnecessary-duplication/no-unnecessary-duplication.md +5 -5
  17. package/rules/no-useless-methods/no-useless-methods.js +82 -0
  18. package/rules/no-useless-methods/no-useless-methods.md +14 -0
  19. package/rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping.js +95 -0
  20. package/rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping.md +27 -0
  21. package/utils/are-nodes-same-in-text.js +22 -0
  22. package/utils/get-corrected-store-name.js +23 -0
  23. package/utils/get-store-name-convention.js +6 -0
  24. package/utils/is-store-name-valid.js +22 -0
  25. package/utils/traverse-nested-object-node.js +9 -0
  26. package/utils/traverse-parent-by-type.js +13 -0
  27. package/utils/validate-store-name-convention.js +13 -0
  28. package/coverage/lcov-report/base.css +0 -224
  29. package/coverage/lcov-report/block-navigation.js +0 -79
  30. package/coverage/lcov-report/enforce-effect-naming-convention/enforce-effect-naming-convention.js.html +0 -482
  31. package/coverage/lcov-report/enforce-effect-naming-convention/index.html +0 -111
  32. package/coverage/lcov-report/enforce-store-naming-convention/enforce-store-naming-convention.js.html +0 -560
  33. package/coverage/lcov-report/enforce-store-naming-convention/index.html +0 -111
  34. package/coverage/lcov-report/favicon.png +0 -0
  35. package/coverage/lcov-report/index.html +0 -141
  36. package/coverage/lcov-report/no-getState/index.html +0 -111
  37. package/coverage/lcov-report/no-getState/no-getState.js.html +0 -326
  38. package/coverage/lcov-report/prettify.css +0 -1
  39. package/coverage/lcov-report/prettify.js +0 -2
  40. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  41. package/coverage/lcov-report/sorter.js +0 -170
  42. package/coverage/lcov.info +0 -256
@@ -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
+ ```
@@ -0,0 +1,95 @@
1
+ const {
2
+ extractImportedFromEffector,
3
+ } = require("../../utils/extract-imported-from-effector");
4
+ const {
5
+ traverseNestedObjectNode,
6
+ } = require("../../utils/traverse-nested-object-node");
7
+
8
+ module.exports = {
9
+ meta: {
10
+ type: "problem",
11
+ docs: {
12
+ description: "Prefer `sample` over `forward` with `.map`/`.prepend`",
13
+ category: "Quality",
14
+ recommended: true,
15
+ },
16
+ messages: {
17
+ overMap:
18
+ "Instead of `forward` with `{{ eventName }}.map` you can use `sample`",
19
+ overPrepend:
20
+ "Instead of `forward` with `{{ eventName }}.prepend` you can use `sample`",
21
+ },
22
+ schema: [],
23
+ },
24
+ create(context) {
25
+ const importedFromEffector = new Map();
26
+
27
+ return {
28
+ ImportDeclaration(node) {
29
+ extractImportedFromEffector(importedFromEffector, node);
30
+ },
31
+ CallExpression(node) {
32
+ const localMethod = importedFromEffector.get("forward");
33
+ if (!localMethod) {
34
+ return;
35
+ }
36
+
37
+ const isEffectorMethod = node?.callee?.name === localMethod;
38
+ if (!isEffectorMethod) {
39
+ return;
40
+ }
41
+
42
+ const forwardConfig = {
43
+ from: node.arguments?.[0]?.properties.find(
44
+ (n) => n.key?.name === "from"
45
+ ),
46
+ to: node.arguments?.[0]?.properties.find((n) => n.key?.name === "to"),
47
+ };
48
+
49
+ if (!forwardConfig.from || !forwardConfig.to) {
50
+ return;
51
+ }
52
+
53
+ function checkForMapping({ paramNode, methodName, messageId }) {
54
+ if (paramNode.value?.type !== "CallExpression") {
55
+ return;
56
+ }
57
+
58
+ if (paramNode.value?.callee?.property?.name !== methodName) {
59
+ return;
60
+ }
61
+
62
+ const eventNode = traverseNestedObjectNode(
63
+ paramNode.value?.callee?.object
64
+ );
65
+ const eventName = eventNode?.name;
66
+
67
+ if (!eventName) {
68
+ return;
69
+ }
70
+
71
+ // console.log(eventName);
72
+
73
+ context.report({
74
+ node,
75
+ messageId,
76
+ data: {
77
+ eventName,
78
+ },
79
+ });
80
+ }
81
+
82
+ checkForMapping({
83
+ paramNode: forwardConfig.from,
84
+ methodName: "map",
85
+ messageId: "overMap",
86
+ });
87
+ checkForMapping({
88
+ paramNode: forwardConfig.to,
89
+ methodName: "prepend",
90
+ messageId: "overPrepend",
91
+ });
92
+ },
93
+ };
94
+ },
95
+ };
@@ -0,0 +1,27 @@
1
+ # effector/prefer-sample-over-forward-with-mapping
2
+
3
+ Prefer `sample` over `forward` with `.map`/`.prepend`.
4
+
5
+ ```js
6
+ const eventOne = createEvent();
7
+ const eventTwo = createEvent();
8
+
9
+ // 👎 looks weird
10
+ forward({
11
+ from: eventOne.map((items) => items.length),
12
+ to: eventTwo,
13
+ });
14
+
15
+ // 👎 weird too
16
+ forward({
17
+ from: eventOne,
18
+ to: eventTwo.prepend((items) => items.length),
19
+ });
20
+
21
+ // 👍 better
22
+ sample({
23
+ source: eventOne,
24
+ fn: (items) => items.length,
25
+ target: eventTwo,
26
+ });
27
+ ```
@@ -0,0 +1,22 @@
1
+ const prettier = require("prettier");
2
+
3
+ function areNodesSameInText({ context, nodes }) {
4
+ const texts = nodes.map((node) => {
5
+ let sourceText = context.getSourceCode().getText(node);
6
+
7
+ const shouldBeWrapped =
8
+ sourceText.startsWith("{") && sourceText.endsWith("}");
9
+
10
+ if (shouldBeWrapped) {
11
+ sourceText = `(${sourceText})`;
12
+ }
13
+
14
+ return prettier.format(sourceText, {
15
+ parser: "babel-ts",
16
+ });
17
+ });
18
+
19
+ return texts.every((text) => text === texts[0]);
20
+ }
21
+
22
+ module.exports = { areNodesSameInText };
@@ -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,9 @@
1
+ function traverseNestedObjectNode(node) {
2
+ if (node.type === "MemberExpression") {
3
+ return traverseNestedObjectNode(node.property);
4
+ }
5
+
6
+ return node;
7
+ }
8
+
9
+ module.exports = { traverseNestedObjectNode };
@@ -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 };
@@ -1,224 +0,0 @@
1
- body, html {
2
- margin:0; padding: 0;
3
- height: 100%;
4
- }
5
- body {
6
- font-family: Helvetica Neue, Helvetica, Arial;
7
- font-size: 14px;
8
- color:#333;
9
- }
10
- .small { font-size: 12px; }
11
- *, *:after, *:before {
12
- -webkit-box-sizing:border-box;
13
- -moz-box-sizing:border-box;
14
- box-sizing:border-box;
15
- }
16
- h1 { font-size: 20px; margin: 0;}
17
- h2 { font-size: 14px; }
18
- pre {
19
- font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
20
- margin: 0;
21
- padding: 0;
22
- -moz-tab-size: 2;
23
- -o-tab-size: 2;
24
- tab-size: 2;
25
- }
26
- a { color:#0074D9; text-decoration:none; }
27
- a:hover { text-decoration:underline; }
28
- .strong { font-weight: bold; }
29
- .space-top1 { padding: 10px 0 0 0; }
30
- .pad2y { padding: 20px 0; }
31
- .pad1y { padding: 10px 0; }
32
- .pad2x { padding: 0 20px; }
33
- .pad2 { padding: 20px; }
34
- .pad1 { padding: 10px; }
35
- .space-left2 { padding-left:55px; }
36
- .space-right2 { padding-right:20px; }
37
- .center { text-align:center; }
38
- .clearfix { display:block; }
39
- .clearfix:after {
40
- content:'';
41
- display:block;
42
- height:0;
43
- clear:both;
44
- visibility:hidden;
45
- }
46
- .fl { float: left; }
47
- @media only screen and (max-width:640px) {
48
- .col3 { width:100%; max-width:100%; }
49
- .hide-mobile { display:none!important; }
50
- }
51
-
52
- .quiet {
53
- color: #7f7f7f;
54
- color: rgba(0,0,0,0.5);
55
- }
56
- .quiet a { opacity: 0.7; }
57
-
58
- .fraction {
59
- font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
60
- font-size: 10px;
61
- color: #555;
62
- background: #E8E8E8;
63
- padding: 4px 5px;
64
- border-radius: 3px;
65
- vertical-align: middle;
66
- }
67
-
68
- div.path a:link, div.path a:visited { color: #333; }
69
- table.coverage {
70
- border-collapse: collapse;
71
- margin: 10px 0 0 0;
72
- padding: 0;
73
- }
74
-
75
- table.coverage td {
76
- margin: 0;
77
- padding: 0;
78
- vertical-align: top;
79
- }
80
- table.coverage td.line-count {
81
- text-align: right;
82
- padding: 0 5px 0 20px;
83
- }
84
- table.coverage td.line-coverage {
85
- text-align: right;
86
- padding-right: 10px;
87
- min-width:20px;
88
- }
89
-
90
- table.coverage td span.cline-any {
91
- display: inline-block;
92
- padding: 0 5px;
93
- width: 100%;
94
- }
95
- .missing-if-branch {
96
- display: inline-block;
97
- margin-right: 5px;
98
- border-radius: 3px;
99
- position: relative;
100
- padding: 0 4px;
101
- background: #333;
102
- color: yellow;
103
- }
104
-
105
- .skip-if-branch {
106
- display: none;
107
- margin-right: 10px;
108
- position: relative;
109
- padding: 0 4px;
110
- background: #ccc;
111
- color: white;
112
- }
113
- .missing-if-branch .typ, .skip-if-branch .typ {
114
- color: inherit !important;
115
- }
116
- .coverage-summary {
117
- border-collapse: collapse;
118
- width: 100%;
119
- }
120
- .coverage-summary tr { border-bottom: 1px solid #bbb; }
121
- .keyline-all { border: 1px solid #ddd; }
122
- .coverage-summary td, .coverage-summary th { padding: 10px; }
123
- .coverage-summary tbody { border: 1px solid #bbb; }
124
- .coverage-summary td { border-right: 1px solid #bbb; }
125
- .coverage-summary td:last-child { border-right: none; }
126
- .coverage-summary th {
127
- text-align: left;
128
- font-weight: normal;
129
- white-space: nowrap;
130
- }
131
- .coverage-summary th.file { border-right: none !important; }
132
- .coverage-summary th.pct { }
133
- .coverage-summary th.pic,
134
- .coverage-summary th.abs,
135
- .coverage-summary td.pct,
136
- .coverage-summary td.abs { text-align: right; }
137
- .coverage-summary td.file { white-space: nowrap; }
138
- .coverage-summary td.pic { min-width: 120px !important; }
139
- .coverage-summary tfoot td { }
140
-
141
- .coverage-summary .sorter {
142
- height: 10px;
143
- width: 7px;
144
- display: inline-block;
145
- margin-left: 0.5em;
146
- background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
147
- }
148
- .coverage-summary .sorted .sorter {
149
- background-position: 0 -20px;
150
- }
151
- .coverage-summary .sorted-desc .sorter {
152
- background-position: 0 -10px;
153
- }
154
- .status-line { height: 10px; }
155
- /* yellow */
156
- .cbranch-no { background: yellow !important; color: #111; }
157
- /* dark red */
158
- .red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
159
- .low .chart { border:1px solid #C21F39 }
160
- .highlighted,
161
- .highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{
162
- background: #C21F39 !important;
163
- }
164
- /* medium red */
165
- .cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
166
- /* light red */
167
- .low, .cline-no { background:#FCE1E5 }
168
- /* light green */
169
- .high, .cline-yes { background:rgb(230,245,208) }
170
- /* medium green */
171
- .cstat-yes { background:rgb(161,215,106) }
172
- /* dark green */
173
- .status-line.high, .high .cover-fill { background:rgb(77,146,33) }
174
- .high .chart { border:1px solid rgb(77,146,33) }
175
- /* dark yellow (gold) */
176
- .status-line.medium, .medium .cover-fill { background: #f9cd0b; }
177
- .medium .chart { border:1px solid #f9cd0b; }
178
- /* light yellow */
179
- .medium { background: #fff4c2; }
180
-
181
- .cstat-skip { background: #ddd; color: #111; }
182
- .fstat-skip { background: #ddd; color: #111 !important; }
183
- .cbranch-skip { background: #ddd !important; color: #111; }
184
-
185
- span.cline-neutral { background: #eaeaea; }
186
-
187
- .coverage-summary td.empty {
188
- opacity: .5;
189
- padding-top: 4px;
190
- padding-bottom: 4px;
191
- line-height: 1;
192
- color: #888;
193
- }
194
-
195
- .cover-fill, .cover-empty {
196
- display:inline-block;
197
- height: 12px;
198
- }
199
- .chart {
200
- line-height: 0;
201
- }
202
- .cover-empty {
203
- background: white;
204
- }
205
- .cover-full {
206
- border-right: none !important;
207
- }
208
- pre.prettyprint {
209
- border: none !important;
210
- padding: 0 !important;
211
- margin: 0 !important;
212
- }
213
- .com { color: #999 !important; }
214
- .ignore-none { color: #999; font-weight: normal; }
215
-
216
- .wrapper {
217
- min-height: 100%;
218
- height: auto !important;
219
- height: 100%;
220
- margin: 0 auto -48px;
221
- }
222
- .footer, .push {
223
- height: 48px;
224
- }
@@ -1,79 +0,0 @@
1
- /* eslint-disable */
2
- var jumpToCode = (function init() {
3
- // Classes of code we would like to highlight in the file view
4
- var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no'];
5
-
6
- // Elements to highlight in the file listing view
7
- var fileListingElements = ['td.pct.low'];
8
-
9
- // We don't want to select elements that are direct descendants of another match
10
- var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > `
11
-
12
- // Selecter that finds elements on the page to which we can jump
13
- var selector =
14
- fileListingElements.join(', ') +
15
- ', ' +
16
- notSelector +
17
- missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b`
18
-
19
- // The NodeList of matching elements
20
- var missingCoverageElements = document.querySelectorAll(selector);
21
-
22
- var currentIndex;
23
-
24
- function toggleClass(index) {
25
- missingCoverageElements
26
- .item(currentIndex)
27
- .classList.remove('highlighted');
28
- missingCoverageElements.item(index).classList.add('highlighted');
29
- }
30
-
31
- function makeCurrent(index) {
32
- toggleClass(index);
33
- currentIndex = index;
34
- missingCoverageElements.item(index).scrollIntoView({
35
- behavior: 'smooth',
36
- block: 'center',
37
- inline: 'center'
38
- });
39
- }
40
-
41
- function goToPrevious() {
42
- var nextIndex = 0;
43
- if (typeof currentIndex !== 'number' || currentIndex === 0) {
44
- nextIndex = missingCoverageElements.length - 1;
45
- } else if (missingCoverageElements.length > 1) {
46
- nextIndex = currentIndex - 1;
47
- }
48
-
49
- makeCurrent(nextIndex);
50
- }
51
-
52
- function goToNext() {
53
- var nextIndex = 0;
54
-
55
- if (
56
- typeof currentIndex === 'number' &&
57
- currentIndex < missingCoverageElements.length - 1
58
- ) {
59
- nextIndex = currentIndex + 1;
60
- }
61
-
62
- makeCurrent(nextIndex);
63
- }
64
-
65
- return function jump(event) {
66
- switch (event.which) {
67
- case 78: // n
68
- case 74: // j
69
- goToNext();
70
- break;
71
- case 66: // b
72
- case 75: // k
73
- case 80: // p
74
- goToPrevious();
75
- break;
76
- }
77
- };
78
- })();
79
- window.addEventListener('keydown', jumpToCode);