@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,53 @@
|
|
|
1
|
+
// createStore sections must use method shorthand — model({ many }) { ... } —
|
|
2
|
+
// not arrow functions or plain function expressions. Whether a section is a
|
|
3
|
+
// function at all is left to store-section-function.
|
|
4
|
+
|
|
5
|
+
import { FUNCTION_SECTIONS, getCreateStoreConfig, getKeyedProperties, isCreateStoreCall } from "../utils/store.mjs";
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "suggestion",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "createStore sections must use method shorthand, not arrow functions",
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
useMethod:
|
|
15
|
+
"createStore '{{section}}' must use method shorthand, e.g. {{section}}() { ... }, not an arrow function.",
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
},
|
|
19
|
+
create(context) {
|
|
20
|
+
return {
|
|
21
|
+
CallExpression(node) {
|
|
22
|
+
if (!isCreateStoreCall(node)) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const config = getCreateStoreConfig(node);
|
|
27
|
+
if (!config) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const property of getKeyedProperties(config)) {
|
|
32
|
+
if (!FUNCTION_SECTIONS.has(property.key.name)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// property.method is only true for method shorthand. Arrow functions
|
|
37
|
+
// and `model: function () {}` are functions but not shorthand.
|
|
38
|
+
const value = property.value;
|
|
39
|
+
const isFunction = value.type === "ArrowFunctionExpression" || value.type === "FunctionExpression";
|
|
40
|
+
if (isFunction && !property.method) {
|
|
41
|
+
context.report({
|
|
42
|
+
node: property.key,
|
|
43
|
+
messageId: "useMethod",
|
|
44
|
+
data: {
|
|
45
|
+
section: property.key.name,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// createStore sections must return a shorthand object of named consts —
|
|
2
|
+
// `const list = many(shape); return { list };` — rather than returning inline
|
|
3
|
+
// expressions like `return { list: many(shape) }`.
|
|
4
|
+
|
|
5
|
+
import { FUNCTION_SECTIONS, getCreateStoreConfig, getKeyedProperties, isCreateStoreCall } from "../utils/store.mjs";
|
|
6
|
+
|
|
7
|
+
// The object(s) a section function returns: an arrow concise body, or the
|
|
8
|
+
// top-level `return { ... }` statements of a block body (returns nested inside
|
|
9
|
+
// inner functions/branches are left alone).
|
|
10
|
+
function getReturnedObjects(fn) {
|
|
11
|
+
const body = fn.body;
|
|
12
|
+
|
|
13
|
+
if (body.type === "ObjectExpression") {
|
|
14
|
+
return [body];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (body.type !== "BlockStatement") {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return body.body
|
|
22
|
+
.filter((statement) => statement.type === "ReturnStatement")
|
|
23
|
+
.map((statement) => statement.argument)
|
|
24
|
+
.filter((argument) => argument && argument.type === "ObjectExpression");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default {
|
|
28
|
+
meta: {
|
|
29
|
+
type: "suggestion",
|
|
30
|
+
docs: {
|
|
31
|
+
description: "createStore sections must return a shorthand object of named consts",
|
|
32
|
+
},
|
|
33
|
+
messages: {
|
|
34
|
+
useShorthand:
|
|
35
|
+
"Assign '{{key}}' to a const and return it as a shorthand property ({ {{key}} }) from '{{section}}'.",
|
|
36
|
+
},
|
|
37
|
+
schema: [],
|
|
38
|
+
},
|
|
39
|
+
create(context) {
|
|
40
|
+
return {
|
|
41
|
+
CallExpression(node) {
|
|
42
|
+
if (!isCreateStoreCall(node)) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const config = getCreateStoreConfig(node);
|
|
47
|
+
if (!config) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const property of getKeyedProperties(config)) {
|
|
52
|
+
if (!FUNCTION_SECTIONS.has(property.key.name)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const value = property.value;
|
|
57
|
+
if (value.type !== "ArrowFunctionExpression" && value.type !== "FunctionExpression") {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const returned of getReturnedObjects(value)) {
|
|
62
|
+
for (const member of returned.properties) {
|
|
63
|
+
if (member.type !== "Property" || member.computed || member.shorthand) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
context.report({
|
|
68
|
+
node: member,
|
|
69
|
+
messageId: "useShorthand",
|
|
70
|
+
data: {
|
|
71
|
+
section: property.key.name,
|
|
72
|
+
key: member.key.type === "Identifier" ? member.key.name : "value",
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// shape() result must be named with a 'Shape' suffix, e.g.
|
|
2
|
+
// export const collectionShape = shape(collectionSchema)
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
meta: {
|
|
6
|
+
type: "problem",
|
|
7
|
+
docs: {
|
|
8
|
+
description: "shape() result must use a 'Shape' suffix",
|
|
9
|
+
},
|
|
10
|
+
messages: {
|
|
11
|
+
invalidName: "shape() assigned to '{{name}}' should use a 'Shape' suffix (e.g. collectionShape).",
|
|
12
|
+
},
|
|
13
|
+
schema: [],
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
return {
|
|
17
|
+
VariableDeclarator(node) {
|
|
18
|
+
if (
|
|
19
|
+
node.init &&
|
|
20
|
+
node.init.type === "CallExpression" &&
|
|
21
|
+
node.init.callee.type === "Identifier" &&
|
|
22
|
+
node.init.callee.name === "shape" &&
|
|
23
|
+
node.id.type === "Identifier" &&
|
|
24
|
+
!/Shape$/.test(node.id.name)
|
|
25
|
+
) {
|
|
26
|
+
context.report({
|
|
27
|
+
node: node.id,
|
|
28
|
+
messageId: "invalidName",
|
|
29
|
+
data: {
|
|
30
|
+
name: node.id.name,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// A store assigned from createStore must use a `Store` suffix.
|
|
2
|
+
|
|
3
|
+
import { isCreateStoreCall } from "../utils/store.mjs";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "store assigned from createStore must use a 'Store' suffix",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
invalidName: "Store assigned to '{{name}}' should use a 'Store' suffix (e.g. accountStore).",
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
CallExpression(node) {
|
|
19
|
+
if (!isCreateStoreCall(node)) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// const accountStore = createStore({ ... })
|
|
24
|
+
const parent = node.parent;
|
|
25
|
+
if (parent && parent.type === "VariableDeclarator" && parent.id.type === "Identifier") {
|
|
26
|
+
if (!/Store$/.test(parent.id.name)) {
|
|
27
|
+
context.report({
|
|
28
|
+
node: parent.id,
|
|
29
|
+
messageId: "invalidName",
|
|
30
|
+
data: {
|
|
31
|
+
name: parent.id.name,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Bare template text should be wrapped in an HTML tag.
|
|
2
|
+
|
|
3
|
+
import { defineTemplateRule } from "../utils/vue.mjs";
|
|
4
|
+
|
|
5
|
+
const TEXT_ELEMENTS = new Set([
|
|
6
|
+
"span",
|
|
7
|
+
"p",
|
|
8
|
+
"h1",
|
|
9
|
+
"h2",
|
|
10
|
+
"h3",
|
|
11
|
+
"h4",
|
|
12
|
+
"h5",
|
|
13
|
+
"h6",
|
|
14
|
+
"a",
|
|
15
|
+
"li",
|
|
16
|
+
"td",
|
|
17
|
+
"th",
|
|
18
|
+
"dt",
|
|
19
|
+
"dd",
|
|
20
|
+
"label",
|
|
21
|
+
"legend",
|
|
22
|
+
"caption",
|
|
23
|
+
"figcaption",
|
|
24
|
+
"summary",
|
|
25
|
+
"em",
|
|
26
|
+
"strong",
|
|
27
|
+
"b",
|
|
28
|
+
"i",
|
|
29
|
+
"u",
|
|
30
|
+
"s",
|
|
31
|
+
"small",
|
|
32
|
+
"mark",
|
|
33
|
+
"q",
|
|
34
|
+
"cite",
|
|
35
|
+
"dfn",
|
|
36
|
+
"abbr",
|
|
37
|
+
"time",
|
|
38
|
+
"code",
|
|
39
|
+
"pre",
|
|
40
|
+
"blockquote",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const TEXT_PATTERN = /^[A-Za-z][A-Za-z .,]*$/;
|
|
44
|
+
|
|
45
|
+
export default defineTemplateRule(
|
|
46
|
+
{
|
|
47
|
+
type: "suggestion",
|
|
48
|
+
docs: {
|
|
49
|
+
description: "text content should be wrapped in an HTML tag",
|
|
50
|
+
},
|
|
51
|
+
messages: {
|
|
52
|
+
wrapText: "Text content '{{text}}' should be wrapped in an HTML tag like <span>.",
|
|
53
|
+
},
|
|
54
|
+
schema: [],
|
|
55
|
+
},
|
|
56
|
+
(context) => ({
|
|
57
|
+
VText(node) {
|
|
58
|
+
const value = node.value.trim();
|
|
59
|
+
if (value.length <= 1) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!TEXT_PATTERN.test(value)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const isWrapped = node.parent.type === "VElement" && TEXT_ELEMENTS.has(node.parent.name);
|
|
68
|
+
if (isWrapped) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const label = value.length > 40 ? value.substring(0, 40) + "..." : value;
|
|
73
|
+
context.report({
|
|
74
|
+
node,
|
|
75
|
+
messageId: "wrapText",
|
|
76
|
+
data: { text: label },
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Prefer VueUse useClipboard over navigator.clipboard.
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: "suggestion",
|
|
6
|
+
docs: {
|
|
7
|
+
description: "prefer VueUse useClipboard over navigator.clipboard",
|
|
8
|
+
},
|
|
9
|
+
messages: {
|
|
10
|
+
preferClipboard: "Use VueUse 'useClipboard()' instead of navigator.clipboard.",
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
},
|
|
14
|
+
create(context) {
|
|
15
|
+
return {
|
|
16
|
+
MemberExpression(node) {
|
|
17
|
+
if (
|
|
18
|
+
!node.computed &&
|
|
19
|
+
node.object.type === "Identifier" &&
|
|
20
|
+
node.object.name === "navigator" &&
|
|
21
|
+
node.property.type === "Identifier" &&
|
|
22
|
+
node.property.name === "clipboard"
|
|
23
|
+
) {
|
|
24
|
+
context.report({ node, messageId: "preferClipboard" });
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Prefer VueUse composables over raw DOM/window method calls.
|
|
2
|
+
|
|
3
|
+
// `<any>.<method>(...)`
|
|
4
|
+
const MEMBER_CALLS = {
|
|
5
|
+
addEventListener: "useEventListener",
|
|
6
|
+
matchMedia: "useMediaQuery",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "suggestion",
|
|
12
|
+
docs: {
|
|
13
|
+
description: "prefer VueUse composables over raw DOM/window method calls",
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
preferMemberCall: "Use VueUse '{{helper}}()' instead of '{{native}}()' for auto-cleanup.",
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
return {
|
|
22
|
+
CallExpression(node) {
|
|
23
|
+
const callee = node.callee;
|
|
24
|
+
if (callee.type !== "MemberExpression" || callee.computed || callee.property.type !== "Identifier") {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const helper = MEMBER_CALLS[callee.property.name];
|
|
29
|
+
if (helper) {
|
|
30
|
+
context.report({
|
|
31
|
+
node: callee.property,
|
|
32
|
+
messageId: "preferMemberCall",
|
|
33
|
+
data: { helper, native: callee.property.name },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Prefer VueUse observer composables over raw observers.
|
|
2
|
+
|
|
3
|
+
// `new <Observer>(...)`
|
|
4
|
+
const CONSTRUCTORS = {
|
|
5
|
+
ResizeObserver: "useResizeObserver",
|
|
6
|
+
IntersectionObserver: "useIntersectionObserver",
|
|
7
|
+
MutationObserver: "useMutationObserver",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
meta: {
|
|
12
|
+
type: "suggestion",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "prefer VueUse observer composables over raw observers",
|
|
15
|
+
},
|
|
16
|
+
messages: {
|
|
17
|
+
preferObserver: "Use VueUse '{{helper}}()' instead of 'new {{native}}()' for auto-cleanup.",
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
},
|
|
21
|
+
create(context) {
|
|
22
|
+
return {
|
|
23
|
+
NewExpression(node) {
|
|
24
|
+
if (node.callee.type === "Identifier" && CONSTRUCTORS[node.callee.name]) {
|
|
25
|
+
context.report({
|
|
26
|
+
node: node.callee,
|
|
27
|
+
messageId: "preferObserver",
|
|
28
|
+
data: { helper: CONSTRUCTORS[node.callee.name], native: node.callee.name },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Prefer VueUse route composables (useRouteQuery / useRouteParams / useRouteHash)
|
|
2
|
+
// over reading query / params / hash from a raw useRoute() result. Other route
|
|
3
|
+
// properties (name, path, meta, ...) have no VueUse equivalent and are left alone.
|
|
4
|
+
|
|
5
|
+
// route property → VueUse composable
|
|
6
|
+
const ROUTE_PROPERTIES = {
|
|
7
|
+
query: "useRouteQuery",
|
|
8
|
+
params: "useRouteParams",
|
|
9
|
+
hash: "useRouteHash",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function isUseRouteCall(node) {
|
|
13
|
+
return node.type === "CallExpression" && node.callee.type === "Identifier" && node.callee.name === "useRoute";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
meta: {
|
|
18
|
+
type: "suggestion",
|
|
19
|
+
docs: {
|
|
20
|
+
description: "prefer VueUse route composables over raw useRoute() query/params/hash access",
|
|
21
|
+
},
|
|
22
|
+
messages: {
|
|
23
|
+
preferRoute: "Use VueUse '{{helper}}()' instead of reading '{{property}}' from useRoute().",
|
|
24
|
+
},
|
|
25
|
+
schema: [],
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
// Names bound to a useRoute() result, e.g. `const route = useRoute()`.
|
|
29
|
+
const routeBindings = new Set();
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
VariableDeclarator(node) {
|
|
33
|
+
if (node.init && isUseRouteCall(node.init) && node.id.type === "Identifier") {
|
|
34
|
+
routeBindings.add(node.id.name);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
MemberExpression(node) {
|
|
38
|
+
if (node.computed || node.property.type !== "Identifier") {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const helper = ROUTE_PROPERTIES[node.property.name];
|
|
43
|
+
if (!helper) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// useRoute().query | const route = useRoute(); route.query
|
|
48
|
+
const object = node.object;
|
|
49
|
+
const fromRoute =
|
|
50
|
+
isUseRouteCall(object) || (object.type === "Identifier" && routeBindings.has(object.name));
|
|
51
|
+
if (fromRoute) {
|
|
52
|
+
context.report({
|
|
53
|
+
node,
|
|
54
|
+
messageId: "preferRoute",
|
|
55
|
+
data: {
|
|
56
|
+
helper,
|
|
57
|
+
property: node.property.name,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Prefer VueUse useLocalStorage/useSessionStorage over raw Web Storage.
|
|
2
|
+
|
|
3
|
+
// Bare/qualified identifier access, e.g. localStorage / window.localStorage
|
|
4
|
+
const MEMBER_ACCESS = {
|
|
5
|
+
localStorage: "useLocalStorage",
|
|
6
|
+
sessionStorage: "useSessionStorage",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "suggestion",
|
|
12
|
+
docs: {
|
|
13
|
+
description: "prefer VueUse useLocalStorage/useSessionStorage over raw Web Storage",
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
preferStorage: "Use VueUse '{{helper}}()' instead of accessing {{native}} directly.",
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
return {
|
|
22
|
+
MemberExpression(node) {
|
|
23
|
+
// localStorage.getItem(...)
|
|
24
|
+
if (node.object.type === "Identifier" && MEMBER_ACCESS[node.object.name]) {
|
|
25
|
+
context.report({
|
|
26
|
+
node: node.object,
|
|
27
|
+
messageId: "preferStorage",
|
|
28
|
+
data: { helper: MEMBER_ACCESS[node.object.name], native: node.object.name },
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// window.localStorage / globalThis.sessionStorage
|
|
34
|
+
if (!node.computed && node.property.type === "Identifier" && MEMBER_ACCESS[node.property.name]) {
|
|
35
|
+
context.report({
|
|
36
|
+
node: node.property,
|
|
37
|
+
messageId: "preferStorage",
|
|
38
|
+
data: { helper: MEMBER_ACCESS[node.property.name], native: node.property.name },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Prefer VueUse timer composables over native timers.
|
|
2
|
+
|
|
3
|
+
// `<global>(...)`
|
|
4
|
+
const GLOBAL_CALLS = {
|
|
5
|
+
setInterval: "useIntervalFn",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
meta: {
|
|
10
|
+
type: "suggestion",
|
|
11
|
+
docs: {
|
|
12
|
+
description: "prefer VueUse timer composables over native timers",
|
|
13
|
+
},
|
|
14
|
+
messages: {
|
|
15
|
+
preferTimer: "Use VueUse '{{helper}}()' instead of '{{native}}()' for auto-cleanup.",
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
},
|
|
19
|
+
create(context) {
|
|
20
|
+
return {
|
|
21
|
+
CallExpression(node) {
|
|
22
|
+
if (node.callee.type === "Identifier" && GLOBAL_CALLS[node.callee.name]) {
|
|
23
|
+
context.report({
|
|
24
|
+
node: node.callee,
|
|
25
|
+
messageId: "preferTimer",
|
|
26
|
+
data: { helper: GLOBAL_CALLS[node.callee.name], native: node.callee.name },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Generic AST helpers shared across rules.
|
|
2
|
+
|
|
3
|
+
// `<Object>.<prop>` for a non-computed static member expression → "Object.prop",
|
|
4
|
+
// otherwise null.
|
|
5
|
+
export function staticMemberName(node) {
|
|
6
|
+
if (!node || node.type !== "MemberExpression" || node.computed) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (node.object.type !== "Identifier" || node.property.type !== "Identifier") {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return `${node.object.name}.${node.property.name}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Callee name for `foo(...)` where the callee is a bare identifier, otherwise null.
|
|
18
|
+
export function getCallName(node) {
|
|
19
|
+
if (node && node.type === "CallExpression" && node.callee.type === "Identifier") {
|
|
20
|
+
return node.callee.name;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Builds the `meta.docs.url` for a rule from its name. Kept in one place so the
|
|
2
|
+
// URL scheme matches the docs/rules/<name>.md layout.
|
|
3
|
+
|
|
4
|
+
const DOCS_BASE = "https://github.com/diphyx/eslint-plugin/blob/main/docs/rules";
|
|
5
|
+
|
|
6
|
+
export function docsUrl(name) {
|
|
7
|
+
return `${DOCS_BASE}/${name}.md`;
|
|
8
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Helpers for the Harlemify createStore rules.
|
|
2
|
+
//
|
|
3
|
+
// Grounded in harlemify's StoreConfig (src/runtime/core/types/store.ts):
|
|
4
|
+
// { name: string; model: fn; view: fn; action: fn; compose?: fn; lazy?: boolean }
|
|
5
|
+
// name/model/view/action are required; model/view/action/compose are factory functions.
|
|
6
|
+
|
|
7
|
+
export const SECTION_ORDER = ["name", "model", "view", "action", "compose", "lazy"];
|
|
8
|
+
|
|
9
|
+
export const ALLOWED_KEYS = new Set(SECTION_ORDER);
|
|
10
|
+
|
|
11
|
+
export const FUNCTION_SECTIONS = new Set(["model", "view", "action", "compose"]);
|
|
12
|
+
|
|
13
|
+
export function isCreateStoreCall(node) {
|
|
14
|
+
return node.callee && node.callee.type === "Identifier" && node.callee.name === "createStore";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getCreateStoreConfig(node) {
|
|
18
|
+
return node.arguments.find((argument) => argument.type === "ObjectExpression") || null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getKeyedProperties(objectExpression) {
|
|
22
|
+
return objectExpression.properties.filter(
|
|
23
|
+
(property) => property.type === "Property" && property.key.type === "Identifier",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function findProperty(objectExpression, key) {
|
|
28
|
+
return getKeyedProperties(objectExpression).find((property) => property.key.name === key) || null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Factory for "config must include <key>" rules.
|
|
32
|
+
export function requireKey(key, message) {
|
|
33
|
+
return {
|
|
34
|
+
meta: {
|
|
35
|
+
type: "problem",
|
|
36
|
+
docs: {
|
|
37
|
+
description: `createStore config must include '${key}'`,
|
|
38
|
+
},
|
|
39
|
+
messages: {
|
|
40
|
+
missing: message,
|
|
41
|
+
},
|
|
42
|
+
schema: [],
|
|
43
|
+
},
|
|
44
|
+
create(context) {
|
|
45
|
+
return {
|
|
46
|
+
CallExpression(node) {
|
|
47
|
+
if (!isCreateStoreCall(node)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const config = getCreateStoreConfig(node);
|
|
52
|
+
if (!config) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!findProperty(config, key)) {
|
|
57
|
+
context.report({
|
|
58
|
+
node,
|
|
59
|
+
messageId: "missing",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|