padding-line-between-statements.js 17 KB

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