]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/lines-around-comment.js
74df09b01bd2b4838d9d7e9bb072b3d9755b42b4
[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 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 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/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 },
118 additionalProperties: false
119 }
120 ],
121 messages: {
122 after: "Expected line after comment.",
123 before: "Expected line before comment."
124 }
125 },
126
127 create(context) {
128
129 const options = Object.assign({}, context.options[0]);
130 const ignorePattern = options.ignorePattern;
131 const defaultIgnoreRegExp = astUtils.COMMENTS_IGNORE_PATTERN;
132 const customIgnoreRegExp = new RegExp(ignorePattern, "u");
133 const applyDefaultIgnorePatterns = options.applyDefaultIgnorePatterns !== false;
134
135 options.beforeBlockComment = typeof options.beforeBlockComment !== "undefined" ? options.beforeBlockComment : true;
136
137 const sourceCode = context.getSourceCode();
138
139 const lines = sourceCode.lines,
140 numLines = lines.length + 1,
141 comments = sourceCode.getAllComments(),
142 commentLines = getCommentLineNums(comments),
143 emptyLines = getEmptyLineNums(lines),
144 commentAndEmptyLines = commentLines.concat(emptyLines);
145
146 /**
147 * Returns whether or not comments are on lines starting with or ending with code
148 * @param {token} token The comment token to check.
149 * @returns {boolean} True if the comment is not alone.
150 */
151 function codeAroundComment(token) {
152 let currentToken = token;
153
154 do {
155 currentToken = sourceCode.getTokenBefore(currentToken, { includeComments: true });
156 } while (currentToken && astUtils.isCommentToken(currentToken));
157
158 if (currentToken && astUtils.isTokenOnSameLine(currentToken, token)) {
159 return true;
160 }
161
162 currentToken = token;
163 do {
164 currentToken = sourceCode.getTokenAfter(currentToken, { includeComments: true });
165 } while (currentToken && astUtils.isCommentToken(currentToken));
166
167 if (currentToken && astUtils.isTokenOnSameLine(token, currentToken)) {
168 return true;
169 }
170
171 return false;
172 }
173
174 /**
175 * Returns whether or not comments are inside a node type or not.
176 * @param {ASTNode} parent The Comment parent node.
177 * @param {string} nodeType The parent type to check against.
178 * @returns {boolean} True if the comment is inside nodeType.
179 */
180 function isParentNodeType(parent, nodeType) {
181 return parent.type === nodeType ||
182 (parent.body && parent.body.type === nodeType) ||
183 (parent.consequent && parent.consequent.type === nodeType);
184 }
185
186 /**
187 * Returns the parent node that contains the given token.
188 * @param {token} token The token to check.
189 * @returns {ASTNode|null} The parent node that contains the given token.
190 */
191 function getParentNodeOfToken(token) {
192 const node = sourceCode.getNodeByRangeIndex(token.range[0]);
193
194 /*
195 * For the purpose of this rule, the comment token is in a `StaticBlock` node only
196 * if it's inside the braces of that `StaticBlock` node.
197 *
198 * Example where this function returns `null`:
199 *
200 * static
201 * // comment
202 * {
203 * }
204 *
205 * Example where this function returns `StaticBlock` node:
206 *
207 * static
208 * {
209 * // comment
210 * }
211 *
212 */
213 if (node && node.type === "StaticBlock") {
214 const openingBrace = sourceCode.getFirstToken(node, { skip: 1 }); // skip the `static` token
215
216 return token.range[0] >= openingBrace.range[0]
217 ? node
218 : null;
219 }
220
221 return node;
222 }
223
224 /**
225 * Returns whether or not comments are at the parent start or not.
226 * @param {token} token The Comment token.
227 * @param {string} nodeType The parent type to check against.
228 * @returns {boolean} True if the comment is at parent start.
229 */
230 function isCommentAtParentStart(token, nodeType) {
231 const parent = getParentNodeOfToken(token);
232
233 if (parent && isParentNodeType(parent, nodeType)) {
234 const parentStartNodeOrToken = parent.type === "StaticBlock"
235 ? sourceCode.getFirstToken(parent, { skip: 1 }) // opening brace of the static block
236 : parent;
237
238 return token.loc.start.line - parentStartNodeOrToken.loc.start.line === 1;
239 }
240
241 return false;
242 }
243
244 /**
245 * Returns whether or not comments are at the parent end or not.
246 * @param {token} token The Comment token.
247 * @param {string} nodeType The parent type to check against.
248 * @returns {boolean} True if the comment is at parent end.
249 */
250 function isCommentAtParentEnd(token, nodeType) {
251 const parent = getParentNodeOfToken(token);
252
253 return !!parent && isParentNodeType(parent, nodeType) &&
254 parent.loc.end.line - token.loc.end.line === 1;
255 }
256
257 /**
258 * Returns whether or not comments are at the block start or not.
259 * @param {token} token The Comment token.
260 * @returns {boolean} True if the comment is at block start.
261 */
262 function isCommentAtBlockStart(token) {
263 return (
264 isCommentAtParentStart(token, "ClassBody") ||
265 isCommentAtParentStart(token, "BlockStatement") ||
266 isCommentAtParentStart(token, "StaticBlock") ||
267 isCommentAtParentStart(token, "SwitchCase")
268 );
269 }
270
271 /**
272 * Returns whether or not comments are at the block end or not.
273 * @param {token} token The Comment token.
274 * @returns {boolean} True if the comment is at block end.
275 */
276 function isCommentAtBlockEnd(token) {
277 return (
278 isCommentAtParentEnd(token, "ClassBody") ||
279 isCommentAtParentEnd(token, "BlockStatement") ||
280 isCommentAtParentEnd(token, "StaticBlock") ||
281 isCommentAtParentEnd(token, "SwitchCase") ||
282 isCommentAtParentEnd(token, "SwitchStatement")
283 );
284 }
285
286 /**
287 * Returns whether or not comments are at the class start or not.
288 * @param {token} token The Comment token.
289 * @returns {boolean} True if the comment is at class start.
290 */
291 function isCommentAtClassStart(token) {
292 return isCommentAtParentStart(token, "ClassBody");
293 }
294
295 /**
296 * Returns whether or not comments are at the class end or not.
297 * @param {token} token The Comment token.
298 * @returns {boolean} True if the comment is at class end.
299 */
300 function isCommentAtClassEnd(token) {
301 return isCommentAtParentEnd(token, "ClassBody");
302 }
303
304 /**
305 * Returns whether or not comments are at the object start or not.
306 * @param {token} token The Comment token.
307 * @returns {boolean} True if the comment is at object start.
308 */
309 function isCommentAtObjectStart(token) {
310 return isCommentAtParentStart(token, "ObjectExpression") || isCommentAtParentStart(token, "ObjectPattern");
311 }
312
313 /**
314 * Returns whether or not comments are at the object end or not.
315 * @param {token} token The Comment token.
316 * @returns {boolean} True if the comment is at object end.
317 */
318 function isCommentAtObjectEnd(token) {
319 return isCommentAtParentEnd(token, "ObjectExpression") || isCommentAtParentEnd(token, "ObjectPattern");
320 }
321
322 /**
323 * Returns whether or not comments are at the array start or not.
324 * @param {token} token The Comment token.
325 * @returns {boolean} True if the comment is at array start.
326 */
327 function isCommentAtArrayStart(token) {
328 return isCommentAtParentStart(token, "ArrayExpression") || isCommentAtParentStart(token, "ArrayPattern");
329 }
330
331 /**
332 * Returns whether or not comments are at the array end or not.
333 * @param {token} token The Comment token.
334 * @returns {boolean} True if the comment is at array end.
335 */
336 function isCommentAtArrayEnd(token) {
337 return isCommentAtParentEnd(token, "ArrayExpression") || isCommentAtParentEnd(token, "ArrayPattern");
338 }
339
340 /**
341 * Checks if a comment token has lines around it (ignores inline comments)
342 * @param {token} token The Comment token.
343 * @param {Object} opts Options to determine the newline.
344 * @param {boolean} opts.after Should have a newline after this line.
345 * @param {boolean} opts.before Should have a newline before this line.
346 * @returns {void}
347 */
348 function checkForEmptyLine(token, opts) {
349 if (applyDefaultIgnorePatterns && defaultIgnoreRegExp.test(token.value)) {
350 return;
351 }
352
353 if (ignorePattern && customIgnoreRegExp.test(token.value)) {
354 return;
355 }
356
357 let after = opts.after,
358 before = opts.before;
359
360 const prevLineNum = token.loc.start.line - 1,
361 nextLineNum = token.loc.end.line + 1,
362 commentIsNotAlone = codeAroundComment(token);
363
364 const blockStartAllowed = options.allowBlockStart &&
365 isCommentAtBlockStart(token) &&
366 !(options.allowClassStart === false &&
367 isCommentAtClassStart(token)),
368 blockEndAllowed = options.allowBlockEnd && isCommentAtBlockEnd(token) && !(options.allowClassEnd === false && isCommentAtClassEnd(token)),
369 classStartAllowed = options.allowClassStart && isCommentAtClassStart(token),
370 classEndAllowed = options.allowClassEnd && isCommentAtClassEnd(token),
371 objectStartAllowed = options.allowObjectStart && isCommentAtObjectStart(token),
372 objectEndAllowed = options.allowObjectEnd && isCommentAtObjectEnd(token),
373 arrayStartAllowed = options.allowArrayStart && isCommentAtArrayStart(token),
374 arrayEndAllowed = options.allowArrayEnd && isCommentAtArrayEnd(token);
375
376 const exceptionStartAllowed = blockStartAllowed || classStartAllowed || objectStartAllowed || arrayStartAllowed;
377 const exceptionEndAllowed = blockEndAllowed || classEndAllowed || objectEndAllowed || arrayEndAllowed;
378
379 // ignore top of the file and bottom of the file
380 if (prevLineNum < 1) {
381 before = false;
382 }
383 if (nextLineNum >= numLines) {
384 after = false;
385 }
386
387 // we ignore all inline comments
388 if (commentIsNotAlone) {
389 return;
390 }
391
392 const previousTokenOrComment = sourceCode.getTokenBefore(token, { includeComments: true });
393 const nextTokenOrComment = sourceCode.getTokenAfter(token, { includeComments: true });
394
395 // check for newline before
396 if (!exceptionStartAllowed && before && !commentAndEmptyLines.includes(prevLineNum) &&
397 !(astUtils.isCommentToken(previousTokenOrComment) && astUtils.isTokenOnSameLine(previousTokenOrComment, token))) {
398 const lineStart = token.range[0] - token.loc.start.column;
399 const range = [lineStart, lineStart];
400
401 context.report({
402 node: token,
403 messageId: "before",
404 fix(fixer) {
405 return fixer.insertTextBeforeRange(range, "\n");
406 }
407 });
408 }
409
410 // check for newline after
411 if (!exceptionEndAllowed && after && !commentAndEmptyLines.includes(nextLineNum) &&
412 !(astUtils.isCommentToken(nextTokenOrComment) && astUtils.isTokenOnSameLine(token, nextTokenOrComment))) {
413 context.report({
414 node: token,
415 messageId: "after",
416 fix(fixer) {
417 return fixer.insertTextAfter(token, "\n");
418 }
419 });
420 }
421
422 }
423
424 //--------------------------------------------------------------------------
425 // Public
426 //--------------------------------------------------------------------------
427
428 return {
429 Program() {
430 comments.forEach(token => {
431 if (token.type === "Line") {
432 if (options.beforeLineComment || options.afterLineComment) {
433 checkForEmptyLine(token, {
434 after: options.afterLineComment,
435 before: options.beforeLineComment
436 });
437 }
438 } else if (token.type === "Block") {
439 if (options.beforeBlockComment || options.afterBlockComment) {
440 checkForEmptyLine(token, {
441 after: options.afterBlockComment,
442 before: options.beforeBlockComment
443 });
444 }
445 }
446 });
447 }
448 };
449 }
450 };