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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-crisp",
3
- "version": "1.4.19",
3
+ "version": "1.4.21",
4
4
  "description": "Custom ESLint Rules for Crisp",
5
5
  "author": "Crisp IM SAS",
6
6
  "main": "index.js",
@@ -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: null, // This rule is not auto-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 updateCurrentGroupComment(node) {
106
- const comments = context.getSourceCode().getCommentsBefore(node);
106
+ function findGroupComment(node) {
107
+ const comments = sourceCode.getCommentsBefore(node);
107
108
 
108
- if (comments.length > 0) {
109
- for (let i = comments.length - 1; i >= 0; i--) {
110
- if (isIgnoreComment(comments[i])) {
111
- continue;
112
- }
109
+ for (let i = comments.length - 1; i >= 0; i--) {
110
+ if (isIgnoreComment(comments[i])) {
111
+ continue;
112
+ }
113
113
 
114
- if (isGroupComment(comments[i])) {
115
- currentGroupComment = comments[i];
114
+ if (isGroupComment(comments[i])) {
115
+ return comments[i];
116
+ }
117
+ }
116
118
 
117
- break;
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
- // Get comment for current group
132
- updateCurrentGroupComment(node);
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
+ }