padding-line-between-statements.js 19 KB

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