eslint-plugin-effector 0.5.2 → 0.7.1

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 (39) hide show
  1. package/.nvmrc +1 -0
  2. package/README.md +22 -13
  3. package/config/future.js +7 -0
  4. package/config/react.js +1 -0
  5. package/config/recommended.js +1 -0
  6. package/index.js +5 -0
  7. package/package.json +2 -2
  8. package/rules/keep-options-order/config.js +3 -0
  9. package/rules/keep-options-order/keep-options-order.js +107 -0
  10. package/rules/mandatory-useEvent/mandatory-useEvent.js +63 -0
  11. package/rules/no-ambiguity-target/no-ambiguity-target.js +32 -35
  12. package/rules/no-duplicate-on/no-duplicate-on.js +10 -1
  13. package/rules/no-forward/no-forward.js +73 -0
  14. package/rules/no-guard/no-guard.js +78 -0
  15. package/rules/no-unnecessary-combination/no-unnecessary-combination.js +33 -41
  16. package/rules/no-unnecessary-duplication/no-unnecessary-duplication.js +35 -40
  17. package/rules/no-useless-methods/no-useless-methods.js +50 -53
  18. package/rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping.js +27 -15
  19. package/utils/builders.js +19 -0
  20. package/utils/extract-config.js +13 -0
  21. package/utils/extract-imported-from.js +2 -1
  22. package/utils/method.js +23 -0
  23. package/utils/node-type-is.js +16 -0
  24. package/utils/react.js +184 -0
  25. package/utils/read-example.js +38 -1
  26. package/utils/replace-by-sample.js +94 -0
  27. package/rules/enforce-effect-naming-convention/enforce-effect-naming-convention.md +0 -11
  28. package/rules/enforce-gate-naming-convention/enforce-gate-naming-convention.md +0 -11
  29. package/rules/enforce-store-naming-convention/enforce-store-naming-convention.md +0 -52
  30. package/rules/no-ambiguity-target/no-ambiguity-target.md +0 -12
  31. package/rules/no-duplicate-on/no-duplicate-on.md +0 -16
  32. package/rules/no-getState/no-getState.md +0 -20
  33. package/rules/no-unnecessary-combination/no-unnecessary-combination.md +0 -25
  34. package/rules/no-unnecessary-duplication/no-unnecessary-duplication.md +0 -32
  35. package/rules/no-useless-methods/no-useless-methods.md +0 -14
  36. package/rules/no-watch/no-watch.md +0 -42
  37. package/rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping.md +0 -27
  38. package/rules/strict-effect-handlers/strict-effect-handlers.md +0 -38
  39. package/rules/tsconfig.json +0 -10
@@ -1,6 +1,8 @@
1
1
  const { extractImportedFrom } = require("../../utils/extract-imported-from");
2
2
  const { areNodesSameInText } = require("../../utils/are-nodes-same-in-text");
3
3
  const { createLinkToRule } = require("../../utils/create-link-to-rule");
4
+ const { buildObjectInText } = require("../../utils/builders");
5
+ const { method } = require("../../utils/method");
4
6
 
5
7
  module.exports = {
6
8
  meta: {
@@ -32,45 +34,41 @@ module.exports = {
32
34
  });
33
35
  },
34
36
  CallExpression(node) {
35
- const METHODS_WITH_POSSIBLE_DUPLCATION = ["sample", "guard"];
36
- for (const method of METHODS_WITH_POSSIBLE_DUPLCATION) {
37
- const localMethod = importedFromEffector.get(method);
38
- if (!localMethod) {
39
- continue;
40
- }
41
-
42
- const isEffectorMethod = node?.callee?.name === localMethod;
43
- if (!isEffectorMethod) {
44
- continue;
45
- }
46
-
47
- const params = {
48
- source: node?.arguments?.[0]?.properties?.find(
49
- (n) => n.key.name === "source"
50
- ),
51
- clock: node?.arguments?.[0]?.properties?.find(
52
- (n) => n.key.name === "clock"
53
- ),
54
- };
55
- if (!params.source || !params.clock) {
56
- return;
57
- }
37
+ if (
38
+ method.isNot(["sample", "guard"], {
39
+ node,
40
+ importMap: importedFromEffector,
41
+ })
42
+ ) {
43
+ return;
44
+ }
58
45
 
59
- const sameSourceAndClock = areNodesSameInText({
60
- context,
61
- nodes: [params.source?.value, params.clock?.value],
62
- });
63
- if (!sameSourceAndClock) {
64
- return;
65
- }
46
+ const params = {
47
+ source: node?.arguments?.[0]?.properties?.find(
48
+ (n) => n.key.name === "source"
49
+ ),
50
+ clock: node?.arguments?.[0]?.properties?.find(
51
+ (n) => n.key.name === "clock"
52
+ ),
53
+ };
54
+ if (!params.source || !params.clock) {
55
+ return;
56
+ }
66
57
 
67
- reportUnnecessaryDuplication({
68
- context,
69
- node,
70
- params,
71
- firstArgument: node?.arguments?.[0],
72
- });
58
+ const sameSourceAndClock = areNodesSameInText({
59
+ context,
60
+ nodes: [params.source?.value, params.clock?.value],
61
+ });
62
+ if (!sameSourceAndClock) {
63
+ return;
73
64
  }
65
+
66
+ reportUnnecessaryDuplication({
67
+ context,
68
+ node,
69
+ params,
70
+ firstArgument: node?.arguments?.[0],
71
+ });
74
72
  },
75
73
  };
76
74
  },
@@ -86,11 +84,8 @@ function reportUnnecessaryDuplication({
86
84
  const properties = objectNode?.properties?.filter?.(
87
85
  (p) => p !== paramToExcludeNode
88
86
  );
89
- const newPropertiesText = properties
90
- .map((p) => context.getSourceCode().getText(p))
91
- .join(", ");
92
87
 
93
- return `{ ${newPropertiesText} }`;
88
+ return buildObjectInText.fromArrayOfNodes({ properties, context });
94
89
  }
95
90
 
96
91
  context.report({
@@ -1,6 +1,7 @@
1
1
  const { extractImportedFrom } = require("../../utils/extract-imported-from");
2
2
  const { traverseParentByType } = require("../../utils/traverse-parent-by-type");
3
3
  const { createLinkToRule } = require("../../utils/create-link-to-rule");
4
+ const { method } = require("../../utils/method");
4
5
 
5
6
  module.exports = {
6
7
  meta: {
@@ -29,67 +30,63 @@ module.exports = {
29
30
  });
30
31
  },
31
32
  CallExpression(node) {
32
- const POSSIBLE_USELESS_METHODS = ["sample", "guard"];
33
- for (const method of POSSIBLE_USELESS_METHODS) {
34
- const localMethod = importedFromEffector.get(method);
35
- if (!localMethod) {
36
- continue;
37
- }
38
-
39
- const isEffectorMethod = node?.callee?.name === localMethod;
40
- if (!isEffectorMethod) {
41
- continue;
42
- }
43
-
44
- const resultAssignedInVariable = traverseParentByType(
33
+ if (
34
+ method.isNot(["sample", "guard"], {
45
35
  node,
46
- "VariableDeclarator"
47
- );
48
- if (resultAssignedInVariable) {
49
- continue;
50
- }
36
+ importMap: importedFromEffector,
37
+ })
38
+ ) {
39
+ return;
40
+ }
51
41
 
52
- const resultReturnedFromFactory = traverseParentByType(
53
- node,
54
- "ReturnStatement"
55
- );
56
- if (resultReturnedFromFactory) {
57
- continue;
58
- }
42
+ const resultAssignedInVariable = traverseParentByType(
43
+ node,
44
+ "VariableDeclarator"
45
+ );
46
+ if (resultAssignedInVariable) {
47
+ return;
48
+ }
59
49
 
60
- const resultPartOfChain = traverseParentByType(
61
- node,
62
- "ObjectExpression"
63
- );
64
- if (resultPartOfChain) {
65
- continue;
66
- }
50
+ const resultReturnedFromFactory = traverseParentByType(
51
+ node,
52
+ "ReturnStatement"
53
+ );
54
+ if (resultReturnedFromFactory) {
55
+ return;
56
+ }
67
57
 
68
- const configHasTarget = node?.arguments?.[0]?.properties?.some(
69
- (prop) => prop?.key.name === "target"
70
- );
71
- if (configHasTarget) {
72
- continue;
73
- }
58
+ const resultPartOfChain = traverseParentByType(
59
+ node,
60
+ "ObjectExpression"
61
+ );
62
+ if (resultPartOfChain) {
63
+ return;
64
+ }
74
65
 
75
- const resultIsWatched = node?.parent?.property?.name === "watch";
76
- if (resultIsWatched) {
77
- continue;
78
- }
66
+ const configHasTarget = node?.arguments?.[0]?.properties?.some(
67
+ (prop) => prop?.key.name === "target"
68
+ );
69
+ if (configHasTarget) {
70
+ return;
71
+ }
79
72
 
80
- const resultIsArgument = node?.parent?.type === "CallExpression";
81
- if (resultIsArgument) {
82
- continue;
83
- }
73
+ const resultIsWatched = node?.parent?.property?.name === "watch";
74
+ if (resultIsWatched) {
75
+ return;
76
+ }
84
77
 
85
- context.report({
86
- node,
87
- messageId: "uselessMethod",
88
- data: {
89
- methodName: node?.callee?.name,
90
- },
91
- });
78
+ const resultIsArgument = node?.parent?.type === "CallExpression";
79
+ if (resultIsArgument) {
80
+ return;
92
81
  }
82
+
83
+ context.report({
84
+ node,
85
+ messageId: "uselessMethod",
86
+ data: {
87
+ methodName: node?.callee?.name,
88
+ },
89
+ });
93
90
  },
94
91
  };
95
92
  },
@@ -3,6 +3,9 @@ const {
3
3
  traverseNestedObjectNode,
4
4
  } = require("../../utils/traverse-nested-object-node");
5
5
  const { createLinkToRule } = require("../../utils/create-link-to-rule");
6
+ const { method } = require("../../utils/method");
7
+ const { replaceForwardBySample } = require("../../utils/replace-by-sample");
8
+ const { extractConfig } = require("../../utils/extract-config");
6
9
 
7
10
  module.exports = {
8
11
  meta: {
@@ -18,37 +21,35 @@ module.exports = {
18
21
  "Instead of `forward` with `{{ eventName }}.map` you can use `sample`",
19
22
  overPrepend:
20
23
  "Instead of `forward` with `{{ eventName }}.prepend` you can use `sample`",
24
+ replaceWithSample: "Repalce `forward` with `sample`.",
21
25
  },
22
26
  schema: [],
27
+ hasSuggestions: true,
23
28
  },
24
29
  create(context) {
30
+ const importNodes = new Map();
25
31
  const importedFromEffector = new Map();
26
32
 
27
33
  return {
28
34
  ImportDeclaration(node) {
29
35
  extractImportedFrom({
30
36
  importMap: importedFromEffector,
37
+ nodeMap: importNodes,
31
38
  node,
32
39
  packageName: "effector",
33
40
  });
34
41
  },
35
42
  CallExpression(node) {
36
- const localMethod = importedFromEffector.get("forward");
37
- if (!localMethod) {
38
- return;
39
- }
40
-
41
- const isEffectorMethod = node?.callee?.name === localMethod;
42
- if (!isEffectorMethod) {
43
+ if (
44
+ method.isNot("forward", {
45
+ node,
46
+ importMap: importedFromEffector,
47
+ })
48
+ ) {
43
49
  return;
44
50
  }
45
51
 
46
- const forwardConfig = {
47
- from: node.arguments?.[0]?.properties.find(
48
- (n) => n.key?.name === "from"
49
- ),
50
- to: node.arguments?.[0]?.properties.find((n) => n.key?.name === "to"),
51
- };
52
+ const forwardConfig = extractConfig(["from", "to"], { node });
52
53
 
53
54
  if (!forwardConfig.from || !forwardConfig.to) {
54
55
  return;
@@ -72,14 +73,25 @@ module.exports = {
72
73
  return;
73
74
  }
74
75
 
75
- // console.log(eventName);
76
-
77
76
  context.report({
78
77
  node,
79
78
  messageId,
80
79
  data: {
81
80
  eventName,
82
81
  },
82
+ suggest: [
83
+ {
84
+ messageId: "replaceWithSample",
85
+ *fix(fixer) {
86
+ yield* replaceForwardBySample(forwardConfig, {
87
+ fixer,
88
+ node,
89
+ context,
90
+ importNodes,
91
+ });
92
+ },
93
+ },
94
+ ],
83
95
  });
84
96
  }
85
97
 
@@ -0,0 +1,19 @@
1
+ const buildObjectInText = {
2
+ fromArrayOfNodes({ properties, context }) {
3
+ const content = properties
4
+ .map((property) => context.getSourceCode().getText(property))
5
+ .join(", ");
6
+
7
+ return `{ ${content} }`;
8
+ },
9
+ fromMapOfNodes({ properties, context }) {
10
+ const content = Object.entries(properties)
11
+ .filter(([_, node]) => Boolean(node))
12
+ .map(([key, node]) => `${key}: ${context.getSourceCode().getText(node)}`)
13
+ .join(", ");
14
+
15
+ return `{ ${content} }`;
16
+ },
17
+ };
18
+
19
+ module.exports = { buildObjectInText };
@@ -0,0 +1,13 @@
1
+ function extractConfig(fields, { node }) {
2
+ const config = {};
3
+
4
+ fields.forEach((field) => {
5
+ config[field] = node.arguments?.[0]?.properties.find(
6
+ (n) => n.key?.name === field
7
+ );
8
+ });
9
+
10
+ return config;
11
+ }
12
+
13
+ module.exports = { extractConfig };
@@ -1,7 +1,8 @@
1
- function extractImportedFrom({ importMap, node, packageName }) {
1
+ function extractImportedFrom({ importMap, nodeMap, node, packageName }) {
2
2
  if (node.source.value === packageName) {
3
3
  for (const s of node.specifiers) {
4
4
  importMap.set(s.imported.name, s.local.name);
5
+ nodeMap?.set(s.imported.name, s);
5
6
  }
6
7
  }
7
8
  }
@@ -0,0 +1,23 @@
1
+ function isSomeMethod(methodName, { node, importMap }) {
2
+ const normalizedMethodNames = Array.isArray(methodName)
3
+ ? methodName
4
+ : [methodName];
5
+
6
+ return normalizedMethodNames.some((method) => {
7
+ const localMethod = importMap.get(method);
8
+ if (!localMethod) {
9
+ return false;
10
+ }
11
+
12
+ const isEffectorMethod = node?.callee?.name === localMethod;
13
+
14
+ return isEffectorMethod;
15
+ });
16
+ }
17
+
18
+ const method = {
19
+ is: (...args) => isSomeMethod(...args),
20
+ isNot: (...args) => !isSomeMethod(...args),
21
+ };
22
+
23
+ module.exports = { method };
@@ -32,6 +32,21 @@ const nodeTypeIs = {
32
32
  }),
33
33
  gate: (opts) =>
34
34
  hasType({ ...opts, possibleTypes: ["Gate"], from: "effector-react" }),
35
+ effectorReactHook: (opts) =>
36
+ hasType({
37
+ ...opts,
38
+ possibleTypes: opts.hook
39
+ ? [].concat(opts.hook)
40
+ : [
41
+ "useStore",
42
+ "useStoreMap",
43
+ "useList",
44
+ "useEvent",
45
+ "useGate",
46
+ "useUnit",
47
+ ],
48
+ from: "effector-react",
49
+ }),
35
50
  not: {
36
51
  effect: (opts) =>
37
52
  !hasType({
@@ -51,6 +66,7 @@ const nodeTypeIs = {
51
66
  }),
52
67
  gate: (opts) =>
53
68
  !hasType({ ...opts, possibleTypes: ["Gate"], from: "effector-react" }),
69
+ effectorReactHook: (opts) => !nodeTypeIs.effectorReactHook(opts),
54
70
  },
55
71
  };
56
72
 
package/utils/react.js ADDED
@@ -0,0 +1,184 @@
1
+ // borrowed from
2
+ // https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js
3
+
4
+ /**
5
+ * Catch all identifiers that begin with "use" followed by an uppercase Latin
6
+ * character to exclude identifiers like "user".
7
+ */
8
+
9
+ function isHookName(s) {
10
+ return /^use[A-Z0-9].*$/.test(s);
11
+ }
12
+
13
+ /**
14
+ * We consider hooks to be a hook name identifier or a member expression
15
+ * containing a hook name.
16
+ */
17
+
18
+ function isHook(node) {
19
+ if (node.type === "Identifier") {
20
+ return isHookName(node.name);
21
+ } else if (
22
+ node.type === "MemberExpression" &&
23
+ !node.computed &&
24
+ isHook(node.property)
25
+ ) {
26
+ const obj = node.object;
27
+ const isPascalCaseNameSpace = /^[A-Z].*/;
28
+ return obj.type === "Identifier" && isPascalCaseNameSpace.test(obj.name);
29
+ } else {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Checks if the node is a React component name. React component names must
36
+ * always start with a non-lowercase letter. So `MyComponent` or `_MyComponent`
37
+ * are valid component names for instance.
38
+ */
39
+ function isComponentName(node) {
40
+ if (node.type === "Identifier") {
41
+ return !/^[a-z]/.test(node.name);
42
+ } else {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ function isReactFunction(node, functionName) {
48
+ return (
49
+ node.name === functionName ||
50
+ (node.type === "MemberExpression" &&
51
+ node.object.name === "React" &&
52
+ node.property.name === functionName)
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Checks if the node is a callback argument of forwardRef. This render function
58
+ * should follow the rules of hooks.
59
+ */
60
+
61
+ function isForwardRefCallback(node) {
62
+ return !!(
63
+ node.parent &&
64
+ node.parent.callee &&
65
+ isReactFunction(node.parent.callee, "forwardRef")
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Checks if the node is a callback argument of React.memo. This anonymous
71
+ * functional component should follow the rules of hooks.
72
+ */
73
+
74
+ function isMemoCallback(node) {
75
+ return !!(
76
+ node.parent &&
77
+ node.parent.callee &&
78
+ isReactFunction(node.parent.callee, "memo")
79
+ );
80
+ }
81
+
82
+ function isInsideReactComponent(node) {
83
+ while (node) {
84
+ const functionName = getFunctionName(node);
85
+ if (functionName) {
86
+ if (isComponentName(functionName) || isHook(functionName)) {
87
+ return true;
88
+ }
89
+ }
90
+ if (isForwardRefCallback(node) || isMemoCallback(node)) {
91
+ return true;
92
+ }
93
+ node = node.parent;
94
+ }
95
+ return false;
96
+ }
97
+
98
+ function isInsideReactHook(node) {
99
+ while (node) {
100
+ const functionName = getFunctionName(node);
101
+ if (functionName) {
102
+ if (isHook(functionName)) {
103
+ return true;
104
+ }
105
+ }
106
+ node = node.parent;
107
+ }
108
+ return false;
109
+ }
110
+
111
+ module.exports = {
112
+ isInsideReactComponent,
113
+ isInsideReactHook,
114
+ };
115
+
116
+ /**
117
+ * Gets the static name of a function AST node. For function declarations it is
118
+ * easy. For anonymous function expressions it is much harder. If you search for
119
+ * `IsAnonymousFunctionDefinition()` in the ECMAScript spec you'll find places
120
+ * where JS gives anonymous function expressions names. We roughly detect the
121
+ * same AST nodes with some exceptions to better fit our use case.
122
+ */
123
+
124
+ function getFunctionName(node) {
125
+ if (
126
+ node.type === "FunctionDeclaration" ||
127
+ (node.type === "FunctionExpression" && node.id)
128
+ ) {
129
+ // function useHook() {}
130
+ // const whatever = function useHook() {};
131
+ //
132
+ // Function declaration or function expression names win over any
133
+ // assignment statements or other renames.
134
+ return node.id;
135
+ } else if (
136
+ node.type === "FunctionExpression" ||
137
+ node.type === "ArrowFunctionExpression"
138
+ ) {
139
+ if (
140
+ node.parent.type === "VariableDeclarator" &&
141
+ node.parent.init === node
142
+ ) {
143
+ // const useHook = () => {};
144
+ return node.parent.id;
145
+ } else if (
146
+ node.parent.type === "AssignmentExpression" &&
147
+ node.parent.right === node &&
148
+ node.parent.operator === "="
149
+ ) {
150
+ // useHook = () => {};
151
+ return node.parent.left;
152
+ } else if (
153
+ node.parent.type === "Property" &&
154
+ node.parent.value === node &&
155
+ !node.parent.computed
156
+ ) {
157
+ // {useHook: () => {}}
158
+ // {useHook() {}}
159
+ return node.parent.key;
160
+
161
+ // NOTE: We could also support `ClassProperty` and `MethodDefinition`
162
+ // here to be pedantic. However, hooks in a class are an anti-pattern. So
163
+ // we don't allow it to error early.
164
+ //
165
+ // class {useHook = () => {}}
166
+ // class {useHook() {}}
167
+ } else if (
168
+ node.parent.type === "AssignmentPattern" &&
169
+ node.parent.right === node &&
170
+ !node.parent.computed
171
+ ) {
172
+ // const {useHook = () => {}} = {};
173
+ // ({useHook = () => {}} = {});
174
+ //
175
+ // Kinda clowny, but we'd said we'd follow spec convention for
176
+ // `IsAnonymousFunctionDefinition()` usage.
177
+ return node.parent.left;
178
+ } else {
179
+ return undefined;
180
+ }
181
+ } else {
182
+ return undefined;
183
+ }
184
+ }
@@ -1,8 +1,45 @@
1
1
  const { readFileSync } = require("fs");
2
2
  const { join } = require("path");
3
+ const glob = require("glob");
3
4
 
4
5
  function readExample(dirname, exampleName) {
5
6
  return readFileSync(join(dirname, "examples", exampleName)).toString();
6
7
  }
7
8
 
8
- module.exports = { readExample };
9
+ function getCorrectExamples(dirname, config = {}) {
10
+ const { ext = "js", namesOnly = true } = config;
11
+ const pattern = `correct-*.${ext}`;
12
+ const correct = glob.sync(join(dirname, "examples", pattern));
13
+
14
+ let result = correct;
15
+
16
+ if (namesOnly) {
17
+ result = result.map((path) => {
18
+ const rightSlashIdx = path.lastIndexOf("/");
19
+
20
+ return path.slice(rightSlashIdx + 1);
21
+ });
22
+ }
23
+
24
+ return result;
25
+ }
26
+
27
+ function getIncorrectExamples(dirname, config = {}) {
28
+ const { ext = "js", namesOnly = true } = config;
29
+ const pattern = `incorrect-*.${ext}`;
30
+ const incorrect = glob.sync(join(dirname, "examples", pattern));
31
+
32
+ let result = incorrect;
33
+
34
+ if (namesOnly) {
35
+ result = result.map((path) => {
36
+ const rightSlashIdx = path.lastIndexOf("/");
37
+
38
+ return path.slice(rightSlashIdx + 1);
39
+ });
40
+ }
41
+
42
+ return result;
43
+ }
44
+
45
+ module.exports = { readExample, getCorrectExamples, getIncorrectExamples };