1/** 2 * @fileoverview Rule to require or disallow newlines between statements 3 * @author Toru Nagashima 4 */ 5 6"use strict"; 7 8//------------------------------------------------------------------------------ 9// Requirements 10//------------------------------------------------------------------------------ 11 12const astUtils = require("./utils/ast-utils"); 13 14//------------------------------------------------------------------------------ 15// Helpers 16//------------------------------------------------------------------------------ 17 18const LT = `[${Array.from(astUtils.LINEBREAKS).join("")}]`; 19const PADDING_LINE_SEQUENCE = new RegExp( 20 String.raw`^(\s*?${LT})\s*${LT}(\s*;?)$`, 21 "u" 22); 23const CJS_EXPORT = /^(?:module\s*\.\s*)?exports(?:\s*\.|\s*\[|$)/u; 24const CJS_IMPORT = /^require\(/u; 25 26/** 27 * Creates tester which check if a node starts with specific keyword. 28 * @param {string} keyword The keyword to test. 29 * @returns {Object} the created tester. 30 * @private 31 */ 32function newKeywordTester(keyword) { 33 return { 34 test: (node, sourceCode) => 35 sourceCode.getFirstToken(node).value === keyword 36 }; 37} 38 39/** 40 * Creates tester which check if a node starts with specific keyword and spans a single line. 41 * @param {string} keyword The keyword to test. 42 * @returns {Object} the created tester. 43 * @private 44 */ 45function newSinglelineKeywordTester(keyword) { 46 return { 47 test: (node, sourceCode) => 48 node.loc.start.line === node.loc.end.line && 49 sourceCode.getFirstToken(node).value === keyword 50 }; 51} 52 53/** 54 * Creates tester which check if a node starts with specific keyword and spans multiple lines. 55 * @param {string} keyword The keyword to test. 56 * @returns {Object} the created tester. 57 * @private 58 */ 59function newMultilineKeywordTester(keyword) { 60 return { 61 test: (node, sourceCode) => 62 node.loc.start.line !== node.loc.end.line && 63 sourceCode.getFirstToken(node).value === keyword 64 }; 65} 66 67/** 68 * Creates tester which check if a node is specific type. 69 * @param {string} type The node type to test. 70 * @returns {Object} the created tester. 71 * @private 72 */ 73function newNodeTypeTester(type) { 74 return { 75 test: node => 76 node.type === type 77 }; 78} 79 80/** 81 * Checks the given node is an expression statement of IIFE. 82 * @param {ASTNode} node The node to check. 83 * @returns {boolean} `true` if the node is an expression statement of IIFE. 84 * @private 85 */ 86function isIIFEStatement(node) { 87 if (node.type === "ExpressionStatement") { 88 let call = astUtils.skipChainExpression(node.expression); 89 90 if (call.type === "UnaryExpression") { 91 call = astUtils.skipChainExpression(call.argument); 92 } 93 return call.type === "CallExpression" && astUtils.isFunction(call.callee); 94 } 95 return false; 96} 97 98/** 99 * Checks whether the given node is a block-like statement. 100 * This checks the last token of the node is the closing brace of a block. 101 * @param {SourceCode} sourceCode The source code to get tokens. 102 * @param {ASTNode} node The node to check. 103 * @returns {boolean} `true` if the node is a block-like statement. 104 * @private 105 */ 106function isBlockLikeStatement(sourceCode, node) { 107 108 // do-while with a block is a block-like statement. 109 if (node.type === "DoWhileStatement" && node.body.type === "BlockStatement") { 110 return true; 111 } 112 113 /* 114 * IIFE is a block-like statement specially from 115 * JSCS#disallowPaddingNewLinesAfterBlocks. 116 */ 117 if (isIIFEStatement(node)) { 118 return true; 119 } 120 121 // Checks the last token is a closing brace of blocks. 122 const lastToken = sourceCode.getLastToken(node, astUtils.isNotSemicolonToken); 123 const belongingNode = lastToken && astUtils.isClosingBraceToken(lastToken) 124 ? sourceCode.getNodeByRangeIndex(lastToken.range[0]) 125 : null; 126 127 return Boolean(belongingNode) && ( 128 belongingNode.type === "BlockStatement" || 129 belongingNode.type === "SwitchStatement" 130 ); 131} 132 133/** 134 * Check whether the given node is a directive or not. 135 * @param {ASTNode} node The node to check. 136 * @param {SourceCode} sourceCode The source code object to get tokens. 137 * @returns {boolean} `true` if the node is a directive. 138 */ 139function isDirective(node, sourceCode) { 140 return ( 141 node.type === "ExpressionStatement" && 142 ( 143 node.parent.type === "Program" || 144 ( 145 node.parent.type === "BlockStatement" && 146 astUtils.isFunction(node.parent.parent) 147 ) 148 ) && 149 node.expression.type === "Literal" && 150 typeof node.expression.value === "string" && 151 !astUtils.isParenthesised(sourceCode, node.expression) 152 ); 153} 154 155/** 156 * Check whether the given node is a part of directive prologue or not. 157 * @param {ASTNode} node The node to check. 158 * @param {SourceCode} sourceCode The source code object to get tokens. 159 * @returns {boolean} `true` if the node is a part of directive prologue. 160 */ 161function isDirectivePrologue(node, sourceCode) { 162 if (isDirective(node, sourceCode)) { 163 for (const sibling of node.parent.body) { 164 if (sibling === node) { 165 break; 166 } 167 if (!isDirective(sibling, sourceCode)) { 168 return false; 169 } 170 } 171 return true; 172 } 173 return false; 174} 175 176/** 177 * Gets the actual last token. 178 * 179 * If a semicolon is semicolon-less style's semicolon, this ignores it. 180 * For example: 181 * 182 * foo() 183 * ;[1, 2, 3].forEach(bar) 184 * @param {SourceCode} sourceCode The source code to get tokens. 185 * @param {ASTNode} node The node to get. 186 * @returns {Token} The actual last token. 187 * @private 188 */ 189function getActualLastToken(sourceCode, node) { 190 const semiToken = sourceCode.getLastToken(node); 191 const prevToken = sourceCode.getTokenBefore(semiToken); 192 const nextToken = sourceCode.getTokenAfter(semiToken); 193 const isSemicolonLessStyle = Boolean( 194 prevToken && 195 nextToken && 196 prevToken.range[0] >= node.range[0] && 197 astUtils.isSemicolonToken(semiToken) && 198 semiToken.loc.start.line !== prevToken.loc.end.line && 199 semiToken.loc.end.line === nextToken.loc.start.line 200 ); 201 202 return isSemicolonLessStyle ? prevToken : semiToken; 203} 204 205/** 206 * This returns the concatenation of the first 2 captured strings. 207 * @param {string} _ Unused. Whole matched string. 208 * @param {string} trailingSpaces The trailing spaces of the first line. 209 * @param {string} indentSpaces The indentation spaces of the last line. 210 * @returns {string} The concatenation of trailingSpaces and indentSpaces. 211 * @private 212 */ 213function replacerToRemovePaddingLines(_, trailingSpaces, indentSpaces) { 214 return trailingSpaces + indentSpaces; 215} 216 217/** 218 * Check and report statements for `any` configuration. 219 * It does nothing. 220 * @returns {void} 221 * @private 222 */ 223function verifyForAny() { 224} 225 226/** 227 * Check and report statements for `never` configuration. 228 * This autofix removes blank lines between the given 2 statements. 229 * However, if comments exist between 2 blank lines, it does not remove those 230 * blank lines automatically. 231 * @param {RuleContext} context The rule context to report. 232 * @param {ASTNode} _ Unused. The previous node to check. 233 * @param {ASTNode} nextNode The next node to check. 234 * @param {Array<Token[]>} paddingLines The array of token pairs that blank 235 * lines exist between the pair. 236 * @returns {void} 237 * @private 238 */ 239function verifyForNever(context, _, nextNode, paddingLines) { 240 if (paddingLines.length === 0) { 241 return; 242 } 243 244 context.report({ 245 node: nextNode, 246 messageId: "unexpectedBlankLine", 247 fix(fixer) { 248 if (paddingLines.length >= 2) { 249 return null; 250 } 251 252 const prevToken = paddingLines[0][0]; 253 const nextToken = paddingLines[0][1]; 254 const start = prevToken.range[1]; 255 const end = nextToken.range[0]; 256 const text = context.getSourceCode().text 257 .slice(start, end) 258 .replace(PADDING_LINE_SEQUENCE, replacerToRemovePaddingLines); 259 260 return fixer.replaceTextRange([start, end], text); 261 } 262 }); 263} 264 265/** 266 * Check and report statements for `always` configuration. 267 * This autofix inserts a blank line between the given 2 statements. 268 * If the `prevNode` has trailing comments, it inserts a blank line after the 269 * trailing comments. 270 * @param {RuleContext} context The rule context to report. 271 * @param {ASTNode} prevNode The previous node to check. 272 * @param {ASTNode} nextNode The next node to check. 273 * @param {Array<Token[]>} paddingLines The array of token pairs that blank 274 * lines exist between the pair. 275 * @returns {void} 276 * @private 277 */ 278function verifyForAlways(context, prevNode, nextNode, paddingLines) { 279 if (paddingLines.length > 0) { 280 return; 281 } 282 283 context.report({ 284 node: nextNode, 285 messageId: "expectedBlankLine", 286 fix(fixer) { 287 const sourceCode = context.getSourceCode(); 288 let prevToken = getActualLastToken(sourceCode, prevNode); 289 const nextToken = sourceCode.getFirstTokenBetween( 290 prevToken, 291 nextNode, 292 { 293 includeComments: true, 294 295 /** 296 * Skip the trailing comments of the previous node. 297 * This inserts a blank line after the last trailing comment. 298 * 299 * For example: 300 * 301 * foo(); // trailing comment. 302 * // comment. 303 * bar(); 304 * 305 * Get fixed to: 306 * 307 * foo(); // trailing comment. 308 * 309 * // comment. 310 * bar(); 311 * @param {Token} token The token to check. 312 * @returns {boolean} `true` if the token is not a trailing comment. 313 * @private 314 */ 315 filter(token) { 316 if (astUtils.isTokenOnSameLine(prevToken, token)) { 317 prevToken = token; 318 return false; 319 } 320 return true; 321 } 322 } 323 ) || nextNode; 324 const insertText = astUtils.isTokenOnSameLine(prevToken, nextToken) 325 ? "\n\n" 326 : "\n"; 327 328 return fixer.insertTextAfter(prevToken, insertText); 329 } 330 }); 331} 332 333/** 334 * Types of blank lines. 335 * `any`, `never`, and `always` are defined. 336 * Those have `verify` method to check and report statements. 337 * @private 338 */ 339const PaddingTypes = { 340 any: { verify: verifyForAny }, 341 never: { verify: verifyForNever }, 342 always: { verify: verifyForAlways } 343}; 344 345/** 346 * Types of statements. 347 * Those have `test` method to check it matches to the given statement. 348 * @private 349 */ 350const StatementTypes = { 351 "*": { test: () => true }, 352 "block-like": { 353 test: (node, sourceCode) => isBlockLikeStatement(sourceCode, node) 354 }, 355 "cjs-export": { 356 test: (node, sourceCode) => 357 node.type === "ExpressionStatement" && 358 node.expression.type === "AssignmentExpression" && 359 CJS_EXPORT.test(sourceCode.getText(node.expression.left)) 360 }, 361 "cjs-import": { 362 test: (node, sourceCode) => 363 node.type === "VariableDeclaration" && 364 node.declarations.length > 0 && 365 Boolean(node.declarations[0].init) && 366 CJS_IMPORT.test(sourceCode.getText(node.declarations[0].init)) 367 }, 368 directive: { 369 test: isDirectivePrologue 370 }, 371 expression: { 372 test: (node, sourceCode) => 373 node.type === "ExpressionStatement" && 374 !isDirectivePrologue(node, sourceCode) 375 }, 376 iife: { 377 test: isIIFEStatement 378 }, 379 "multiline-block-like": { 380 test: (node, sourceCode) => 381 node.loc.start.line !== node.loc.end.line && 382 isBlockLikeStatement(sourceCode, node) 383 }, 384 "multiline-expression": { 385 test: (node, sourceCode) => 386 node.loc.start.line !== node.loc.end.line && 387 node.type === "ExpressionStatement" && 388 !isDirectivePrologue(node, sourceCode) 389 }, 390 391 "multiline-const": newMultilineKeywordTester("const"), 392 "multiline-let": newMultilineKeywordTester("let"), 393 "multiline-var": newMultilineKeywordTester("var"), 394 "singleline-const": newSinglelineKeywordTester("const"), 395 "singleline-let": newSinglelineKeywordTester("let"), 396 "singleline-var": newSinglelineKeywordTester("var"), 397 398 block: newNodeTypeTester("BlockStatement"), 399 empty: newNodeTypeTester("EmptyStatement"), 400 function: newNodeTypeTester("FunctionDeclaration"), 401 402 break: newKeywordTester("break"), 403 case: newKeywordTester("case"), 404 class: newKeywordTester("class"), 405 const: newKeywordTester("const"), 406 continue: newKeywordTester("continue"), 407 debugger: newKeywordTester("debugger"), 408 default: newKeywordTester("default"), 409 do: newKeywordTester("do"), 410 export: newKeywordTester("export"), 411 for: newKeywordTester("for"), 412 if: newKeywordTester("if"), 413 import: newKeywordTester("import"), 414 let: newKeywordTester("let"), 415 return: newKeywordTester("return"), 416 switch: newKeywordTester("switch"), 417 throw: newKeywordTester("throw"), 418 try: newKeywordTester("try"), 419 var: newKeywordTester("var"), 420 while: newKeywordTester("while"), 421 with: newKeywordTester("with") 422}; 423 424//------------------------------------------------------------------------------ 425// Rule Definition 426//------------------------------------------------------------------------------ 427 428module.exports = { 429 meta: { 430 type: "layout", 431 432 docs: { 433 description: "require or disallow padding lines between statements", 434 category: "Stylistic Issues", 435 recommended: false, 436 url: "https://eslint.org/docs/rules/padding-line-between-statements" 437 }, 438 439 fixable: "whitespace", 440 441 schema: { 442 definitions: { 443 paddingType: { 444 enum: Object.keys(PaddingTypes) 445 }, 446 statementType: { 447 anyOf: [ 448 { enum: Object.keys(StatementTypes) }, 449 { 450 type: "array", 451 items: { enum: Object.keys(StatementTypes) }, 452 minItems: 1, 453 uniqueItems: true, 454 additionalItems: false 455 } 456 ] 457 } 458 }, 459 type: "array", 460 items: { 461 type: "object", 462 properties: { 463 blankLine: { $ref: "#/definitions/paddingType" }, 464 prev: { $ref: "#/definitions/statementType" }, 465 next: { $ref: "#/definitions/statementType" } 466 }, 467 additionalProperties: false, 468 required: ["blankLine", "prev", "next"] 469 }, 470 additionalItems: false 471 }, 472 473 messages: { 474 unexpectedBlankLine: "Unexpected blank line before this statement.", 475 expectedBlankLine: "Expected blank line before this statement." 476 } 477 }, 478 479 create(context) { 480 const sourceCode = context.getSourceCode(); 481 const configureList = context.options || []; 482 let scopeInfo = null; 483 484 /** 485 * Processes to enter to new scope. 486 * This manages the current previous statement. 487 * @returns {void} 488 * @private 489 */ 490 function enterScope() { 491 scopeInfo = { 492 upper: scopeInfo, 493 prevNode: null 494 }; 495 } 496 497 /** 498 * Processes to exit from the current scope. 499 * @returns {void} 500 * @private 501 */ 502 function exitScope() { 503 scopeInfo = scopeInfo.upper; 504 } 505 506 /** 507 * Checks whether the given node matches the given type. 508 * @param {ASTNode} node The statement node to check. 509 * @param {string|string[]} type The statement type to check. 510 * @returns {boolean} `true` if the statement node matched the type. 511 * @private 512 */ 513 function match(node, type) { 514 let innerStatementNode = node; 515 516 while (innerStatementNode.type === "LabeledStatement") { 517 innerStatementNode = innerStatementNode.body; 518 } 519 if (Array.isArray(type)) { 520 return type.some(match.bind(null, innerStatementNode)); 521 } 522 return StatementTypes[type].test(innerStatementNode, sourceCode); 523 } 524 525 /** 526 * Finds the last matched configure from configureList. 527 * @param {ASTNode} prevNode The previous statement to match. 528 * @param {ASTNode} nextNode The current statement to match. 529 * @returns {Object} The tester of the last matched configure. 530 * @private 531 */ 532 function getPaddingType(prevNode, nextNode) { 533 for (let i = configureList.length - 1; i >= 0; --i) { 534 const configure = configureList[i]; 535 const matched = 536 match(prevNode, configure.prev) && 537 match(nextNode, configure.next); 538 539 if (matched) { 540 return PaddingTypes[configure.blankLine]; 541 } 542 } 543 return PaddingTypes.any; 544 } 545 546 /** 547 * Gets padding line sequences between the given 2 statements. 548 * Comments are separators of the padding line sequences. 549 * @param {ASTNode} prevNode The previous statement to count. 550 * @param {ASTNode} nextNode The current statement to count. 551 * @returns {Array<Token[]>} The array of token pairs. 552 * @private 553 */ 554 function getPaddingLineSequences(prevNode, nextNode) { 555 const pairs = []; 556 let prevToken = getActualLastToken(sourceCode, prevNode); 557 558 if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) { 559 do { 560 const token = sourceCode.getTokenAfter( 561 prevToken, 562 { includeComments: true } 563 ); 564 565 if (token.loc.start.line - prevToken.loc.end.line >= 2) { 566 pairs.push([prevToken, token]); 567 } 568 prevToken = token; 569 570 } while (prevToken.range[0] < nextNode.range[0]); 571 } 572 573 return pairs; 574 } 575 576 /** 577 * Verify padding lines between the given node and the previous node. 578 * @param {ASTNode} node The node to verify. 579 * @returns {void} 580 * @private 581 */ 582 function verify(node) { 583 const parentType = node.parent.type; 584 const validParent = 585 astUtils.STATEMENT_LIST_PARENTS.has(parentType) || 586 parentType === "SwitchStatement"; 587 588 if (!validParent) { 589 return; 590 } 591 592 // Save this node as the current previous statement. 593 const prevNode = scopeInfo.prevNode; 594 595 // Verify. 596 if (prevNode) { 597 const type = getPaddingType(prevNode, node); 598 const paddingLines = getPaddingLineSequences(prevNode, node); 599 600 type.verify(context, prevNode, node, paddingLines); 601 } 602 603 scopeInfo.prevNode = node; 604 } 605 606 /** 607 * Verify padding lines between the given node and the previous node. 608 * Then process to enter to new scope. 609 * @param {ASTNode} node The node to verify. 610 * @returns {void} 611 * @private 612 */ 613 function verifyThenEnterScope(node) { 614 verify(node); 615 enterScope(); 616 } 617 618 return { 619 Program: enterScope, 620 BlockStatement: enterScope, 621 SwitchStatement: enterScope, 622 "Program:exit": exitScope, 623 "BlockStatement:exit": exitScope, 624 "SwitchStatement:exit": exitScope, 625 626 ":statement": verify, 627 628 SwitchCase: verifyThenEnterScope, 629 "SwitchCase:exit": exitScope 630 }; 631 } 632}; 633