1/** 2 * @fileoverview Enforces empty lines around comments. 3 * @author Jamund Ferguson 4 */ 5"use strict"; 6 7//------------------------------------------------------------------------------ 8// Requirements 9//------------------------------------------------------------------------------ 10 11const lodash = require("lodash"), 12 astUtils = require("./utils/ast-utils"); 13 14//------------------------------------------------------------------------------ 15// Helpers 16//------------------------------------------------------------------------------ 17 18/** 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. 22 */ 23function getEmptyLineNums(lines) { 24 const emptyLines = lines.map((line, i) => ({ 25 code: line.trim(), 26 num: i + 1 27 })).filter(line => !line.code).map(line => line.num); 28 29 return emptyLines; 30} 31 32/** 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. 36 */ 37function getCommentLineNums(comments) { 38 const lines = []; 39 40 comments.forEach(token => { 41 const start = token.loc.start.line; 42 const end = token.loc.end.line; 43 44 lines.push(start, end); 45 }); 46 return lines; 47} 48 49//------------------------------------------------------------------------------ 50// Rule Definition 51//------------------------------------------------------------------------------ 52 53module.exports = { 54 meta: { 55 type: "layout", 56 57 docs: { 58 description: "require empty lines around comments", 59 category: "Stylistic Issues", 60 recommended: false, 61 url: "https://eslint.org/docs/rules/lines-around-comment" 62 }, 63 64 fixable: "whitespace", 65 66 schema: [ 67 { 68 type: "object", 69 properties: { 70 beforeBlockComment: { 71 type: "boolean", 72 default: true 73 }, 74 afterBlockComment: { 75 type: "boolean", 76 default: false 77 }, 78 beforeLineComment: { 79 type: "boolean", 80 default: false 81 }, 82 afterLineComment: { 83 type: "boolean", 84 default: false 85 }, 86 allowBlockStart: { 87 type: "boolean", 88 default: false 89 }, 90 allowBlockEnd: { 91 type: "boolean", 92 default: false 93 }, 94 allowClassStart: { 95 type: "boolean" 96 }, 97 allowClassEnd: { 98 type: "boolean" 99 }, 100 allowObjectStart: { 101 type: "boolean" 102 }, 103 allowObjectEnd: { 104 type: "boolean" 105 }, 106 allowArrayStart: { 107 type: "boolean" 108 }, 109 allowArrayEnd: { 110 type: "boolean" 111 }, 112 ignorePattern: { 113 type: "string" 114 }, 115 applyDefaultIgnorePatterns: { 116 type: "boolean" 117 } 118 }, 119 additionalProperties: false 120 } 121 ], 122 messages: { 123 after: "Expected line after comment.", 124 before: "Expected line before comment." 125 } 126 }, 127 128 create(context) { 129 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; 135 136 options.beforeBlockComment = typeof options.beforeBlockComment !== "undefined" ? options.beforeBlockComment : true; 137 138 const sourceCode = context.getSourceCode(); 139 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); 146 147 /** 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. 151 */ 152 function codeAroundComment(token) { 153 let currentToken = token; 154 155 do { 156 currentToken = sourceCode.getTokenBefore(currentToken, { includeComments: true }); 157 } while (currentToken && astUtils.isCommentToken(currentToken)); 158 159 if (currentToken && astUtils.isTokenOnSameLine(currentToken, token)) { 160 return true; 161 } 162 163 currentToken = token; 164 do { 165 currentToken = sourceCode.getTokenAfter(currentToken, { includeComments: true }); 166 } while (currentToken && astUtils.isCommentToken(currentToken)); 167 168 if (currentToken && astUtils.isTokenOnSameLine(token, currentToken)) { 169 return true; 170 } 171 172 return false; 173 } 174 175 /** 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. 180 */ 181 function isParentNodeType(parent, nodeType) { 182 return parent.type === nodeType || 183 (parent.body && parent.body.type === nodeType) || 184 (parent.consequent && parent.consequent.type === nodeType); 185 } 186 187 /** 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. 191 */ 192 function getParentNodeOfToken(token) { 193 return sourceCode.getNodeByRangeIndex(token.range[0]); 194 } 195 196 /** 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. 201 */ 202 function isCommentAtParentStart(token, nodeType) { 203 const parent = getParentNodeOfToken(token); 204 205 return parent && isParentNodeType(parent, nodeType) && 206 token.loc.start.line - parent.loc.start.line === 1; 207 } 208 209 /** 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. 214 */ 215 function isCommentAtParentEnd(token, nodeType) { 216 const parent = getParentNodeOfToken(token); 217 218 return parent && isParentNodeType(parent, nodeType) && 219 parent.loc.end.line - token.loc.end.line === 1; 220 } 221 222 /** 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. 226 */ 227 function isCommentAtBlockStart(token) { 228 return isCommentAtParentStart(token, "ClassBody") || isCommentAtParentStart(token, "BlockStatement") || isCommentAtParentStart(token, "SwitchCase"); 229 } 230 231 /** 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. 235 */ 236 function isCommentAtBlockEnd(token) { 237 return isCommentAtParentEnd(token, "ClassBody") || isCommentAtParentEnd(token, "BlockStatement") || isCommentAtParentEnd(token, "SwitchCase") || isCommentAtParentEnd(token, "SwitchStatement"); 238 } 239 240 /** 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. 244 */ 245 function isCommentAtClassStart(token) { 246 return isCommentAtParentStart(token, "ClassBody"); 247 } 248 249 /** 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. 253 */ 254 function isCommentAtClassEnd(token) { 255 return isCommentAtParentEnd(token, "ClassBody"); 256 } 257 258 /** 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. 262 */ 263 function isCommentAtObjectStart(token) { 264 return isCommentAtParentStart(token, "ObjectExpression") || isCommentAtParentStart(token, "ObjectPattern"); 265 } 266 267 /** 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. 271 */ 272 function isCommentAtObjectEnd(token) { 273 return isCommentAtParentEnd(token, "ObjectExpression") || isCommentAtParentEnd(token, "ObjectPattern"); 274 } 275 276 /** 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. 280 */ 281 function isCommentAtArrayStart(token) { 282 return isCommentAtParentStart(token, "ArrayExpression") || isCommentAtParentStart(token, "ArrayPattern"); 283 } 284 285 /** 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. 289 */ 290 function isCommentAtArrayEnd(token) { 291 return isCommentAtParentEnd(token, "ArrayExpression") || isCommentAtParentEnd(token, "ArrayPattern"); 292 } 293 294 /** 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. 300 * @returns {void} 301 */ 302 function checkForEmptyLine(token, opts) { 303 if (applyDefaultIgnorePatterns && defaultIgnoreRegExp.test(token.value)) { 304 return; 305 } 306 307 if (ignorePattern && customIgnoreRegExp.test(token.value)) { 308 return; 309 } 310 311 let after = opts.after, 312 before = opts.before; 313 314 const prevLineNum = token.loc.start.line - 1, 315 nextLineNum = token.loc.end.line + 1, 316 commentIsNotAlone = codeAroundComment(token); 317 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); 329 330 const exceptionStartAllowed = blockStartAllowed || classStartAllowed || objectStartAllowed || arrayStartAllowed; 331 const exceptionEndAllowed = blockEndAllowed || classEndAllowed || objectEndAllowed || arrayEndAllowed; 332 333 // ignore top of the file and bottom of the file 334 if (prevLineNum < 1) { 335 before = false; 336 } 337 if (nextLineNum >= numLines) { 338 after = false; 339 } 340 341 // we ignore all inline comments 342 if (commentIsNotAlone) { 343 return; 344 } 345 346 const previousTokenOrComment = sourceCode.getTokenBefore(token, { includeComments: true }); 347 const nextTokenOrComment = sourceCode.getTokenAfter(token, { includeComments: true }); 348 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]; 354 355 context.report({ 356 node: token, 357 messageId: "before", 358 fix(fixer) { 359 return fixer.insertTextBeforeRange(range, "\n"); 360 } 361 }); 362 } 363 364 // check for newline after 365 if (!exceptionEndAllowed && after && !lodash.includes(commentAndEmptyLines, nextLineNum) && 366 !(astUtils.isCommentToken(nextTokenOrComment) && astUtils.isTokenOnSameLine(token, nextTokenOrComment))) { 367 context.report({ 368 node: token, 369 messageId: "after", 370 fix(fixer) { 371 return fixer.insertTextAfter(token, "\n"); 372 } 373 }); 374 } 375 376 } 377 378 //-------------------------------------------------------------------------- 379 // Public 380 //-------------------------------------------------------------------------- 381 382 return { 383 Program() { 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 390 }); 391 } 392 } else if (token.type === "Block") { 393 if (options.beforeBlockComment || options.afterBlockComment) { 394 checkForEmptyLine(token, { 395 after: options.afterBlockComment, 396 before: options.beforeBlockComment 397 }); 398 } 399 } 400 }); 401 } 402 }; 403 } 404}; 405