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 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 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/rules/lines-around-comment"
63 fixable
: "whitespace",
114 applyDefaultIgnorePatterns
: {
118 additionalProperties
: false
122 after
: "Expected line after comment.",
123 before
: "Expected line before comment."
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;
135 options
.beforeBlockComment
= typeof options
.beforeBlockComment
!== "undefined" ? options
.beforeBlockComment
: true;
137 const sourceCode
= context
.getSourceCode();
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
);
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.
151 function codeAroundComment(token
) {
152 let currentToken
= token
;
155 currentToken
= sourceCode
.getTokenBefore(currentToken
, { includeComments
: true });
156 } while (currentToken
&& astUtils
.isCommentToken(currentToken
));
158 if (currentToken
&& astUtils
.isTokenOnSameLine(currentToken
, token
)) {
162 currentToken
= token
;
164 currentToken
= sourceCode
.getTokenAfter(currentToken
, { includeComments
: true });
165 } while (currentToken
&& astUtils
.isCommentToken(currentToken
));
167 if (currentToken
&& astUtils
.isTokenOnSameLine(token
, currentToken
)) {
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.
180 function isParentNodeType(parent
, nodeType
) {
181 return parent
.type
=== nodeType
||
182 (parent
.body
&& parent
.body
.type
=== nodeType
) ||
183 (parent
.consequent
&& parent
.consequent
.type
=== nodeType
);
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.
191 function getParentNodeOfToken(token
) {
192 const node
= sourceCode
.getNodeByRangeIndex(token
.range
[0]);
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.
198 * Example where this function returns `null`:
205 * Example where this function returns `StaticBlock` node:
213 if (node
&& node
.type
=== "StaticBlock") {
214 const openingBrace
= sourceCode
.getFirstToken(node
, { skip
: 1 }); // skip the `static` token
216 return token
.range
[0] >= openingBrace
.range
[0]
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.
230 function isCommentAtParentStart(token
, nodeType
) {
231 const parent
= getParentNodeOfToken(token
);
233 if (parent
&& isParentNodeType(parent
, nodeType
)) {
234 const parentStartNodeOrToken
= parent
.type
=== "StaticBlock"
235 ? sourceCode
.getFirstToken(parent
, { skip
: 1 }) // opening brace of the static block
238 return token
.loc
.start
.line
- parentStartNodeOrToken
.loc
.start
.line
=== 1;
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.
250 function isCommentAtParentEnd(token
, nodeType
) {
251 const parent
= getParentNodeOfToken(token
);
253 return !!parent
&& isParentNodeType(parent
, nodeType
) &&
254 parent
.loc
.end
.line
- token
.loc
.end
.line
=== 1;
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.
262 function isCommentAtBlockStart(token
) {
264 isCommentAtParentStart(token
, "ClassBody") ||
265 isCommentAtParentStart(token
, "BlockStatement") ||
266 isCommentAtParentStart(token
, "StaticBlock") ||
267 isCommentAtParentStart(token
, "SwitchCase")
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.
276 function isCommentAtBlockEnd(token
) {
278 isCommentAtParentEnd(token
, "ClassBody") ||
279 isCommentAtParentEnd(token
, "BlockStatement") ||
280 isCommentAtParentEnd(token
, "StaticBlock") ||
281 isCommentAtParentEnd(token
, "SwitchCase") ||
282 isCommentAtParentEnd(token
, "SwitchStatement")
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.
291 function isCommentAtClassStart(token
) {
292 return isCommentAtParentStart(token
, "ClassBody");
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.
300 function isCommentAtClassEnd(token
) {
301 return isCommentAtParentEnd(token
, "ClassBody");
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.
309 function isCommentAtObjectStart(token
) {
310 return isCommentAtParentStart(token
, "ObjectExpression") || isCommentAtParentStart(token
, "ObjectPattern");
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.
318 function isCommentAtObjectEnd(token
) {
319 return isCommentAtParentEnd(token
, "ObjectExpression") || isCommentAtParentEnd(token
, "ObjectPattern");
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.
327 function isCommentAtArrayStart(token
) {
328 return isCommentAtParentStart(token
, "ArrayExpression") || isCommentAtParentStart(token
, "ArrayPattern");
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.
336 function isCommentAtArrayEnd(token
) {
337 return isCommentAtParentEnd(token
, "ArrayExpression") || isCommentAtParentEnd(token
, "ArrayPattern");
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.
348 function checkForEmptyLine(token
, opts
) {
349 if (applyDefaultIgnorePatterns
&& defaultIgnoreRegExp
.test(token
.value
)) {
353 if (ignorePattern
&& customIgnoreRegExp
.test(token
.value
)) {
357 let after
= opts
.after
,
358 before
= opts
.before
;
360 const prevLineNum
= token
.loc
.start
.line
- 1,
361 nextLineNum
= token
.loc
.end
.line
+ 1,
362 commentIsNotAlone
= codeAroundComment(token
);
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
);
376 const exceptionStartAllowed
= blockStartAllowed
|| classStartAllowed
|| objectStartAllowed
|| arrayStartAllowed
;
377 const exceptionEndAllowed
= blockEndAllowed
|| classEndAllowed
|| objectEndAllowed
|| arrayEndAllowed
;
379 // ignore top of the file and bottom of the file
380 if (prevLineNum
< 1) {
383 if (nextLineNum
>= numLines
) {
387 // we ignore all inline comments
388 if (commentIsNotAlone
) {
392 const previousTokenOrComment
= sourceCode
.getTokenBefore(token
, { includeComments
: true });
393 const nextTokenOrComment
= sourceCode
.getTokenAfter(token
, { includeComments
: true });
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
];
405 return fixer
.insertTextBeforeRange(range
, "\n");
410 // check for newline after
411 if (!exceptionEndAllowed
&& after
&& !commentAndEmptyLines
.includes(nextLineNum
) &&
412 !(astUtils
.isCommentToken(nextTokenOrComment
) && astUtils
.isTokenOnSameLine(token
, nextTokenOrComment
))) {
417 return fixer
.insertTextAfter(token
, "\n");
424 //--------------------------------------------------------------------------
426 //--------------------------------------------------------------------------
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
438 } else if (token
.type
=== "Block") {
439 if (options
.beforeBlockComment
|| options
.afterBlockComment
) {
440 checkForEmptyLine(token
, {
441 after
: options
.afterBlockComment
,
442 before
: options
.beforeBlockComment