padding-line-between-statements.js 20 KB

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