eslint-plugin-effector 0.6.0 → 0.7.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/.nvmrc +1 -0
- package/README.md +22 -15
- package/config/future.js +7 -0
- package/config/react.js +1 -0
- package/index.js +3 -0
- package/package.json +2 -2
- package/rules/mandatory-useEvent/mandatory-useEvent.js +63 -0
- package/rules/no-forward/no-forward.js +2 -6
- package/rules/no-guard/no-guard.js +78 -0
- package/rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping.js +2 -6
- package/utils/extract-config.js +13 -0
- package/utils/node-type-is.js +16 -0
- package/utils/react.js +184 -0
- package/utils/read-example.js +38 -1
- package/utils/replace-by-sample.js +31 -3
package/.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
16.10.0
|
package/README.md
CHANGED
|
@@ -39,36 +39,43 @@ To configure individual rules:
|
|
|
39
39
|
}
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
### Available
|
|
42
|
+
### Available rules by preset
|
|
43
43
|
|
|
44
44
|
#### plugin:effector/recommended
|
|
45
45
|
|
|
46
46
|
This preset is recommended for most projects.
|
|
47
47
|
|
|
48
|
+
- [effector/enforce-store-naming-convention](rules/enforce-store-naming-convention/enforce-store-naming-convention.md)
|
|
49
|
+
- [effector/enforce-effect-naming-convention](rules/enforce-effect-naming-convention/enforce-effect-naming-convention.md)
|
|
50
|
+
- [effector/no-getState](rules/no-getState/no-getState.md)
|
|
51
|
+
- [effector/no-useless-methods](rules/no-useless-methods/no-useless-methods.md)
|
|
52
|
+
- [effector/no-unnecessary-duplication](rules/no-unnecessary-duplication/no-unnecessary-duplication.md)
|
|
53
|
+
- [effector/prefer-sample-over-forward-with-mapping](rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping.md)
|
|
54
|
+
- [effector/no-ambiguity-target](rules/no-ambiguity-target/no-ambiguity-target.md)
|
|
55
|
+
- [effector/no-watch](rules/no-watch/no-watch.md)
|
|
56
|
+
- [effector/no-unnecessary-combination](rules/no-unnecessary-combination/no-unnecessary-combination.md)
|
|
57
|
+
- [effector/no-duplicate-on](rules/no-duplicate-on/no-duplicate-on.md)
|
|
58
|
+
- [effector/keep-options-order](rules/keep-options-order/keep-options-order.md)
|
|
59
|
+
|
|
48
60
|
#### plugin:effector/scope
|
|
49
61
|
|
|
50
62
|
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
63
|
|
|
64
|
+
- [effector/strict-effect-handlers](rules/strict-effect-handlers/strict-effect-handlers.md)
|
|
65
|
+
|
|
52
66
|
#### plugin:effector/react
|
|
53
67
|
|
|
54
68
|
This preset is recommended for projects that use [React](https://reactjs.org) with Effector.
|
|
55
69
|
|
|
56
|
-
### Supported rules
|
|
57
|
-
|
|
58
|
-
- [effector/enforce-store-naming-convention](rules/enforce-store-naming-convention/enforce-store-naming-convention.md)
|
|
59
|
-
- [effector/enforce-effect-naming-convention](rules/enforce-effect-naming-convention/enforce-effect-naming-convention.md)
|
|
60
70
|
- [effector/enforce-gate-naming-convention](rules/enforce-gate-naming-convention/enforce-gate-naming-convention.md)
|
|
61
|
-
- [effector/
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
- [effector/mandatory-useEvent](rules/mandatory-useEvent/mandatory-useEvent.md)
|
|
72
|
+
|
|
73
|
+
#### plugin:effector/future
|
|
74
|
+
|
|
75
|
+
This preset contains rules wich enforce _future-effector_ code-style.
|
|
76
|
+
|
|
64
77
|
- [effector/no-forward](rules/no-forward/no-forward.md)
|
|
65
|
-
- [effector/no-
|
|
66
|
-
- [effector/no-duplicate-on](rules/no-duplicate-on/no-duplicate-on.md)
|
|
67
|
-
- [effector/no-getState](rules/no-getState/no-getState.md)
|
|
68
|
-
- [effector/no-watch](rules/no-watch/no-watch.md)
|
|
69
|
-
- [effector/prefer-sample-over-forward-with-mapping](rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping.md)
|
|
70
|
-
- [effector/strict-effect-handlers](rules/strict-effect-handlers/strict-effect-handlers.md)
|
|
71
|
-
- [effector/keep-options-order](rules/keep-options-order/keep-options-order.md)
|
|
78
|
+
- [effector/no-guard](rules/no-guard/no-guard.md)
|
|
72
79
|
|
|
73
80
|
## Maintenance
|
|
74
81
|
|
package/config/future.js
ADDED
package/config/react.js
CHANGED
package/index.js
CHANGED
|
@@ -14,10 +14,13 @@ module.exports = {
|
|
|
14
14
|
"enforce-gate-naming-convention": require("./rules/enforce-gate-naming-convention/enforce-gate-naming-convention"),
|
|
15
15
|
"keep-options-order": require("./rules/keep-options-order/keep-options-order"),
|
|
16
16
|
"no-forward": require("./rules/no-forward/no-forward"),
|
|
17
|
+
"no-guard": require("./rules/no-guard/no-guard"),
|
|
18
|
+
"mandatory-useEvent": require("./rules/mandatory-useEvent/mandatory-useEvent"),
|
|
17
19
|
},
|
|
18
20
|
configs: {
|
|
19
21
|
recommended: require("./config/recommended"),
|
|
20
22
|
scope: require("./config/scope"),
|
|
21
23
|
react: require("./config/react"),
|
|
24
|
+
future: require("./config/future"),
|
|
22
25
|
},
|
|
23
26
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-effector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Enforcing best practices for Effector",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"eslint",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"access": "public"
|
|
17
17
|
},
|
|
18
18
|
"engines": {
|
|
19
|
-
"node": "^14 || ^16"
|
|
19
|
+
"node": "^14 || ^16 || ^18"
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
22
|
"effector": "*",
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
2
|
+
const { isInsideReactComponent } = require("../../utils/react");
|
|
3
|
+
const { nodeTypeIs } = require("../../utils/node-type-is");
|
|
4
|
+
const { traverseParentByType } = require("../../utils/traverse-parent-by-type");
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
meta: {
|
|
8
|
+
type: "problem",
|
|
9
|
+
docs: {
|
|
10
|
+
description:
|
|
11
|
+
"Forbids `Event` and `Effect` usage without `useEvent` in React components.",
|
|
12
|
+
category: "Quality",
|
|
13
|
+
recommended: true,
|
|
14
|
+
url: createLinkToRule("mandatory-useEvent"),
|
|
15
|
+
},
|
|
16
|
+
messages: {
|
|
17
|
+
useEventNeeded:
|
|
18
|
+
"{{ unitName }} must be wrapped with `useEvent` from `effector-react` before usage inside React components",
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
},
|
|
22
|
+
create(context) {
|
|
23
|
+
const parserServices = context.parserServices;
|
|
24
|
+
|
|
25
|
+
// TypeScript-only rule, since units can be imported from anywhere
|
|
26
|
+
if (parserServices.hasFullTypeInformation) {
|
|
27
|
+
return {
|
|
28
|
+
Identifier(node) {
|
|
29
|
+
if (isInsideReactComponent(node)) {
|
|
30
|
+
if (
|
|
31
|
+
nodeTypeIs.effect({ node, context }) ||
|
|
32
|
+
nodeTypeIs.event({ node, context })
|
|
33
|
+
) {
|
|
34
|
+
if (!isInsideUseEventCall({ node, context })) {
|
|
35
|
+
context.report({
|
|
36
|
+
node,
|
|
37
|
+
messageId: "useEventNeeded",
|
|
38
|
+
data: {
|
|
39
|
+
unitName: node.name,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {};
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function isInsideUseEventCall({ node, context }) {
|
|
54
|
+
const calleeParentNode = traverseParentByType(node.parent, "CallExpression");
|
|
55
|
+
|
|
56
|
+
if (!calleeParentNode?.callee) return false;
|
|
57
|
+
|
|
58
|
+
return nodeTypeIs.effectorReactHook({
|
|
59
|
+
node: calleeParentNode.callee,
|
|
60
|
+
context,
|
|
61
|
+
hook: ["useEvent", "useUnit"],
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -2,6 +2,7 @@ const { extractImportedFrom } = require("../../utils/extract-imported-from");
|
|
|
2
2
|
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
3
3
|
const { method } = require("../../utils/method");
|
|
4
4
|
const { replaceForwardBySample } = require("../../utils/replace-by-sample");
|
|
5
|
+
const { extractConfig } = require("../../utils/extract-config");
|
|
5
6
|
|
|
6
7
|
module.exports = {
|
|
7
8
|
meta: {
|
|
@@ -43,12 +44,7 @@ module.exports = {
|
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
|
|
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
|
-
};
|
|
47
|
+
const forwardConfig = extractConfig(["from", "to"], { node });
|
|
52
48
|
|
|
53
49
|
if (!forwardConfig.from || !forwardConfig.to) {
|
|
54
50
|
return;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const { extractImportedFrom } = require("../../utils/extract-imported-from");
|
|
2
|
+
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
3
|
+
const { method } = require("../../utils/method");
|
|
4
|
+
const { replaceGuardBySample } = require("../../utils/replace-by-sample");
|
|
5
|
+
const { extractConfig } = require("../../utils/extract-config");
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Prefer `sample` over `guard`",
|
|
12
|
+
category: "Quality",
|
|
13
|
+
recommended: true,
|
|
14
|
+
url: createLinkToRule("no-guard"),
|
|
15
|
+
},
|
|
16
|
+
messages: {
|
|
17
|
+
noGuard:
|
|
18
|
+
"Instead of `guard` you can use `sample`, it is more extendable.",
|
|
19
|
+
replaceWithSample: "Repalce `guard` with `sample`.",
|
|
20
|
+
},
|
|
21
|
+
schema: [],
|
|
22
|
+
hasSuggestions: true,
|
|
23
|
+
},
|
|
24
|
+
create(context) {
|
|
25
|
+
const importNodes = new Map();
|
|
26
|
+
const importedFromEffector = new Map();
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
ImportDeclaration(node) {
|
|
30
|
+
extractImportedFrom({
|
|
31
|
+
importMap: importedFromEffector,
|
|
32
|
+
nodeMap: importNodes,
|
|
33
|
+
node,
|
|
34
|
+
packageName: "effector",
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
CallExpression(node) {
|
|
38
|
+
if (
|
|
39
|
+
method.isNot("guard", {
|
|
40
|
+
node,
|
|
41
|
+
importMap: importedFromEffector,
|
|
42
|
+
})
|
|
43
|
+
) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const guardConfig = extractConfig(
|
|
48
|
+
["source", "clock", "target", "filter"],
|
|
49
|
+
{
|
|
50
|
+
node,
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (!guardConfig.clock || !guardConfig.filter) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
context.report({
|
|
59
|
+
messageId: "noGuard",
|
|
60
|
+
node,
|
|
61
|
+
suggest: [
|
|
62
|
+
{
|
|
63
|
+
messageId: "replaceWithSample",
|
|
64
|
+
*fix(fixer) {
|
|
65
|
+
yield* replaceGuardBySample(guardConfig, {
|
|
66
|
+
fixer,
|
|
67
|
+
node,
|
|
68
|
+
context,
|
|
69
|
+
importNodes,
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
package/rules/prefer-sample-over-forward-with-mapping/prefer-sample-over-forward-with-mapping.js
CHANGED
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
const { createLinkToRule } = require("../../utils/create-link-to-rule");
|
|
6
6
|
const { method } = require("../../utils/method");
|
|
7
7
|
const { replaceForwardBySample } = require("../../utils/replace-by-sample");
|
|
8
|
+
const { extractConfig } = require("../../utils/extract-config");
|
|
8
9
|
|
|
9
10
|
module.exports = {
|
|
10
11
|
meta: {
|
|
@@ -48,12 +49,7 @@ module.exports = {
|
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
const forwardConfig = {
|
|
52
|
-
from: node.arguments?.[0]?.properties.find(
|
|
53
|
-
(n) => n.key?.name === "from"
|
|
54
|
-
),
|
|
55
|
-
to: node.arguments?.[0]?.properties.find((n) => n.key?.name === "to"),
|
|
56
|
-
};
|
|
52
|
+
const forwardConfig = extractConfig(["from", "to"], { node });
|
|
57
53
|
|
|
58
54
|
if (!forwardConfig.from || !forwardConfig.to) {
|
|
59
55
|
return;
|
|
@@ -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 };
|
package/utils/node-type-is.js
CHANGED
|
@@ -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
|
+
}
|
package/utils/read-example.js
CHANGED
|
@@ -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
|
-
|
|
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 };
|
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
const { buildObjectInText } = require("./builders");
|
|
2
2
|
|
|
3
|
+
function* replaceGuardBySample(
|
|
4
|
+
guardConfig,
|
|
5
|
+
{ fixer, node, context, importNodes }
|
|
6
|
+
) {
|
|
7
|
+
let mapperFunctionNode = null;
|
|
8
|
+
|
|
9
|
+
let clockNode = guardConfig.clock?.value;
|
|
10
|
+
let targetNode = guardConfig.target?.value;
|
|
11
|
+
let sourceNode = guardConfig.source?.value;
|
|
12
|
+
let filterNode = guardConfig.filter?.value;
|
|
13
|
+
|
|
14
|
+
if (
|
|
15
|
+
targetNode.type === "CallExpression" &&
|
|
16
|
+
targetNode?.callee?.property?.name === "prepend"
|
|
17
|
+
) {
|
|
18
|
+
mapperFunctionNode = targetNode?.arguments?.[0];
|
|
19
|
+
targetNode = targetNode.callee.object;
|
|
20
|
+
targetMapperUsed = true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
yield* replaceBySample(
|
|
24
|
+
{ clockNode, sourceNode, filterNode, mapperFunctionNode, targetNode },
|
|
25
|
+
{ node, fixer, context, importNodes, methodName: "guard" }
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
3
29
|
function* replaceForwardBySample(
|
|
4
30
|
forwardConfig,
|
|
5
31
|
{ fixer, node, context, importNodes }
|
|
@@ -39,13 +65,13 @@ function* replaceForwardBySample(
|
|
|
39
65
|
}
|
|
40
66
|
|
|
41
67
|
yield* replaceBySample(
|
|
42
|
-
{ clockNode,
|
|
68
|
+
{ clockNode, mapperFunctionNode, targetNode },
|
|
43
69
|
{ node, fixer, context, importNodes, methodName: "forward" }
|
|
44
70
|
);
|
|
45
71
|
}
|
|
46
72
|
|
|
47
73
|
function* replaceBySample(
|
|
48
|
-
{ clockNode,
|
|
74
|
+
{ clockNode, sourceNode, filterNode, mapperFunctionNode, targetNode },
|
|
49
75
|
{ node, fixer, context, importNodes, methodName }
|
|
50
76
|
) {
|
|
51
77
|
yield fixer.replaceText(
|
|
@@ -53,6 +79,8 @@ function* replaceBySample(
|
|
|
53
79
|
`sample(${buildObjectInText.fromMapOfNodes({
|
|
54
80
|
properties: {
|
|
55
81
|
clock: clockNode,
|
|
82
|
+
source: sourceNode,
|
|
83
|
+
filter: filterNode,
|
|
56
84
|
fn: mapperFunctionNode,
|
|
57
85
|
target: targetNode,
|
|
58
86
|
},
|
|
@@ -63,4 +91,4 @@ function* replaceBySample(
|
|
|
63
91
|
yield fixer.replaceText(importNodes.get(methodName), "sample");
|
|
64
92
|
}
|
|
65
93
|
|
|
66
|
-
module.exports = { replaceForwardBySample };
|
|
94
|
+
module.exports = { replaceForwardBySample, replaceGuardBySample };
|