@diphyx/eslint-plugin 1.0.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/LICENSE +21 -0
- package/README.md +130 -0
- package/package.json +55 -0
- package/src/configs/index.mjs +11 -0
- package/src/configs/recommended.mjs +184 -0
- package/src/index.mjs +22 -0
- package/src/rules/composable-naming.mjs +63 -0
- package/src/rules/index.mjs +105 -0
- package/src/rules/radash-prefer-call.mjs +33 -0
- package/src/rules/radash-prefer-clone.mjs +33 -0
- package/src/rules/radash-prefer-is.mjs +51 -0
- package/src/rules/radash-prefer-sleep.mjs +53 -0
- package/src/rules/radash-prefer-sum.mjs +71 -0
- package/src/rules/radash-prefer-unique.mjs +30 -0
- package/src/rules/script-define-object.mjs +49 -0
- package/src/rules/script-section-order.mjs +159 -0
- package/src/rules/store-config-order.mjs +55 -0
- package/src/rules/store-mode-enum.mjs +41 -0
- package/src/rules/store-name-match.mjs +56 -0
- package/src/rules/store-no-unknown-key.mjs +42 -0
- package/src/rules/store-require-action.mjs +5 -0
- package/src/rules/store-require-model.mjs +5 -0
- package/src/rules/store-require-name.mjs +5 -0
- package/src/rules/store-require-view.mjs +5 -0
- package/src/rules/store-section-function.mjs +47 -0
- package/src/rules/store-section-method.mjs +53 -0
- package/src/rules/store-section-return-shorthand.mjs +81 -0
- package/src/rules/store-shape-suffix.mjs +37 -0
- package/src/rules/store-suffix.mjs +39 -0
- package/src/rules/template-text.mjs +80 -0
- package/src/rules/template-v-else.mjs +8 -0
- package/src/rules/template-v-for.mjs +8 -0
- package/src/rules/template-v-if.mjs +8 -0
- package/src/rules/vueuse-prefer-clipboard.mjs +29 -0
- package/src/rules/vueuse-prefer-member-call.mjs +39 -0
- package/src/rules/vueuse-prefer-observer.mjs +34 -0
- package/src/rules/vueuse-prefer-route.mjs +64 -0
- package/src/rules/vueuse-prefer-storage.mjs +44 -0
- package/src/rules/vueuse-prefer-timer.mjs +32 -0
- package/src/utils/ast.mjs +24 -0
- package/src/utils/docs.mjs +8 -0
- package/src/utils/store.mjs +66 -0
- package/src/utils/vue.mjs +70 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Prefer radash is* helpers over `typeof` comparisons.
|
|
2
|
+
|
|
3
|
+
const TYPEOF_HELPERS = {
|
|
4
|
+
number: "isNumber",
|
|
5
|
+
string: "isString",
|
|
6
|
+
boolean: "isBoolean",
|
|
7
|
+
function: "isFunction",
|
|
8
|
+
symbol: "isSymbol",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// typeof x === "<type>" → the compared type string, or null.
|
|
12
|
+
function getTypeofComparison(node) {
|
|
13
|
+
if (node.operator !== "===" && node.operator !== "!==") {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const typeofSide = [node.left, node.right].find(
|
|
18
|
+
(side) => side.type === "UnaryExpression" && side.operator === "typeof",
|
|
19
|
+
);
|
|
20
|
+
const literalSide = [node.left, node.right].find((side) => side.type === "Literal");
|
|
21
|
+
|
|
22
|
+
if (!typeofSide || !literalSide || typeof literalSide.value !== "string") {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return literalSide.value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default {
|
|
30
|
+
meta: {
|
|
31
|
+
type: "suggestion",
|
|
32
|
+
docs: {
|
|
33
|
+
description: "prefer radash is* helpers over typeof comparisons",
|
|
34
|
+
},
|
|
35
|
+
messages: {
|
|
36
|
+
preferIs: "Use radash '{{helper}}()' instead of a 'typeof' comparison.",
|
|
37
|
+
},
|
|
38
|
+
schema: [],
|
|
39
|
+
},
|
|
40
|
+
create(context) {
|
|
41
|
+
return {
|
|
42
|
+
BinaryExpression(node) {
|
|
43
|
+
const type = getTypeofComparison(node);
|
|
44
|
+
const helper = type && TYPEOF_HELPERS[type];
|
|
45
|
+
if (helper) {
|
|
46
|
+
context.report({ node, messageId: "preferIs", data: { helper } });
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Prefer radash sleep() over wrapping setTimeout in a Promise.
|
|
2
|
+
|
|
3
|
+
function isSetTimeoutCall(node) {
|
|
4
|
+
return (
|
|
5
|
+
Boolean(node) &&
|
|
6
|
+
node.type === "CallExpression" &&
|
|
7
|
+
node.callee.type === "Identifier" &&
|
|
8
|
+
node.callee.name === "setTimeout"
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
meta: {
|
|
14
|
+
type: "suggestion",
|
|
15
|
+
docs: {
|
|
16
|
+
description: "prefer radash sleep() over wrapping setTimeout in a Promise",
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
preferSleep: "Use radash 'sleep()' instead of wrapping setTimeout in a Promise.",
|
|
20
|
+
},
|
|
21
|
+
schema: [],
|
|
22
|
+
},
|
|
23
|
+
create(context) {
|
|
24
|
+
return {
|
|
25
|
+
NewExpression(node) {
|
|
26
|
+
if (node.callee.type !== "Identifier" || node.callee.name !== "Promise") {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const executor = node.arguments[0];
|
|
31
|
+
if (
|
|
32
|
+
!executor ||
|
|
33
|
+
(executor.type !== "ArrowFunctionExpression" && executor.type !== "FunctionExpression")
|
|
34
|
+
) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const body = executor.body;
|
|
39
|
+
const wrapsSetTimeout =
|
|
40
|
+
isSetTimeoutCall(body) ||
|
|
41
|
+
(body.type === "BlockStatement" &&
|
|
42
|
+
body.body.some(
|
|
43
|
+
(statement) =>
|
|
44
|
+
statement.type === "ExpressionStatement" && isSetTimeoutCall(statement.expression),
|
|
45
|
+
));
|
|
46
|
+
|
|
47
|
+
if (wrapsSetTimeout) {
|
|
48
|
+
context.report({ node, messageId: "preferSleep" });
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Prefer radash sum() over a reduce that adds values.
|
|
2
|
+
|
|
3
|
+
// (a, b) => a + b | (a, b) => { return a + b }
|
|
4
|
+
function isSumReducer(node) {
|
|
5
|
+
if (node.type !== "ArrowFunctionExpression" && node.type !== "FunctionExpression") {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (node.params.length < 2 || node.params[0].type !== "Identifier" || node.params[1].type !== "Identifier") {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let expression = node.body;
|
|
14
|
+
if (expression.type === "BlockStatement") {
|
|
15
|
+
if (expression.body.length !== 1 || expression.body[0].type !== "ReturnStatement") {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
expression = expression.body[0].argument;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!expression || expression.type !== "BinaryExpression" || expression.operator !== "+") {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const names = new Set([node.params[0].name, node.params[1].name]);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
expression.left.type === "Identifier" &&
|
|
30
|
+
expression.right.type === "Identifier" &&
|
|
31
|
+
names.has(expression.left.name) &&
|
|
32
|
+
names.has(expression.right.name)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default {
|
|
37
|
+
meta: {
|
|
38
|
+
type: "suggestion",
|
|
39
|
+
docs: {
|
|
40
|
+
description: "prefer radash sum() over a reduce that adds values",
|
|
41
|
+
},
|
|
42
|
+
messages: {
|
|
43
|
+
preferSum: "Use radash 'sum()' instead of a reduce that adds values.",
|
|
44
|
+
},
|
|
45
|
+
schema: [],
|
|
46
|
+
},
|
|
47
|
+
create(context) {
|
|
48
|
+
return {
|
|
49
|
+
CallExpression(node) {
|
|
50
|
+
if (node.callee.type !== "MemberExpression" || node.callee.computed) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (node.callee.property.type !== "Identifier" || node.callee.property.name !== "reduce") {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const [reducer, initial] = node.arguments;
|
|
59
|
+
if (!reducer || !isSumReducer(reducer)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (initial && !(initial.type === "Literal" && initial.value === 0)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
context.report({ node: node.callee.property, messageId: "preferSum" });
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Prefer radash unique() over spreading a new Set.
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: "suggestion",
|
|
6
|
+
docs: {
|
|
7
|
+
description: "prefer radash unique() over spreading a new Set",
|
|
8
|
+
},
|
|
9
|
+
messages: {
|
|
10
|
+
preferUnique: "Use radash 'unique()' instead of spreading a 'new Set(...)'.",
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
},
|
|
14
|
+
create(context) {
|
|
15
|
+
return {
|
|
16
|
+
ArrayExpression(node) {
|
|
17
|
+
const only = node.elements.length === 1 ? node.elements[0] : null;
|
|
18
|
+
if (
|
|
19
|
+
only &&
|
|
20
|
+
only.type === "SpreadElement" &&
|
|
21
|
+
only.argument.type === "NewExpression" &&
|
|
22
|
+
only.argument.callee.type === "Identifier" &&
|
|
23
|
+
only.argument.callee.name === "Set"
|
|
24
|
+
) {
|
|
25
|
+
context.report({ node, messageId: "preferUnique" });
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Compiler macros must declare their shape with a runtime object argument, e.g.
|
|
2
|
+
// defineProps({ ... }), rather than the type-only generic form defineProps<{ ... }>().
|
|
3
|
+
// A type-only call has no runtime object, so it is reported; defineModel<T>({ ... })
|
|
4
|
+
// (a type annotation plus the object) is fine because the object shape is present.
|
|
5
|
+
|
|
6
|
+
import { isVueFile } from "../utils/vue.mjs";
|
|
7
|
+
|
|
8
|
+
const MACROS = new Set(["defineProps", "defineModel", "defineEmits", "defineExpose"]);
|
|
9
|
+
|
|
10
|
+
function hasObjectArgument(node) {
|
|
11
|
+
return node.arguments.some((argument) => argument.type === "ObjectExpression");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
meta: {
|
|
16
|
+
type: "suggestion",
|
|
17
|
+
docs: {
|
|
18
|
+
description: "define* macros must declare their shape with a runtime object, not the type-only form",
|
|
19
|
+
},
|
|
20
|
+
messages: {
|
|
21
|
+
requireObject:
|
|
22
|
+
"'{{macro}}' should declare its shape with a runtime object, e.g. {{macro}}({ ... }), not the type-only form.",
|
|
23
|
+
},
|
|
24
|
+
schema: [],
|
|
25
|
+
},
|
|
26
|
+
create(context) {
|
|
27
|
+
if (!isVueFile(context)) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
CallExpression(node) {
|
|
33
|
+
if (node.callee.type !== "Identifier" || !MACROS.has(node.callee.name)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!hasObjectArgument(node)) {
|
|
38
|
+
context.report({
|
|
39
|
+
node: node.callee,
|
|
40
|
+
messageId: "requireObject",
|
|
41
|
+
data: {
|
|
42
|
+
macro: node.callee.name,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Enforce script-setup section order:
|
|
2
|
+
// import → props → model → emit → composable → state → computed → watch → method
|
|
3
|
+
// → lifecycle → expose.
|
|
4
|
+
//
|
|
5
|
+
// The compiler macros have a fixed order of their own: defineProps, then
|
|
6
|
+
// defineModel, then defineEmits at the top, with defineExpose always last.
|
|
7
|
+
|
|
8
|
+
import { getCallName } from "../utils/ast.mjs";
|
|
9
|
+
import { isVueFile } from "../utils/vue.mjs";
|
|
10
|
+
|
|
11
|
+
const ORDER = [
|
|
12
|
+
"import",
|
|
13
|
+
"props",
|
|
14
|
+
"model",
|
|
15
|
+
"emit",
|
|
16
|
+
"composable",
|
|
17
|
+
"state",
|
|
18
|
+
"computed",
|
|
19
|
+
"watch",
|
|
20
|
+
"method",
|
|
21
|
+
"lifecycle",
|
|
22
|
+
"expose",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const WATCH_CALLS = new Set(["watch", "watchEffect", "watchDebounced"]);
|
|
26
|
+
|
|
27
|
+
const LIFECYCLE_CALLS = new Set(["onMounted", "onUnmounted", "onBeforeMount", "onBeforeUnmount", "onUpdated"]);
|
|
28
|
+
|
|
29
|
+
const STATE_CALLS = new Set(["ref", "reactive", "shallowRef", "shallowReactive"]);
|
|
30
|
+
|
|
31
|
+
// Compiler macros → their section. defineExpose is last; the rest sit at the top.
|
|
32
|
+
const MACRO_SECTIONS = {
|
|
33
|
+
defineProps: "props",
|
|
34
|
+
defineModel: "model",
|
|
35
|
+
defineEmits: "emit",
|
|
36
|
+
defineExpose: "expose",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function classify(node) {
|
|
40
|
+
if (node.type === "ImportDeclaration") {
|
|
41
|
+
return "import";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (node.type === "ExpressionStatement") {
|
|
45
|
+
const callName = getCallName(node.expression);
|
|
46
|
+
|
|
47
|
+
if (callName && MACRO_SECTIONS[callName]) {
|
|
48
|
+
return MACRO_SECTIONS[callName];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (callName && WATCH_CALLS.has(callName)) {
|
|
52
|
+
return "watch";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (callName && LIFECYCLE_CALLS.has(callName)) {
|
|
56
|
+
return "lifecycle";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (node.type === "VariableDeclaration") {
|
|
63
|
+
for (const declarator of node.declarations) {
|
|
64
|
+
if (!declarator.init) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// const handler = () => {} | function () {}
|
|
69
|
+
if (declarator.init.type === "ArrowFunctionExpression" || declarator.init.type === "FunctionExpression") {
|
|
70
|
+
return "method";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const callName = getCallName(declarator.init);
|
|
74
|
+
if (!callName) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (MACRO_SECTIONS[callName]) {
|
|
79
|
+
return MACRO_SECTIONS[callName];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (/^use[A-Z]/.test(callName)) {
|
|
83
|
+
return "composable";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Empty refs (e.g. `const el = ref()`) are template refs — they must sit next
|
|
87
|
+
// to the composable that consumes them, so they are exempt from state ordering.
|
|
88
|
+
if (STATE_CALLS.has(callName) && declarator.init.arguments.length > 0) {
|
|
89
|
+
return "state";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (callName === "computed") {
|
|
93
|
+
return "computed";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (WATCH_CALLS.has(callName)) {
|
|
97
|
+
return "watch";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (LIFECYCLE_CALLS.has(callName)) {
|
|
101
|
+
return "lifecycle";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (node.type === "FunctionDeclaration") {
|
|
109
|
+
return "method";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default {
|
|
116
|
+
meta: {
|
|
117
|
+
type: "suggestion",
|
|
118
|
+
docs: {
|
|
119
|
+
description:
|
|
120
|
+
"enforce script setup section order (import → props → model → emit → composable → state → computed → watch → method → lifecycle → expose)",
|
|
121
|
+
},
|
|
122
|
+
messages: {
|
|
123
|
+
outOfOrder: "'{{current}}' section should come after '{{previous}}' section, not before.",
|
|
124
|
+
},
|
|
125
|
+
schema: [],
|
|
126
|
+
},
|
|
127
|
+
create(context) {
|
|
128
|
+
if (!isVueFile(context)) {
|
|
129
|
+
return {};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let lastSeenIndex = -1;
|
|
133
|
+
let lastSeenCategory = null;
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
"Program > *"(node) {
|
|
137
|
+
const category = classify(node);
|
|
138
|
+
if (!category) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const categoryIndex = ORDER.indexOf(category);
|
|
143
|
+
if (categoryIndex < lastSeenIndex) {
|
|
144
|
+
context.report({
|
|
145
|
+
node,
|
|
146
|
+
messageId: "outOfOrder",
|
|
147
|
+
data: {
|
|
148
|
+
current: category,
|
|
149
|
+
previous: lastSeenCategory,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
} else if (categoryIndex > lastSeenIndex) {
|
|
153
|
+
lastSeenIndex = categoryIndex;
|
|
154
|
+
lastSeenCategory = category;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// createStore config keys must follow name → model → view → action → compose → lazy.
|
|
2
|
+
|
|
3
|
+
import { SECTION_ORDER, getCreateStoreConfig, getKeyedProperties, isCreateStoreCall } from "../utils/store.mjs";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "suggestion",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "createStore config keys must follow name → model → view → action → compose → lazy",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
outOfOrder: "createStore '{{current}}' should come before '{{previous}}'.",
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
CallExpression(node) {
|
|
19
|
+
if (!isCreateStoreCall(node)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const config = getCreateStoreConfig(node);
|
|
24
|
+
if (!config) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let lastIndex = -1;
|
|
29
|
+
let lastKey = null;
|
|
30
|
+
|
|
31
|
+
for (const property of getKeyedProperties(config)) {
|
|
32
|
+
const index = SECTION_ORDER.indexOf(property.key.name);
|
|
33
|
+
if (index === -1) {
|
|
34
|
+
// Unknown keys are handled by store-no-unknown-key.
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (index < lastIndex) {
|
|
39
|
+
context.report({
|
|
40
|
+
node: property,
|
|
41
|
+
messageId: "outOfOrder",
|
|
42
|
+
data: {
|
|
43
|
+
current: property.key.name,
|
|
44
|
+
previous: lastKey,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
} else {
|
|
48
|
+
lastIndex = index;
|
|
49
|
+
lastKey = property.key.name;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// A commit-options object pairs `model` + `mode`; `mode` must use a ModelMode
|
|
2
|
+
// enum (ModelManyMode.SET, ...) rather than a raw string literal.
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
meta: {
|
|
6
|
+
type: "suggestion",
|
|
7
|
+
docs: {
|
|
8
|
+
description: "store action 'mode' must use a ModelMode enum, not a string literal",
|
|
9
|
+
},
|
|
10
|
+
messages: {
|
|
11
|
+
useEnum: "Use a ModelManyMode/ModelOneMode enum for 'mode' instead of the string literal '{{value}}'.",
|
|
12
|
+
},
|
|
13
|
+
schema: [],
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
return {
|
|
17
|
+
ObjectExpression(node) {
|
|
18
|
+
const properties = node.properties.filter(
|
|
19
|
+
(property) => property.type === "Property" && property.key.type === "Identifier",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const modeProperty = properties.find((property) => property.key.name === "mode");
|
|
23
|
+
const hasModel = properties.some((property) => property.key.name === "model");
|
|
24
|
+
if (!modeProperty || !hasModel) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const value = modeProperty.value;
|
|
29
|
+
if (value.type === "Literal" && typeof value.value === "string") {
|
|
30
|
+
context.report({
|
|
31
|
+
node: value,
|
|
32
|
+
messageId: "useEnum",
|
|
33
|
+
data: {
|
|
34
|
+
value: value.value,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// createStore `name` should match the store variable (accountStore → "account").
|
|
2
|
+
|
|
3
|
+
import { findProperty, getCreateStoreConfig, isCreateStoreCall } from "../utils/store.mjs";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "suggestion",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "createStore name should match the store variable (accountStore → 'account')",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
mismatch: "createStore name '{{name}}' should match the store variable, expected '{{expected}}'.",
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
CallExpression(node) {
|
|
19
|
+
if (!isCreateStoreCall(node)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const parent = node.parent;
|
|
24
|
+
if (!parent || parent.type !== "VariableDeclarator" || parent.id.type !== "Identifier") {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const config = getCreateStoreConfig(node);
|
|
29
|
+
if (!config) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const nameProperty = findProperty(config, "name");
|
|
34
|
+
if (
|
|
35
|
+
!nameProperty ||
|
|
36
|
+
nameProperty.value.type !== "Literal" ||
|
|
37
|
+
typeof nameProperty.value.value !== "string"
|
|
38
|
+
) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const expected = parent.id.name.replace(/Store$/, "");
|
|
43
|
+
if (nameProperty.value.value !== expected) {
|
|
44
|
+
context.report({
|
|
45
|
+
node: nameProperty.value,
|
|
46
|
+
messageId: "mismatch",
|
|
47
|
+
data: {
|
|
48
|
+
name: nameProperty.value.value,
|
|
49
|
+
expected,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// createStore config must not contain unknown keys.
|
|
2
|
+
|
|
3
|
+
import { ALLOWED_KEYS, getCreateStoreConfig, getKeyedProperties, isCreateStoreCall } from "../utils/store.mjs";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "createStore config must not contain unknown keys",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
unknown: "Unknown createStore key '{{key}}'. Allowed: name, model, view, action, compose, lazy.",
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
CallExpression(node) {
|
|
19
|
+
if (!isCreateStoreCall(node)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const config = getCreateStoreConfig(node);
|
|
24
|
+
if (!config) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const property of getKeyedProperties(config)) {
|
|
29
|
+
if (!ALLOWED_KEYS.has(property.key.name)) {
|
|
30
|
+
context.report({
|
|
31
|
+
node: property.key,
|
|
32
|
+
messageId: "unknown",
|
|
33
|
+
data: {
|
|
34
|
+
key: property.key.name,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// createStore model/view/action/compose sections must be factory functions.
|
|
2
|
+
|
|
3
|
+
import { FUNCTION_SECTIONS, getCreateStoreConfig, getKeyedProperties, isCreateStoreCall } from "../utils/store.mjs";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "createStore model/view/action/compose sections must be factory functions",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
notFunction: "createStore '{{section}}' must be a factory function, e.g. ({{section}}) => ({ ... }).",
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
CallExpression(node) {
|
|
19
|
+
if (!isCreateStoreCall(node)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const config = getCreateStoreConfig(node);
|
|
24
|
+
if (!config) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const property of getKeyedProperties(config)) {
|
|
29
|
+
if (!FUNCTION_SECTIONS.has(property.key.name)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const value = property.value;
|
|
34
|
+
if (value.type !== "ArrowFunctionExpression" && value.type !== "FunctionExpression") {
|
|
35
|
+
context.report({
|
|
36
|
+
node: property,
|
|
37
|
+
messageId: "notFunction",
|
|
38
|
+
data: {
|
|
39
|
+
section: property.key.name,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
};
|