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