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 //------------------------------------------------------------------------------
57 description: "require empty lines around comments",
58 category: "Stylistic Issues",
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} The parent node that contains the given token.
191 function getParentNodeOfToken(token) {
192 return sourceCode.getNodeByRangeIndex(token.range[0]);
196 * Returns whether or not comments are at the parent start or not.
197 * @param {token} token The Comment token.
198 * @param {string} nodeType The parent type to check against.
199 * @returns {boolean} True if the comment is at parent start.
201 function isCommentAtParentStart(token, nodeType) {
202 const parent = getParentNodeOfToken(token);
204 return parent && isParentNodeType(parent, nodeType) &&
205 token.loc.start.line - parent.loc.start.line === 1;
209 * Returns whether or not comments are at the parent end or not.
210 * @param {token} token The Comment token.
211 * @param {string} nodeType The parent type to check against.
212 * @returns {boolean} True if the comment is at parent end.
214 function isCommentAtParentEnd(token, nodeType) {
215 const parent = getParentNodeOfToken(token);
217 return parent && isParentNodeType(parent, nodeType) &&
218 parent.loc.end.line - token.loc.end.line === 1;
222 * Returns whether or not comments are at the block start or not.
223 * @param {token} token The Comment token.
224 * @returns {boolean} True if the comment is at block start.
226 function isCommentAtBlockStart(token) {
227 return isCommentAtParentStart(token, "ClassBody") || isCommentAtParentStart(token, "BlockStatement") || isCommentAtParentStart(token, "SwitchCase");
231 * Returns whether or not comments are at the block end or not.
232 * @param {token} token The Comment token.
233 * @returns {boolean} True if the comment is at block end.
235 function isCommentAtBlockEnd(token) {
236 return isCommentAtParentEnd(token, "ClassBody") || isCommentAtParentEnd(token, "BlockStatement") || isCommentAtParentEnd(token, "SwitchCase") || isCommentAtParentEnd(token, "SwitchStatement");
240 * Returns whether or not comments are at the class start or not.
241 * @param {token} token The Comment token.
242 * @returns {boolean} True if the comment is at class start.
244 function isCommentAtClassStart(token) {
245 return isCommentAtParentStart(token, "ClassBody");
249 * Returns whether or not comments are at the class end or not.
250 * @param {token} token The Comment token.
251 * @returns {boolean} True if the comment is at class end.
253 function isCommentAtClassEnd(token) {
254 return isCommentAtParentEnd(token, "ClassBody");
258 * Returns whether or not comments are at the object start or not.
259 * @param {token} token The Comment token.
260 * @returns {boolean} True if the comment is at object start.
262 function isCommentAtObjectStart(token) {
263 return isCommentAtParentStart(token, "ObjectExpression") || isCommentAtParentStart(token, "ObjectPattern");
267 * Returns whether or not comments are at the object end or not.
268 * @param {token} token The Comment token.
269 * @returns {boolean} True if the comment is at object end.
271 function isCommentAtObjectEnd(token) {
272 return isCommentAtParentEnd(token, "ObjectExpression") || isCommentAtParentEnd(token, "ObjectPattern");
276 * Returns whether or not comments are at the array start or not.
277 * @param {token} token The Comment token.
278 * @returns {boolean} True if the comment is at array start.
280 function isCommentAtArrayStart(token) {
281 return isCommentAtParentStart(token, "ArrayExpression") || isCommentAtParentStart(token, "ArrayPattern");
285 * Returns whether or not comments are at the array end or not.
286 * @param {token} token The Comment token.
287 * @returns {boolean} True if the comment is at array end.
289 function isCommentAtArrayEnd(token) {
290 return isCommentAtParentEnd(token, "ArrayExpression") || isCommentAtParentEnd(token, "ArrayPattern");
294 * Checks if a comment token has lines around it (ignores inline comments)
295 * @param {token} token The Comment token.
296 * @param {Object} opts Options to determine the newline.
297 * @param {boolean} opts.after Should have a newline after this line.
298 * @param {boolean} opts.before Should have a newline before this line.
301 function checkForEmptyLine(token, opts) {
302 if (applyDefaultIgnorePatterns && defaultIgnoreRegExp.test(token.value)) {
306 if (ignorePattern && customIgnoreRegExp.test(token.value)) {
310 let after = opts.after,
311 before = opts.before;
313 const prevLineNum = token.loc.start.line - 1,
314 nextLineNum = token.loc.end.line + 1,
315 commentIsNotAlone = codeAroundComment(token);
317 const blockStartAllowed = options.allowBlockStart &&
318 isCommentAtBlockStart(token) &&
319 !(options.allowClassStart === false &&
320 isCommentAtClassStart(token)),
321 blockEndAllowed = options.allowBlockEnd && isCommentAtBlockEnd(token) && !(options.allowClassEnd === false && isCommentAtClassEnd(token)),
322 classStartAllowed = options.allowClassStart && isCommentAtClassStart(token),
323 classEndAllowed = options.allowClassEnd && isCommentAtClassEnd(token),
324 objectStartAllowed = options.allowObjectStart && isCommentAtObjectStart(token),
325 objectEndAllowed = options.allowObjectEnd && isCommentAtObjectEnd(token),
326 arrayStartAllowed = options.allowArrayStart && isCommentAtArrayStart(token),
327 arrayEndAllowed = options.allowArrayEnd && isCommentAtArrayEnd(token);
329 const exceptionStartAllowed = blockStartAllowed || classStartAllowed || objectStartAllowed || arrayStartAllowed;
330 const exceptionEndAllowed = blockEndAllowed || classEndAllowed || objectEndAllowed || arrayEndAllowed;
332 // ignore top of the file and bottom of the file
333 if (prevLineNum < 1) {
336 if (nextLineNum >= numLines) {
340 // we ignore all inline comments
341 if (commentIsNotAlone) {
345 const previousTokenOrComment = sourceCode.getTokenBefore(token, { includeComments: true });
346 const nextTokenOrComment = sourceCode.getTokenAfter(token, { includeComments: true });
348 // check for newline before
349 if (!exceptionStartAllowed && before && !commentAndEmptyLines.includes(prevLineNum) &&
350 !(astUtils.isCommentToken(previousTokenOrComment) && astUtils.isTokenOnSameLine(previousTokenOrComment, token))) {
351 const lineStart = token.range[0] - token.loc.start.column;
352 const range = [lineStart, lineStart];
358 return fixer.insertTextBeforeRange(range, "\n");
363 // check for newline after
364 if (!exceptionEndAllowed && after && !commentAndEmptyLines.includes(nextLineNum) &&
365 !(astUtils.isCommentToken(nextTokenOrComment) && astUtils.isTokenOnSameLine(token, nextTokenOrComment))) {
370 return fixer.insertTextAfter(token, "\n");
377 //--------------------------------------------------------------------------
379 //--------------------------------------------------------------------------
383 comments.forEach(token => {
384 if (token.type === "Line") {
385 if (options.beforeLineComment || options.afterLineComment) {
386 checkForEmptyLine(token, {
387 after: options.afterLineComment,
388 before: options.beforeLineComment
391 } else if (token.type === "Block") {
392 if (options.beforeBlockComment || options.afterBlockComment) {
393 checkForEmptyLine(token, {
394 after: options.afterBlockComment,
395 before: options.beforeBlockComment