padding-line-between-statements.js 20 KB

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