]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/lines-around-comment.js
buildsys: change upload dist to bullseye
[pve-eslint.git] / eslint / lib / rules / lines-around-comment.js
1 /**
2 * @fileoverview Enforces empty lines around comments.
3 * @author Jamund Ferguson
4 */
5 "use strict";
6
7 //------------------------------------------------------------------------------
8 // Requirements
9 //------------------------------------------------------------------------------
10
11 const lodash = require("lodash"),
12 astUtils = require("./utils/ast-utils");
13
14 //------------------------------------------------------------------------------
15 // Helpers
16 //------------------------------------------------------------------------------
17
18 /**
19 * Return an array with with any line numbers that are empty.
20 * @param {Array} lines An array of each line of the file.
21 * @returns {Array} An array of line numbers.
22 */
23 function getEmptyLineNums(lines) {
24 const emptyLines = lines.map((line, i) => ({
25 code: line.trim(),
26 num: i + 1
27 })).filter(line => !line.code).map(line => line.num);
28
29 return emptyLines;
30 }
31
32 /**
33 * Return an array with with any line numbers that contain comments.
34 * @param {Array} comments An array of comment tokens.
35 * @returns {Array} An array of line numbers.
36 */
37 function getCommentLineNums(comments) {
38 const lines = [];
39
40 comments.forEach(token => {
41 const start = token.loc.start.line;
42 const end = token.loc.end.line;
43
44 lines.push(start, end);
45 });
46 return lines;
47 }
48
49 //------------------------------------------------------------------------------
50 // Rule Definition
51 //------------------------------------------------------------------------------
52
53 module.exports = {
54 meta: {
55 type: "layout",
56
57 docs: {
58 description: "require empty lines around comments",
59 category: "Stylistic Issues",
60 recommended: false,
61 url: "https://eslint.org/docs/rules/lines-around-comment"
62 },
63
64 fixable: "whitespace",
65
66 schema: [
67 {
68 type: "object",
69 properties: {
70 beforeBlockComment: {
71 type: "boolean",
72 default: true
73 },
74 afterBlockComment: {
75 type: "boolean",
76 default: false
77 },
78 beforeLineComment: {
79 type: "boolean",
80 default: false
81 },
82 afterLineComment: {
83 type: "boolean",
84 default: false
85 },
86 allowBlockStart: {
87 type: "boolean",
88 default: false
89 },
90 allowBlockEnd: {
91 type: "boolean",
92 default: false
93 },
94 allowClassStart: {
95 type: "boolean"
96 },
97 allowClassEnd: {
98 type: "boolean"
99 },
100 allowObjectStart: {
101 type: "boolean"
102 },
103 allowObjectEnd: {
104 type: "boolean"
105 },
106 allowArrayStart: {
107 type: "boolean"
108 },
109 allowArrayEnd: {
110 type: "boolean"
111 },
112 ignorePattern: {
113 type: "string"
114 },
115 applyDefaultIgnorePatterns: {
116 type: "boolean"
117 }
118 },
119 additionalProperties: false
120 }
121 ],
122 messages: {
123 after: "Expected line after comment.",
124 before: "Expected line before comment."
125 }
126 },
127
128 create(context) {
129
130 const options = Object.assign({}, context.options[0]);
131 const ignorePattern = options.ignorePattern;
132 const defaultIgnoreRegExp = astUtils.COMMENTS_IGNORE_PATTERN;
133 const customIgnoreRegExp = new RegExp(ignorePattern, "u");
134 const applyDefaultIgnorePatterns = options.applyDefaultIgnorePatterns !== false;
135
136 options.beforeBlockComment = typeof options.beforeBlockComment !== "undefined" ? options.beforeBlockComment : true;
137
138 const sourceCode = context.getSourceCode();
139
140 const lines = sourceCode.lines,
141 numLines = lines.length + 1,
142 comments = sourceCode.getAllComments(),
143 commentLines = getCommentLineNums(comments),
144 emptyLines = getEmptyLineNums(lines),
145 commentAndEmptyLines = commentLines.concat(emptyLines);
146
147 /**
148 * Returns whether or not comments are on lines starting with or ending with code
149 * @param {token} token The comment token to check.
150 * @returns {boolean} True if the comment is not alone.
151 */
152 function codeAroundComment(token) {
153 let currentToken = token;
154
155 do {
156 currentToken = sourceCode.getTokenBefore(currentToken, { includeComments: true });
157 } while (currentToken && astUtils.isCommentToken(currentToken));
158
159 if (currentToken && astUtils.isTokenOnSameLine(currentToken, token)) {
160 return true;
161 }
162
163 currentToken = token;
164 do {
165 currentToken = sourceCode.getTokenAfter(currentToken, { includeComments: true });
166 } while (currentToken && astUtils.isCommentToken(currentToken));
167
168 if (currentToken && astUtils.isTokenOnSameLine(token, currentToken)) {
169 return true;
170 }
171
172 return false;
173 }
174
175 /**
176 * Returns whether or not comments are inside a node type or not.
177 * @param {ASTNode} parent The Comment parent node.
178 * @param {string} nodeType The parent type to check against.
179 * @returns {boolean} True if the comment is inside nodeType.
180 */
181 function isParentNodeType(parent, nodeType) {
182 return parent.type === nodeType ||
183 (parent.body && parent.body.type === nodeType) ||
184 (parent.consequent && parent.consequent.type === nodeType);
185 }
186
187 /**
188 * Returns the parent node that contains the given token.
189 * @param {token} token The token to check.
190 * @returns {ASTNode} The parent node that contains the given token.
191 */
192 function getParentNodeOfToken(token) {
193 return sourceCode.getNodeByRangeIndex(token.range[0]);
194 }
195
196 /**
197 * Returns whether or not comments are at the parent start or not.
198 * @param {token} token The Comment token.
199 * @param {string} nodeType The parent type to check against.
200 * @returns {boolean} True if the comment is at parent start.
201 */
202 function isCommentAtParentStart(token, nodeType) {
203 const parent = getParentNodeOfToken(token);
204
205 return parent && isParentNodeType(parent, nodeType) &&
206 token.loc.start.line - parent.loc.start.line === 1;
207 }
208
209 /**
210 * Returns whether or not comments are at the parent end or not.
211 * @param {token} token The Comment token.
212 * @param {string} nodeType The parent type to check against.
213 * @returns {boolean} True if the comment is at parent end.
214 */
215 function isCommentAtParentEnd(token, nodeType) {
216 const parent = getParentNodeOfToken(token);
217
218 return parent && isParentNodeType(parent, nodeType) &&
219 parent.loc.end.line - token.loc.end.line === 1;
220 }
221
222 /**
223 * Returns whether or not comments are at the block start or not.
224 * @param {token} token The Comment token.
225 * @returns {boolean} True if the comment is at block start.
226 */
227 function isCommentAtBlockStart(token) {
228 return isCommentAtParentStart(token, "ClassBody") || isCommentAtParentStart(token, "BlockStatement") || isCommentAtParentStart(token, "SwitchCase");
229 }
230
231 /**
232 * Returns whether or not comments are at the block end or not.
233 * @param {token} token The Comment token.
234 * @returns {boolean} True if the comment is at block end.
235 */
236 function isCommentAtBlockEnd(token) {
237 return isCommentAtParentEnd(token, "ClassBody") || isCommentAtParentEnd(token, "BlockStatement") || isCommentAtParentEnd(token, "SwitchCase") || isCommentAtParentEnd(token, "SwitchStatement");
238 }
239
240 /**
241 * Returns whether or not comments are at the class start or not.
242 * @param {token} token The Comment token.
243 * @returns {boolean} True if the comment is at class start.
244 */
245 function isCommentAtClassStart(token) {
246 return isCommentAtParentStart(token, "ClassBody");
247 }
248
249 /**
250 * Returns whether or not comments are at the class end or not.
251 * @param {token} token The Comment token.
252 * @returns {boolean} True if the comment is at class end.
253 */
254 function isCommentAtClassEnd(token) {
255 return isCommentAtParentEnd(token, "ClassBody");
256 }
257
258 /**
259 * Returns whether or not comments are at the object start or not.
260 * @param {token} token The Comment token.
261 * @returns {boolean} True if the comment is at object start.
262 */
263 function isCommentAtObjectStart(token) {
264 return isCommentAtParentStart(token, "ObjectExpression") || isCommentAtParentStart(token, "ObjectPattern");
265 }
266
267 /**
268 * Returns whether or not comments are at the object end or not.
269 * @param {token} token The Comment token.
270 * @returns {boolean} True if the comment is at object end.
271 */
272 function isCommentAtObjectEnd(token) {
273 return isCommentAtParentEnd(token, "ObjectExpression") || isCommentAtParentEnd(token, "ObjectPattern");
274 }
275
276 /**
277 * Returns whether or not comments are at the array start or not.
278 * @param {token} token The Comment token.
279 * @returns {boolean} True if the comment is at array start.
280 */
281 function isCommentAtArrayStart(token) {
282 return isCommentAtParentStart(token, "ArrayExpression") || isCommentAtParentStart(token, "ArrayPattern");
283 }
284
285 /**
286 * Returns whether or not comments are at the array end or not.
287 * @param {token} token The Comment token.
288 * @returns {boolean} True if the comment is at array end.
289 */
290 function isCommentAtArrayEnd(token) {
291 return isCommentAtParentEnd(token, "ArrayExpression") || isCommentAtParentEnd(token, "ArrayPattern");
292 }
293
294 /**
295 * Checks if a comment token has lines around it (ignores inline comments)
296 * @param {token} token The Comment token.
297 * @param {Object} opts Options to determine the newline.
298 * @param {boolean} opts.after Should have a newline after this line.
299 * @param {boolean} opts.before Should have a newline before this line.
300 * @returns {void}
301 */
302 function checkForEmptyLine(token, opts) {
303 if (applyDefaultIgnorePatterns && defaultIgnoreRegExp.test(token.value)) {
304 return;
305 }
306
307 if (ignorePattern && customIgnoreRegExp.test(token.value)) {
308 return;
309 }
310
311 let after = opts.after,
312 before = opts.before;
313
314 const prevLineNum = token.loc.start.line - 1,
315 nextLineNum = token.loc.end.line + 1,
316 commentIsNotAlone = codeAroundComment(token);
317
318 const blockStartAllowed = options.allowBlockStart &&
319 isCommentAtBlockStart(token) &&
320 !(options.allowClassStart === false &&
321 isCommentAtClassStart(token)),
322 blockEndAllowed = options.allowBlockEnd && isCommentAtBlockEnd(token) && !(options.allowClassEnd === false && isCommentAtClassEnd(token)),
323 classStartAllowed = options.allowClassStart && isCommentAtClassStart(token),
324 classEndAllowed = options.allowClassEnd && isCommentAtClassEnd(token),
325 objectStartAllowed = options.allowObjectStart && isCommentAtObjectStart(token),
326 objectEndAllowed = options.allowObjectEnd && isCommentAtObjectEnd(token),
327 arrayStartAllowed = options.allowArrayStart && isCommentAtArrayStart(token),
328 arrayEndAllowed = options.allowArrayEnd && isCommentAtArrayEnd(token);
329
330 const exceptionStartAllowed = blockStartAllowed || classStartAllowed || objectStartAllowed || arrayStartAllowed;
331 const exceptionEndAllowed = blockEndAllowed || classEndAllowed || objectEndAllowed || arrayEndAllowed;
332
333 // ignore top of the file and bottom of the file
334 if (prevLineNum < 1) {
335 before = false;
336 }
337 if (nextLineNum >= numLines) {
338 after = false;
339 }
340
341 // we ignore all inline comments
342 if (commentIsNotAlone) {
343 return;
344 }
345
346 const previousTokenOrComment = sourceCode.getTokenBefore(token, { includeComments: true });
347 const nextTokenOrComment = sourceCode.getTokenAfter(token, { includeComments: true });
348
349 // check for newline before
350 if (!exceptionStartAllowed && before && !lodash.includes(commentAndEmptyLines, prevLineNum) &&
351 !(astUtils.isCommentToken(previousTokenOrComment) && astUtils.isTokenOnSameLine(previousTokenOrComment, token))) {
352 const lineStart = token.range[0] - token.loc.start.column;
353 const range = [lineStart, lineStart];
354
355 context.report({
356 node: token,
357 messageId: "before",
358 fix(fixer) {
359 return fixer.insertTextBeforeRange(range, "\n");
360 }
361 });
362 }
363
364 // check for newline after
365 if (!exceptionEndAllowed && after && !lodash.includes(commentAndEmptyLines, nextLineNum) &&
366 !(astUtils.isCommentToken(nextTokenOrComment) && astUtils.isTokenOnSameLine(token, nextTokenOrComment))) {
367 context.report({
368 node: token,
369 messageId: "after",
370 fix(fixer) {
371 return fixer.insertTextAfter(token, "\n");
372 }
373 });
374 }
375
376 }
377
378 //--------------------------------------------------------------------------
379 // Public
380 //--------------------------------------------------------------------------
381
382 return {
383 Program() {
384 comments.forEach(token => {
385 if (token.type === "Line") {
386 if (options.beforeLineComment || options.afterLineComment) {
387 checkForEmptyLine(token, {
388 after: options.afterLineComment,
389 before: options.beforeLineComment
390 });
391 }
392 } else if (token.type === "Block") {
393 if (options.beforeBlockComment || options.afterBlockComment) {
394 checkForEmptyLine(token, {
395 after: options.afterBlockComment,
396 before: options.beforeBlockComment
397 });
398 }
399 }
400 });
401 }
402 };
403 }
404 };