eslint-plugin-crisp 1.4.19 → 1.4.21
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/README.md +1 -0
- package/index.js +2 -0
- package/package.json +1 -1
- package/recommended-vue.js +1 -0
- package/rules/import-group-comment.js +72 -16
- package/rules/vue-no-name-option.js +108 -0
package/README.md
CHANGED
|
@@ -226,6 +226,7 @@ Each item has emojis denoting:
|
|
|
226
226
|
| [crisp/vue-header-check](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/vue-header-check.js) | Ensures `script`, `template` and `style` tags start with corresponding comment block | | 🟢 |
|
|
227
227
|
| [crisp/vue-html-indent](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/vue-html-indent.js) | Enforces consistent indentation in `template` (supports for Pug) | | 🟢 |
|
|
228
228
|
| [crisp/vue-html-quotes](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/vue-html-quotes.js) | Enforces HTML attributes to be enclosed with double quotes | | 🟢 |
|
|
229
|
+
| [crisp/vue-no-name-option](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/vue-no-name-option.js) | Disallows explicit component names in Vue single-file components | | 🟢 |
|
|
229
230
|
| [crisp/vue-no-regex-data](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/vue-no-regex-data.js) | Disallows regular expressions to be declared in Vue data object | | 🟢 |
|
|
230
231
|
| [crisp/vue-props-declaration-line-break](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/vue-props-declaration-line-break.js) | Enforces line break between type and default function in prop definition | | 🟢 |
|
|
231
232
|
| [crisp/vue-props-declaration-multiline](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/vue-props-declaration-multiline.js) | Enforces props declarations to be multiline | | 🟢 |
|
package/index.js
CHANGED
|
@@ -52,6 +52,7 @@ import ruleVueHtmlIndent from "./rules/vue-html-indent.js";
|
|
|
52
52
|
import ruleVueHtmlQuotes from "./rules/vue-html-quotes.js";
|
|
53
53
|
import ruleVueMethodsSeparator from "./rules/vue-methods-separator.js";
|
|
54
54
|
import ruleVueNameProp from "./rules/vue-name-prop.js";
|
|
55
|
+
import ruleVueNoNameOption from "./rules/vue-no-name-option.js";
|
|
55
56
|
import ruleVueNoRegexData from "./rules/vue-no-regex-data.js";
|
|
56
57
|
import ruleVuePropsDeclarationLineBreak from "./rules/vue-props-declaration-line-break.js";
|
|
57
58
|
import ruleVuePropsDeclarationMultiline from "./rules/vue-props-declaration-multiline.js";
|
|
@@ -122,6 +123,7 @@ const plugin = {
|
|
|
122
123
|
"vue-html-quotes": ruleVueHtmlQuotes,
|
|
123
124
|
"vue-methods-separator": ruleVueMethodsSeparator,
|
|
124
125
|
"vue-name-prop": ruleVueNameProp,
|
|
126
|
+
"vue-no-name-option": ruleVueNoNameOption,
|
|
125
127
|
"vue-no-regex-data": ruleVueNoRegexData,
|
|
126
128
|
"vue-props-declaration-line-break": ruleVuePropsDeclarationLineBreak,
|
|
127
129
|
"vue-props-declaration-multiline": ruleVuePropsDeclarationMultiline,
|
package/package.json
CHANGED
package/recommended-vue.js
CHANGED
|
@@ -391,6 +391,7 @@ export default function configRecommendedVue(pluginCrisp) {
|
|
|
391
391
|
"crisp/vue-html-quotes": "error",
|
|
392
392
|
"crisp/vue-methods-separator": "error",
|
|
393
393
|
"crisp/vue-name-prop": "error",
|
|
394
|
+
"crisp/vue-no-name-option": "error",
|
|
394
395
|
"crisp/vue-no-regex-data": "error",
|
|
395
396
|
"crisp/vue-props-declaration-line-break": "error",
|
|
396
397
|
"crisp/vue-props-declaration-multiline": "error",
|
|
@@ -50,7 +50,7 @@ export default {
|
|
|
50
50
|
meta: {
|
|
51
51
|
type: "suggestion",
|
|
52
52
|
docs: {
|
|
53
|
-
description: "Enforce import statements to be grouped and preceded by a comment",
|
|
53
|
+
description: "Enforce import statements to be grouped and preceded by a comment with no blank line",
|
|
54
54
|
category: "Best Practices",
|
|
55
55
|
recommended: false,
|
|
56
56
|
},
|
|
@@ -62,10 +62,11 @@ export default {
|
|
|
62
62
|
},
|
|
63
63
|
},
|
|
64
64
|
],
|
|
65
|
-
fixable:
|
|
65
|
+
fixable: "whitespace",
|
|
66
66
|
},
|
|
67
67
|
|
|
68
68
|
create(context) {
|
|
69
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
69
70
|
const customGroups = context.options[0] || {}; // Get custom groups from options
|
|
70
71
|
|
|
71
72
|
// Pre-compile custom regexes once instead of on every import
|
|
@@ -102,22 +103,61 @@ export default {
|
|
|
102
103
|
return "NPM";
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
function
|
|
106
|
-
const comments =
|
|
106
|
+
function findGroupComment(node) {
|
|
107
|
+
const comments = sourceCode.getCommentsBefore(node);
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
109
|
+
for (let i = comments.length - 1; i >= 0; i--) {
|
|
110
|
+
if (isIgnoreComment(comments[i])) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
if (isGroupComment(comments[i])) {
|
|
115
|
+
return comments[i];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
116
118
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function hasBlankLineBetween(fromNode, toNode) {
|
|
123
|
+
const startLine = fromNode.loc.end.line;
|
|
124
|
+
const endLine = toNode.loc.start.line;
|
|
125
|
+
|
|
126
|
+
for (let i = startLine; i < endLine - 1; i++) {
|
|
127
|
+
if (sourceCode.lines[i].trim() === "") {
|
|
128
|
+
return true;
|
|
119
129
|
}
|
|
120
130
|
}
|
|
131
|
+
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function checkBlankLineAfterGroupComment(target, groupComment, message) {
|
|
136
|
+
if (!hasBlankLineBetween(groupComment, target)) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
context.report({
|
|
141
|
+
loc: target.loc,
|
|
142
|
+
message,
|
|
143
|
+
|
|
144
|
+
fix(fixer) {
|
|
145
|
+
const textBetween = sourceCode.text.slice(
|
|
146
|
+
groupComment.range[1],
|
|
147
|
+
target.range[0]
|
|
148
|
+
);
|
|
149
|
+
const collapsed = textBetween.replace(/(\n[ \t]*)+\n/g, "\n");
|
|
150
|
+
|
|
151
|
+
if (collapsed === textBetween) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return fixer.replaceTextRange(
|
|
156
|
+
[groupComment.range[1], target.range[0]],
|
|
157
|
+
collapsed
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
121
161
|
}
|
|
122
162
|
|
|
123
163
|
function checkImportGroup(node) {
|
|
@@ -128,8 +168,17 @@ export default {
|
|
|
128
168
|
const expectedGroup = extractGroupFromPath(importPath, filePath);
|
|
129
169
|
const expectedComment = generateExpectedComment(expectedGroup);
|
|
130
170
|
|
|
131
|
-
|
|
132
|
-
|
|
171
|
+
const groupComment = findGroupComment(node);
|
|
172
|
+
|
|
173
|
+
if (groupComment) {
|
|
174
|
+
currentGroupComment = groupComment;
|
|
175
|
+
|
|
176
|
+
checkBlankLineAfterGroupComment(
|
|
177
|
+
node,
|
|
178
|
+
groupComment,
|
|
179
|
+
"Import group comment must be immediately followed by an import (no blank line)."
|
|
180
|
+
);
|
|
181
|
+
}
|
|
133
182
|
|
|
134
183
|
if (!currentGroupComment || !currentGroupComment.value.includes(expectedComment)) {
|
|
135
184
|
context.report({
|
|
@@ -139,7 +188,6 @@ export default {
|
|
|
139
188
|
}
|
|
140
189
|
}
|
|
141
190
|
|
|
142
|
-
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
143
191
|
const allComments = sourceCode.getAllComments();
|
|
144
192
|
const typeComments = allComments.filter(c => isTypeComment(c));
|
|
145
193
|
|
|
@@ -173,7 +221,15 @@ export default {
|
|
|
173
221
|
loc: firstTypeComment.loc,
|
|
174
222
|
message: "Type comments group must be preceded by a '// TYPES' comment",
|
|
175
223
|
});
|
|
224
|
+
|
|
225
|
+
return;
|
|
176
226
|
}
|
|
227
|
+
|
|
228
|
+
checkBlankLineAfterGroupComment(
|
|
229
|
+
firstTypeComment,
|
|
230
|
+
comment,
|
|
231
|
+
"Type group comment must be immediately followed by a type comment (no blank line)."
|
|
232
|
+
);
|
|
177
233
|
}
|
|
178
234
|
|
|
179
235
|
return {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
meta: {
|
|
3
|
+
type: "suggestion",
|
|
4
|
+
docs: {
|
|
5
|
+
description: "disallow explicit names in Vue single-file components",
|
|
6
|
+
category: "Best Practices",
|
|
7
|
+
recommended: false
|
|
8
|
+
},
|
|
9
|
+
fixable: "code",
|
|
10
|
+
schema: []
|
|
11
|
+
},
|
|
12
|
+
|
|
13
|
+
create(context) {
|
|
14
|
+
const fileName = (context.filename || context.getFilename()).replace(/\\/g, "/");
|
|
15
|
+
|
|
16
|
+
if (!fileName.endsWith(".vue")) {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
ExportDefaultDeclaration(node) {
|
|
22
|
+
const componentOptions = getComponentOptions(node.declaration);
|
|
23
|
+
|
|
24
|
+
if (!componentOptions) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
componentOptions.properties.forEach((property) => {
|
|
29
|
+
if (!isNameProperty(property)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
context.report({
|
|
34
|
+
node: property,
|
|
35
|
+
message: "Vue single-file components must rely on their inferred filename.",
|
|
36
|
+
|
|
37
|
+
fix(fixer) {
|
|
38
|
+
return removeProperty(fixer, context, componentOptions, property);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Returns the component options passed to defineComponent()
|
|
48
|
+
function getComponentOptions(node) {
|
|
49
|
+
if (!isDefineComponentCall(node)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const options = node.arguments[0];
|
|
54
|
+
|
|
55
|
+
return options?.type === "ObjectExpression" ? options : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Returns whether the node is a defineComponent() call
|
|
59
|
+
function isDefineComponentCall(node) {
|
|
60
|
+
if (!node || node.type !== "CallExpression") {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (node.callee.type === "Identifier") {
|
|
65
|
+
return node.callee.name === "defineComponent";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
node.callee.type === "MemberExpression" &&
|
|
70
|
+
node.callee.property.type === "Identifier" &&
|
|
71
|
+
node.callee.property.name === "defineComponent"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Returns whether the property is the component name option
|
|
76
|
+
function isNameProperty(property) {
|
|
77
|
+
if (property.type !== "Property") {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (property.key.type === "Identifier") {
|
|
82
|
+
return property.key.name === "name";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return property.key.type === "Literal" && property.key.value === "name";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Removes a property and its adjacent comma
|
|
89
|
+
function removeProperty(fixer, context, options, property) {
|
|
90
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
91
|
+
const nextToken = sourceCode.getTokenAfter(property);
|
|
92
|
+
|
|
93
|
+
if (nextToken?.value === ",") {
|
|
94
|
+
return fixer.removeRange([property.range[0], nextToken.range[1]]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const propertyIndex = options.properties.indexOf(property);
|
|
98
|
+
|
|
99
|
+
if (propertyIndex > 0) {
|
|
100
|
+
const previousToken = sourceCode.getTokenBefore(property);
|
|
101
|
+
|
|
102
|
+
if (previousToken?.value === ",") {
|
|
103
|
+
return fixer.removeRange([previousToken.range[0], property.range[1]]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return fixer.remove(property);
|
|
108
|
+
}
|