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