eslint-plugin-crisp 1.4.9 → 1.4.11

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
@@ -115,6 +115,7 @@ Each item has emojis denoting:
115
115
  | Name | Description | 🟠 | 🟢 | 🟣 |
116
116
  | :- | :- | :- | :- | :- |
117
117
  | [crisp/align-comments](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/align-comments.js) | Enforces alignment of comments compared to the previous line (the `indent` rule doesn't check this case) | 🟠 | 🟢 | 🟣 |
118
+ | [crisp/consecutive-line-comments](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/consecutive-line-comments.js) | Enforces consecutive single-line comments to form a single multi-line continuation block | 🟠 | 🟢 | 🟣 |
118
119
  | [crisp/align-consecutive-class-assignements](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/align-consecutive-class-assignements.js) | Enforces alignment of consecutive assignment statements in a class constructor | 🟠 |
119
120
  | [crisp/align-one-var](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/align-one-var.js) | Enforces alignment of variables in 'one-var' statements | 🟠 |
120
121
  | [crisp/align-requires](https://github.com/crisp-oss/eslint-plugin-crisp/blob/master/rules/align-requires.js) | Enforces alignment of require statements | 🟠 |
package/index.js CHANGED
@@ -6,6 +6,7 @@ import ruleAlignComments from "./rules/align-comments.js";
6
6
  import ruleAlignConsecutiveClassAssignements from "./rules/align-consecutive-class-assignements.js";
7
7
  import ruleAlignOneVar from "./rules/align-one-var.js";
8
8
  import ruleAlignRequires from "./rules/align-requires.js";
9
+ import ruleConsecutiveLineComments from "./rules/consecutive-line-comments.js";
9
10
  import ruleConst from "./rules/const.js";
10
11
  import ruleConstructorVariables from "./rules/constructor-variables.js";
11
12
  import ruleEnforceOptional from "./rules/enforce-optional.js";
@@ -40,6 +41,7 @@ import ruleVariableNames from "./rules/variable-names.js";
40
41
  import ruleVueAttributeComma from "./rules/vue-attribute-comma.js";
41
42
  import ruleVueAttributeLinebreak from "./rules/vue-attribute-linebreak.js";
42
43
  import ruleVueComputedOrder from "./rules/vue-computed-order.js";
44
+ import ruleVueDataComment from "./rules/vue-data-comment.js";
43
45
  import ruleVueEmitsOrder from "./rules/vue-emits-order.js";
44
46
  import ruleVueHeaderCheck from "./rules/vue-header-check.js";
45
47
  import ruleVueHtmlIndent from "./rules/vue-html-indent.js";
@@ -70,6 +72,7 @@ const plugin = {
70
72
  "align-consecutive-class-assignements": ruleAlignConsecutiveClassAssignements,
71
73
  "align-one-var": ruleAlignOneVar,
72
74
  "align-requires": ruleAlignRequires,
75
+ "consecutive-line-comments": ruleConsecutiveLineComments,
73
76
  "const": ruleConst,
74
77
  "constructor-variables": ruleConstructorVariables,
75
78
  "enforce-optional": ruleEnforceOptional,
@@ -104,6 +107,7 @@ const plugin = {
104
107
  "vue-attribute-comma": ruleVueAttributeComma,
105
108
  "vue-attribute-linebreak": ruleVueAttributeLinebreak,
106
109
  "vue-computed-order": ruleVueComputedOrder,
110
+ "vue-data-comment": ruleVueDataComment,
107
111
  "vue-emits-order": ruleVueEmitsOrder,
108
112
  "vue-header-check": ruleVueHeaderCheck,
109
113
  "vue-html-indent": ruleVueHtmlIndent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-crisp",
3
- "version": "1.4.9",
3
+ "version": "1.4.11",
4
4
  "description": "Custom ESLint Rules for Crisp",
5
5
  "author": "Crisp IM SAS",
6
6
  "main": "index.js",
package/recommended-ts.js CHANGED
@@ -151,6 +151,7 @@ export default function configRecommendedTS(pluginCrisp) {
151
151
 
152
152
  // Crisp JS rules
153
153
  "crisp/align-comments": "error",
154
+ "crisp/consecutive-line-comments": "error",
154
155
  "crisp/enforce-optional": "error",
155
156
  "crisp/header-check": "error",
156
157
  "crisp/header-comments-check": "error",
@@ -162,6 +162,7 @@ export default function configRecommendedVue(pluginCrisp) {
162
162
 
163
163
  // Crisp JS rules
164
164
  "crisp/align-comments": "error",
165
+ "crisp/consecutive-line-comments": "error",
165
166
  "crisp/enforce-optional": "error",
166
167
  "crisp/header-check": "error",
167
168
  "crisp/header-comments-check": "error",
@@ -374,6 +375,7 @@ export default function configRecommendedVue(pluginCrisp) {
374
375
  "crisp/vue-attribute-comma": "error",
375
376
  "crisp/vue-attribute-linebreak": "error",
376
377
  "crisp/vue-computed-order": "error",
378
+ "crisp/vue-data-comment": "error",
377
379
  "crisp/vue-emits-order": "error",
378
380
  "crisp/vue-header-check": "error",
379
381
  "crisp/vue-html-indent": "error",
package/recommended.js CHANGED
@@ -156,6 +156,7 @@ export default function configRecommended(pluginCrisp) {
156
156
 
157
157
  // Crisp JS rules
158
158
  "crisp/align-comments": "error",
159
+ "crisp/consecutive-line-comments": "error",
159
160
  "crisp/align-consecutive-class-assignements": "error",
160
161
  "crisp/align-one-var": "error",
161
162
  "crisp/align-requires": "error",
@@ -0,0 +1,250 @@
1
+ const DIRECTIVE_REGEX = /^(eslint|ts-|@ts|prettier-|global |globals |istanbul|c8 |v8 |jshint|jslint)/;
2
+
3
+ // A short capitalized label followed by a colon (e.g. 'Notice:', 'Important:',
4
+ // 'Source:') introduces a new logical comment, so the previous comment does not
5
+ // need to be merged into a continuation block with it. The label is kept short
6
+ // (up to ~40 chars) to avoid matching a regular sentence that contains a colon
7
+ const BOUNDARY_REGEX = /^[A-Z][^:]{0,39}:/;
8
+
9
+ function getCore(comment) {
10
+ // Strip the trailing continuation backslash and surrounding whitespace
11
+ return comment.value.replace(/\\\s*$/, "").trim();
12
+ }
13
+
14
+ function isDirective(core) {
15
+ return DIRECTIVE_REGEX.test(core);
16
+ }
17
+
18
+ function isBoundary(core) {
19
+ return BOUNDARY_REGEX.test(core);
20
+ }
21
+
22
+ function isContinuationLine(comment) {
23
+ // Continuation lines use at least three spaces after '//'
24
+ return /^ {3,}/.test(comment.value);
25
+ }
26
+
27
+ function isBoundaryComment(comment) {
28
+ // Labels such as 'Notice:' only split groups on first lines, not mid-block
29
+ // prose like '// In other words: …'
30
+ return !isContinuationLine(comment) && isBoundary(getCore(comment));
31
+ }
32
+
33
+ function endsWithContinuation(comment) {
34
+ return /\\\s*$/.test(comment.value);
35
+ }
36
+
37
+ function isBlankSeparatorLine(comment) {
38
+ // A line that only contains '\' (e.g. '// \' or '// \') is a visual separator
39
+ return (
40
+ comment.type === "Line" &&
41
+ endsWithContinuation(comment) &&
42
+ getCore(comment).length === 0
43
+ );
44
+ }
45
+
46
+ export default {
47
+ meta: {
48
+ type: "layout",
49
+ docs: {
50
+ description:
51
+ "Enforce that consecutive single-line comments are written as a " +
52
+ "single multi-line continuation block.",
53
+ category: "Stylistic Issues",
54
+ recommended: false,
55
+ },
56
+ fixable: "whitespace",
57
+ schema: [],
58
+ messages: {
59
+ format:
60
+ "Consecutive single-line comments must form a continuation block: " +
61
+ "each line ends with '\\' (except the last) and continuation lines " +
62
+ "are indented with at least three spaces after '//'.",
63
+ },
64
+ },
65
+
66
+ create(context) {
67
+ const sourceCode = context.sourceCode || context.getSourceCode();
68
+
69
+ function isStandalone(comment) {
70
+ const lineText = sourceCode.lines[comment.loc.start.line - 1] || "";
71
+ const before = lineText.slice(0, comment.loc.start.column);
72
+
73
+ return before.trim() === "";
74
+ }
75
+
76
+ function getCanonicalCommentText(comment, index, groupLength) {
77
+ const core = getCore(comment);
78
+ const isLast = index === groupLength - 1;
79
+ const suffix = isLast ? "" : " \\";
80
+
81
+ if (index === 0) {
82
+ return `// ${core}${suffix}`;
83
+ }
84
+
85
+ return `// ${core}${suffix}`;
86
+ }
87
+
88
+ function commentMatchesCanonical(comment, index, groupLength) {
89
+ const core = getCore(comment);
90
+ const isLast = index === groupLength - 1;
91
+ const line = sourceCode.lines[comment.loc.start.line - 1] || "";
92
+ const actual = line.slice(comment.loc.start.column);
93
+
94
+ if (isBlankSeparatorLine(comment) && index < groupLength - 1) {
95
+ return /^\/\/\s*\\$/.test(actual);
96
+ }
97
+
98
+ if (index === 0) {
99
+ const expected = `// ${core}${isLast ? "" : " \\"}`;
100
+
101
+ return actual === expected;
102
+ }
103
+
104
+ const match = actual.match(/^\/\/(\s+)(.*)$/);
105
+
106
+ if (!match || match[1].length < 3) {
107
+ return false;
108
+ }
109
+
110
+ let body = match[2];
111
+
112
+ if (!isLast) {
113
+ if (!/\\\s*$/.test(body)) {
114
+ return false;
115
+ }
116
+
117
+ body = body.replace(/\\\s*$/, "");
118
+ }
119
+
120
+ return body.trim() === core;
121
+ }
122
+
123
+ function checkGroup(group) {
124
+ if (group.length < 2) {
125
+ return;
126
+ }
127
+
128
+ const indent = " ".repeat(group[0].loc.start.column);
129
+
130
+ const canonicalLines = group.map((comment, index) => {
131
+ return getCanonicalCommentText(comment, index, group.length);
132
+ });
133
+
134
+ const canonical = canonicalLines.join(`\n${indent}`);
135
+
136
+ const allMatch = group.every((comment, index) => {
137
+ return commentMatchesCanonical(comment, index, group.length);
138
+ });
139
+
140
+ if (allMatch) {
141
+ return;
142
+ }
143
+
144
+ context.report({
145
+ loc: {
146
+ start: group[0].loc.start,
147
+ end: group[group.length - 1].loc.end,
148
+ },
149
+ messageId: "format",
150
+
151
+ fix(fixer) {
152
+ return fixer.replaceTextRange(
153
+ [group[0].range[0], group[group.length - 1].range[1]],
154
+ canonical
155
+ );
156
+ },
157
+ });
158
+ }
159
+
160
+ return {
161
+ Program() {
162
+ const comments = sourceCode.getAllComments();
163
+
164
+ let group = [];
165
+ let skipDirectiveContinuation = false;
166
+ let skipColumn = 0;
167
+ let skipLine = 0;
168
+
169
+ const flush = () => {
170
+ checkGroup(group);
171
+
172
+ group = [];
173
+ };
174
+
175
+ for (const comment of comments) {
176
+ if (
177
+ skipDirectiveContinuation &&
178
+ comment.type === "Line" &&
179
+ isStandalone(comment) &&
180
+ comment.loc.start.line === skipLine + 1 &&
181
+ comment.loc.start.column === skipColumn
182
+ ) {
183
+ skipLine = comment.loc.start.line;
184
+
185
+ if (!endsWithContinuation(comment)) {
186
+ skipDirectiveContinuation = false;
187
+ }
188
+
189
+ continue;
190
+ }
191
+
192
+ skipDirectiveContinuation = false;
193
+
194
+ const core = getCore(comment);
195
+ const isGroupable =
196
+ comment.type === "Line" &&
197
+ isStandalone(comment) &&
198
+ !isDirective(core) &&
199
+ (core.length > 0 || isBlankSeparatorLine(comment));
200
+
201
+ if (!isGroupable) {
202
+ flush();
203
+
204
+ if (
205
+ comment.type === "Line" &&
206
+ isStandalone(comment) &&
207
+ isDirective(core) &&
208
+ endsWithContinuation(comment)
209
+ ) {
210
+ skipDirectiveContinuation = true;
211
+ skipColumn = comment.loc.start.column;
212
+ skipLine = comment.loc.start.line;
213
+ }
214
+
215
+ continue;
216
+ }
217
+
218
+ if (group.length > 0) {
219
+ const previous = group[group.length - 1];
220
+ const isConsecutive =
221
+ comment.loc.start.line === previous.loc.start.line + 1 &&
222
+ comment.loc.start.column === previous.loc.start.column;
223
+
224
+ // A completed block (no trailing '\') must not absorb the next line
225
+ if (!endsWithContinuation(previous)) {
226
+ flush();
227
+ }
228
+
229
+ if (group.length > 0) {
230
+ const last = group[group.length - 1];
231
+ const stillConsecutive =
232
+ comment.loc.start.line === last.loc.start.line + 1 &&
233
+ comment.loc.start.column === last.loc.start.column;
234
+
235
+ // A boundary marker (e.g. 'Notice:') always starts a fresh group,
236
+ // so the previous comment is left untouched
237
+ if (!stillConsecutive || isBoundaryComment(comment)) {
238
+ flush();
239
+ }
240
+ }
241
+ }
242
+
243
+ group.push(comment);
244
+ }
245
+
246
+ flush();
247
+ },
248
+ };
249
+ },
250
+ };
@@ -0,0 +1,76 @@
1
+ export default {
2
+ meta: {
3
+ type: "suggestion",
4
+ docs: {
5
+ description: "enforce comment as first line in Vue data() return object",
6
+ category: "Stylistic Issues",
7
+ recommended: false
8
+ },
9
+ fixable: null,
10
+ schema: []
11
+ },
12
+
13
+ create(context) {
14
+ const COMMENT_PATTERN = /^--> (STATE|DATA|ROUTE PARAMS|COMPONENTS) <--$/;
15
+ const COMMENT_LIKE_PATTERN = /^--> .+ <--$/;
16
+
17
+ return {
18
+ 'Property[key.name="data"] > FunctionExpression > BlockStatement > ReturnStatement > ObjectExpression'(node) {
19
+ const properties = node.properties;
20
+
21
+ if (!properties || properties.length === 0) {
22
+ return;
23
+ }
24
+
25
+ const sourceCode = context.getSourceCode();
26
+ const comments = sourceCode.getCommentsInside(node);
27
+
28
+ if (comments.length === 0) {
29
+ context.report({
30
+ node,
31
+ message:
32
+ "data() return object must start with a comment: " +
33
+ "'// --> STATE <--', '// --> DATA <--', " +
34
+ "'// --> ROUTE PARAMS <--', or '// --> COMPONENTS <--'."
35
+ });
36
+
37
+ return;
38
+ }
39
+
40
+ const firstComment = comments[0];
41
+
42
+ if (
43
+ firstComment.type !== "Line" ||
44
+ !COMMENT_PATTERN.test(firstComment.value.trim())
45
+ ) {
46
+ context.report({
47
+ node: firstComment,
48
+ message:
49
+ "data() return object must start with a comment: " +
50
+ "'// --> STATE <--', '// --> DATA <--', " +
51
+ "'// --> ROUTE PARAMS <--', or '// --> COMPONENTS <--'."
52
+ });
53
+ }
54
+
55
+ comments.forEach((comment) => {
56
+ if (comment.type === "Line") {
57
+ const trimmed = comment.value.trim();
58
+
59
+ if (
60
+ COMMENT_LIKE_PATTERN.test(trimmed) &&
61
+ !COMMENT_PATTERN.test(trimmed)
62
+ ) {
63
+ context.report({
64
+ node: comment,
65
+ message:
66
+ "Invalid comment. Must be '// --> STATE <--', " +
67
+ "'// --> DATA <--', '// --> ROUTE PARAMS <--', " +
68
+ "or '// --> COMPONENTS <--'."
69
+ });
70
+ }
71
+ }
72
+ });
73
+ }
74
+ };
75
+ }
76
+ };