2 * @fileoverview Enforces empty lines around comments.
3 * @author Jamund Ferguson
7 //------------------------------------------------------------------------------
9 //------------------------------------------------------------------------------
11 const astUtils
= require("./utils/ast-utils");
13 //------------------------------------------------------------------------------
15 //------------------------------------------------------------------------------
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.
22 function getEmptyLineNums(lines
) {
23 const emptyLines
= lines
.map((line
, i
) => ({
26 })).filter(line
=> !line
.code
).map(line
=> line
.num
);
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.
36 function getCommentLineNums(comments
) {
39 comments
.forEach(token
=> {
40 const start
= token
.loc
.start
.line
;
41 const end
= token
.loc
.end
.line
;
43 lines
.push(start
, end
);
48 //------------------------------------------------------------------------------
50 //------------------------------------------------------------------------------
52 /** @type {import('../shared/types').Rule} */
58 description
: "Require empty lines around comments",
60 url
: "https://eslint.org/docs/latest/rules/lines-around-comment"
63 fixable
: "whitespace",
114 applyDefaultIgnorePatterns
: {
117 afterHashbangComment
: {
122 additionalProperties
: false
126 after
: "Expected line after comment.",
127 before
: "Expected line before comment."
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;
139 options
.beforeBlockComment
= typeof options
.beforeBlockComment
!== "undefined" ? options
.beforeBlockComment
: true;
141 const sourceCode
= context
.sourceCode
;
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
));
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.
155 function codeAroundComment(token
) {
156 let currentToken
= token
;
159 currentToken
= sourceCode
.getTokenBefore(currentToken
, { includeComments
: true });
160 } while (currentToken
&& astUtils
.isCommentToken(currentToken
));
162 if (currentToken
&& astUtils
.isTokenOnSameLine(currentToken
, token
)) {
166 currentToken
= token
;
168 currentToken
= sourceCode
.getTokenAfter(currentToken
, { includeComments
: true });
169 } while (currentToken
&& astUtils
.isCommentToken(currentToken
));
171 if (currentToken
&& astUtils
.isTokenOnSameLine(token
, currentToken
)) {
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.
184 function isParentNodeType(parent
, nodeType
) {
185 return parent
.type
=== nodeType
||
186 (parent
.body
&& parent
.body
.type
=== nodeType
) ||
187 (parent
.consequent
&& parent
.consequent
.type
=== nodeType
);
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.
195 function getParentNodeOfToken(token
) {
196 const node
= sourceCode
.getNodeByRangeIndex(token
.range
[0]);
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.
202 * Example where this function returns `null`:
209 * Example where this function returns `StaticBlock` node:
217 if (node
&& node
.type
=== "StaticBlock") {
218 const openingBrace
= sourceCode
.getFirstToken(node
, { skip
: 1 }); // skip the `static` token
220 return token
.range
[0] >= openingBrace
.range
[0]
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.
234 function isCommentAtParentStart(token
, nodeType
) {
235 const parent
= getParentNodeOfToken(token
);
237 if (parent
&& isParentNodeType(parent
, nodeType
)) {
238 let parentStartNodeOrToken
= parent
;
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
248 return token
.loc
.start
.line
- parentStartNodeOrToken
.loc
.start
.line
=== 1;
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.
260 function isCommentAtParentEnd(token
, nodeType
) {
261 const parent
= getParentNodeOfToken(token
);
263 return !!parent
&& isParentNodeType(parent
, nodeType
) &&
264 parent
.loc
.end
.line
- token
.loc
.end
.line
=== 1;
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.
272 function isCommentAtBlockStart(token
) {
274 isCommentAtParentStart(token
, "ClassBody") ||
275 isCommentAtParentStart(token
, "BlockStatement") ||
276 isCommentAtParentStart(token
, "StaticBlock") ||
277 isCommentAtParentStart(token
, "SwitchCase") ||
278 isCommentAtParentStart(token
, "SwitchStatement")
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.
287 function isCommentAtBlockEnd(token
) {
289 isCommentAtParentEnd(token
, "ClassBody") ||
290 isCommentAtParentEnd(token
, "BlockStatement") ||
291 isCommentAtParentEnd(token
, "StaticBlock") ||
292 isCommentAtParentEnd(token
, "SwitchCase") ||
293 isCommentAtParentEnd(token
, "SwitchStatement")
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.
302 function isCommentAtClassStart(token
) {
303 return isCommentAtParentStart(token
, "ClassBody");
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.
311 function isCommentAtClassEnd(token
) {
312 return isCommentAtParentEnd(token
, "ClassBody");
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.
320 function isCommentAtObjectStart(token
) {
321 return isCommentAtParentStart(token
, "ObjectExpression") || isCommentAtParentStart(token
, "ObjectPattern");
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.
329 function isCommentAtObjectEnd(token
) {
330 return isCommentAtParentEnd(token
, "ObjectExpression") || isCommentAtParentEnd(token
, "ObjectPattern");
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.
338 function isCommentAtArrayStart(token
) {
339 return isCommentAtParentStart(token
, "ArrayExpression") || isCommentAtParentStart(token
, "ArrayPattern");
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.
347 function isCommentAtArrayEnd(token
) {
348 return isCommentAtParentEnd(token
, "ArrayExpression") || isCommentAtParentEnd(token
, "ArrayPattern");
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.
359 function checkForEmptyLine(token
, opts
) {
360 if (applyDefaultIgnorePatterns
&& defaultIgnoreRegExp
.test(token
.value
)) {
364 if (ignorePattern
&& customIgnoreRegExp
.test(token
.value
)) {
368 let after
= opts
.after
,
369 before
= opts
.before
;
371 const prevLineNum
= token
.loc
.start
.line
- 1,
372 nextLineNum
= token
.loc
.end
.line
+ 1,
373 commentIsNotAlone
= codeAroundComment(token
);
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
);
387 const exceptionStartAllowed
= blockStartAllowed
|| classStartAllowed
|| objectStartAllowed
|| arrayStartAllowed
;
388 const exceptionEndAllowed
= blockEndAllowed
|| classEndAllowed
|| objectEndAllowed
|| arrayEndAllowed
;
390 // ignore top of the file and bottom of the file
391 if (prevLineNum
< 1) {
394 if (nextLineNum
>= numLines
) {
398 // we ignore all inline comments
399 if (commentIsNotAlone
) {
403 const previousTokenOrComment
= sourceCode
.getTokenBefore(token
, { includeComments
: true });
404 const nextTokenOrComment
= sourceCode
.getTokenAfter(token
, { includeComments
: true });
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
];
416 return fixer
.insertTextBeforeRange(range
, "\n");
421 // check for newline after
422 if (!exceptionEndAllowed
&& after
&& !commentAndEmptyLines
.has(nextLineNum
) &&
423 !(astUtils
.isCommentToken(nextTokenOrComment
) && astUtils
.isTokenOnSameLine(token
, nextTokenOrComment
))) {
428 return fixer
.insertTextAfter(token
, "\n");
435 //--------------------------------------------------------------------------
437 //--------------------------------------------------------------------------
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
449 } else if (token
.type
=== "Block") {
450 if (options
.beforeBlockComment
|| options
.afterBlockComment
) {
451 checkForEmptyLine(token
, {
452 after
: options
.afterBlockComment
,
453 before
: options
.beforeBlockComment
456 } else if (token
.type
=== "Shebang") {
457 if (options
.afterHashbangComment
) {
458 checkForEmptyLine(token
, {
459 after
: options
.afterHashbangComment
,