2 * @fileoverview Enforces empty lines around comments.
3 * @author Jamund Ferguson
7 //------------------------------------------------------------------------------
9 //------------------------------------------------------------------------------
11 const lodash = require("lodash"),
12 astUtils = require("./utils/ast-utils");
14 //------------------------------------------------------------------------------
16 //------------------------------------------------------------------------------
19 * Return an array with with any line numbers that are empty.
20 * @param {Array} lines An array of each line of the file.
21 * @returns {Array} An array of line numbers.
23 function getEmptyLineNums(lines) {
24 const emptyLines = lines.map((line, i) => ({
27 })).filter(line => !line.code).map(line => line.num);
33 * Return an array with with any line numbers that contain comments.
34 * @param {Array} comments An array of comment tokens.
35 * @returns {Array} An array of line numbers.
37 function getCommentLineNums(comments) {
40 comments.forEach(token => {
41 const start = token.loc.start.line;
42 const end = token.loc.end.line;
44 lines.push(start, end);
49 //------------------------------------------------------------------------------
51 //------------------------------------------------------------------------------
58 description: "require empty lines around comments",
59 category: "Stylistic Issues",
61 url: "https://eslint.org/docs/rules/lines-around-comment"
64 fixable: "whitespace",
115 applyDefaultIgnorePatterns: {
119 additionalProperties: false
123 after: "Expected line after comment.",
124 before: "Expected line before comment."
130 const options = Object.assign({}, context.options[0]);
131 const ignorePattern = options.ignorePattern;
132 const defaultIgnoreRegExp = astUtils.COMMENTS_IGNORE_PATTERN;
133 const customIgnoreRegExp = new RegExp(ignorePattern, "u");
134 const applyDefaultIgnorePatterns = options.applyDefaultIgnorePatterns !== false;
136 options.beforeBlockComment = typeof options.beforeBlockComment !== "undefined" ? options.beforeBlockComment : true;
138 const sourceCode = context.getSourceCode();
140 const lines = sourceCode.lines,
141 numLines = lines.length + 1,
142 comments = sourceCode.getAllComments(),
143 commentLines = getCommentLineNums(comments),
144 emptyLines = getEmptyLineNums(lines),
145 commentAndEmptyLines = commentLines.concat(emptyLines);
148 * Returns whether or not comments are on lines starting with or ending with code
149 * @param {token} token The comment token to check.
150 * @returns {boolean} True if the comment is not alone.
152 function codeAroundComment(token) {
153 let currentToken = token;
156 currentToken = sourceCode.getTokenBefore(currentToken, { includeComments: true });
157 } while (currentToken && astUtils.isCommentToken(currentToken));
159 if (currentToken && astUtils.isTokenOnSameLine(currentToken, token)) {
163 currentToken = token;
165 currentToken = sourceCode.getTokenAfter(currentToken, { includeComments: true });
166 } while (currentToken && astUtils.isCommentToken(currentToken));
168 if (currentToken && astUtils.isTokenOnSameLine(token, currentToken)) {
176 * Returns whether or not comments are inside a node type or not.
177 * @param {ASTNode} parent The Comment parent node.
178 * @param {string} nodeType The parent type to check against.
179 * @returns {boolean} True if the comment is inside nodeType.
181 function isParentNodeType(parent, nodeType) {
182 return parent.type === nodeType ||
183 (parent.body && parent.body.type === nodeType) ||
184 (parent.consequent && parent.consequent.type === nodeType);
188 * Returns the parent node that contains the given token.
189 * @param {token} token The token to check.
190 * @returns {ASTNode} The parent node that contains the given token.
192 function getParentNodeOfToken(token) {
193 return sourceCode.getNodeByRangeIndex(token.range[0]);
197 * Returns whether or not comments are at the parent start or not.
198 * @param {token} token The Comment token.
199 * @param {string} nodeType The parent type to check against.
200 * @returns {boolean} True if the comment is at parent start.
202 function isCommentAtParentStart(token, nodeType) {
203 const parent = getParentNodeOfToken(token);
205 return parent && isParentNodeType(parent, nodeType) &&
206 token.loc.start.line - parent.loc.start.line === 1;
210 * Returns whether or not comments are at the parent end or not.
211 * @param {token} token The Comment token.
212 * @param {string} nodeType The parent type to check against.
213 * @returns {boolean} True if the comment is at parent end.
215 function isCommentAtParentEnd(token, nodeType) {
216 const parent = getParentNodeOfToken(token);
218 return parent && isParentNodeType(parent, nodeType) &&
219 parent.loc.end.line - token.loc.end.line === 1;
223 * Returns whether or not comments are at the block start or not.
224 * @param {token} token The Comment token.
225 * @returns {boolean} True if the comment is at block start.
227 function isCommentAtBlockStart(token) {
228 return isCommentAtParentStart(token, "ClassBody") || isCommentAtParentStart(token, "BlockStatement") || isCommentAtParentStart(token, "SwitchCase");
232 * Returns whether or not comments are at the block end or not.
233 * @param {token} token The Comment token.
234 * @returns {boolean} True if the comment is at block end.
236 function isCommentAtBlockEnd(token) {
237 return isCommentAtParentEnd(token, "ClassBody") || isCommentAtParentEnd(token, "BlockStatement") || isCommentAtParentEnd(token, "SwitchCase") || isCommentAtParentEnd(token, "SwitchStatement");
241 * Returns whether or not comments are at the class start or not.
242 * @param {token} token The Comment token.
243 * @returns {boolean} True if the comment is at class start.
245 function isCommentAtClassStart(token) {
246 return isCommentAtParentStart(token, "ClassBody");
250 * Returns whether or not comments are at the class end or not.
251 * @param {token} token The Comment token.
252 * @returns {boolean} True if the comment is at class end.
254 function isCommentAtClassEnd(token) {
255 return isCommentAtParentEnd(token, "ClassBody");
259 * Returns whether or not comments are at the object start or not.
260 * @param {token} token The Comment token.
261 * @returns {boolean} True if the comment is at object start.
263 function isCommentAtObjectStart(token) {
264 return isCommentAtParentStart(token, "ObjectExpression") || isCommentAtParentStart(token, "ObjectPattern");
268 * Returns whether or not comments are at the object end or not.
269 * @param {token} token The Comment token.
270 * @returns {boolean} True if the comment is at object end.
272 function isCommentAtObjectEnd(token) {
273 return isCommentAtParentEnd(token, "ObjectExpression") || isCommentAtParentEnd(token, "ObjectPattern");
277 * Returns whether or not comments are at the array start or not.
278 * @param {token} token The Comment token.
279 * @returns {boolean} True if the comment is at array start.
281 function isCommentAtArrayStart(token) {
282 return isCommentAtParentStart(token, "ArrayExpression") || isCommentAtParentStart(token, "ArrayPattern");
286 * Returns whether or not comments are at the array end or not.
287 * @param {token} token The Comment token.
288 * @returns {boolean} True if the comment is at array end.
290 function isCommentAtArrayEnd(token) {
291 return isCommentAtParentEnd(token, "ArrayExpression") || isCommentAtParentEnd(token, "ArrayPattern");
295 * Checks if a comment token has lines around it (ignores inline comments)
296 * @param {token} token The Comment token.
297 * @param {Object} opts Options to determine the newline.
298 * @param {boolean} opts.after Should have a newline after this line.
299 * @param {boolean} opts.before Should have a newline before this line.
302 function checkForEmptyLine(token, opts) {
303 if (applyDefaultIgnorePatterns && defaultIgnoreRegExp.test(token.value)) {
307 if (ignorePattern && customIgnoreRegExp.test(token.value)) {
311 let after = opts.after,
312 before = opts.before;
314 const prevLineNum = token.loc.start.line - 1,
315 nextLineNum = token.loc.end.line + 1,
316 commentIsNotAlone = codeAroundComment(token);
318 const blockStartAllowed = options.allowBlockStart &&
319 isCommentAtBlockStart(token) &&
320 !(options.allowClassStart === false &&
321 isCommentAtClassStart(token)),
322 blockEndAllowed = options.allowBlockEnd && isCommentAtBlockEnd(token) && !(options.allowClassEnd === false && isCommentAtClassEnd(token)),
323 classStartAllowed = options.allowClassStart && isCommentAtClassStart(token),
324 classEndAllowed = options.allowClassEnd && isCommentAtClassEnd(token),
325 objectStartAllowed = options.allowObjectStart && isCommentAtObjectStart(token),
326 objectEndAllowed = options.allowObjectEnd && isCommentAtObjectEnd(token),
327 arrayStartAllowed = options.allowArrayStart && isCommentAtArrayStart(token),
328 arrayEndAllowed = options.allowArrayEnd && isCommentAtArrayEnd(token);
330 const exceptionStartAllowed = blockStartAllowed || classStartAllowed || objectStartAllowed || arrayStartAllowed;
331 const exceptionEndAllowed = blockEndAllowed || classEndAllowed || objectEndAllowed || arrayEndAllowed;
333 // ignore top of the file and bottom of the file
334 if (prevLineNum < 1) {
337 if (nextLineNum >= numLines) {
341 // we ignore all inline comments
342 if (commentIsNotAlone) {
346 const previousTokenOrComment = sourceCode.getTokenBefore(token, { includeComments: true });
347 const nextTokenOrComment = sourceCode.getTokenAfter(token, { includeComments: true });
349 // check for newline before
350 if (!exceptionStartAllowed && before && !lodash.includes(commentAndEmptyLines, prevLineNum) &&
351 !(astUtils.isCommentToken(previousTokenOrComment) && astUtils.isTokenOnSameLine(previousTokenOrComment, token))) {
352 const lineStart = token.range[0] - token.loc.start.column;
353 const range = [lineStart, lineStart];
359 return fixer.insertTextBeforeRange(range, "\n");
364 // check for newline after
365 if (!exceptionEndAllowed && after && !lodash.includes(commentAndEmptyLines, nextLineNum) &&
366 !(astUtils.isCommentToken(nextTokenOrComment) && astUtils.isTokenOnSameLine(token, nextTokenOrComment))) {
371 return fixer.insertTextAfter(token, "\n");
378 //--------------------------------------------------------------------------
380 //--------------------------------------------------------------------------
384 comments.forEach(token => {
385 if (token.type === "Line") {
386 if (options.beforeLineComment || options.afterLineComment) {
387 checkForEmptyLine(token, {
388 after: options.afterLineComment,
389 before: options.beforeLineComment
392 } else if (token.type === "Block") {
393 if (options.beforeBlockComment || options.afterBlockComment) {
394 checkForEmptyLine(token, {
395 after: options.afterBlockComment,
396 before: options.beforeBlockComment