lines-around-comment.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. /**
  2. * @fileoverview Enforces empty lines around comments.
  3. * @author Jamund Ferguson
  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. /**
  15. * Return an array with any line numbers that are empty.
  16. * @param {Array} lines An array of each line of the file.
  17. * @returns {Array} An array of line numbers.
  18. */
  19. function getEmptyLineNums(lines) {
  20. const emptyLines = lines
  21. .map((line, i) => ({
  22. code: line.trim(),
  23. num: i + 1,
  24. }))
  25. .filter(line => !line.code)
  26. .map(line => line.num);
  27. return emptyLines;
  28. }
  29. /**
  30. * Return an array with any line numbers that contain comments.
  31. * @param {Array} comments An array of comment tokens.
  32. * @returns {Array} An array of line numbers.
  33. */
  34. function getCommentLineNums(comments) {
  35. const lines = [];
  36. comments.forEach(token => {
  37. const start = token.loc.start.line;
  38. const end = token.loc.end.line;
  39. lines.push(start, end);
  40. });
  41. return lines;
  42. }
  43. //------------------------------------------------------------------------------
  44. // Rule Definition
  45. //------------------------------------------------------------------------------
  46. /** @type {import('../types').Rule.RuleModule} */
  47. module.exports = {
  48. meta: {
  49. deprecated: {
  50. message: "Formatting rules are being moved out of ESLint core.",
  51. url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
  52. deprecatedSince: "8.53.0",
  53. availableUntil: "11.0.0",
  54. replacedBy: [
  55. {
  56. message:
  57. "ESLint Stylistic now maintains deprecated stylistic core rules.",
  58. url: "https://eslint.style/guide/migration",
  59. plugin: {
  60. name: "@stylistic/eslint-plugin",
  61. url: "https://eslint.style",
  62. },
  63. rule: {
  64. name: "lines-around-comment",
  65. url: "https://eslint.style/rules/lines-around-comment",
  66. },
  67. },
  68. ],
  69. },
  70. type: "layout",
  71. docs: {
  72. description: "Require empty lines around comments",
  73. recommended: false,
  74. url: "https://eslint.org/docs/latest/rules/lines-around-comment",
  75. },
  76. fixable: "whitespace",
  77. schema: [
  78. {
  79. type: "object",
  80. properties: {
  81. beforeBlockComment: {
  82. type: "boolean",
  83. default: true,
  84. },
  85. afterBlockComment: {
  86. type: "boolean",
  87. default: false,
  88. },
  89. beforeLineComment: {
  90. type: "boolean",
  91. default: false,
  92. },
  93. afterLineComment: {
  94. type: "boolean",
  95. default: false,
  96. },
  97. allowBlockStart: {
  98. type: "boolean",
  99. default: false,
  100. },
  101. allowBlockEnd: {
  102. type: "boolean",
  103. default: false,
  104. },
  105. allowClassStart: {
  106. type: "boolean",
  107. },
  108. allowClassEnd: {
  109. type: "boolean",
  110. },
  111. allowObjectStart: {
  112. type: "boolean",
  113. },
  114. allowObjectEnd: {
  115. type: "boolean",
  116. },
  117. allowArrayStart: {
  118. type: "boolean",
  119. },
  120. allowArrayEnd: {
  121. type: "boolean",
  122. },
  123. ignorePattern: {
  124. type: "string",
  125. },
  126. applyDefaultIgnorePatterns: {
  127. type: "boolean",
  128. },
  129. afterHashbangComment: {
  130. type: "boolean",
  131. default: false,
  132. },
  133. },
  134. additionalProperties: false,
  135. },
  136. ],
  137. messages: {
  138. after: "Expected line after comment.",
  139. before: "Expected line before comment.",
  140. },
  141. },
  142. create(context) {
  143. const options = Object.assign({}, context.options[0]);
  144. const ignorePattern = options.ignorePattern;
  145. const defaultIgnoreRegExp = astUtils.COMMENTS_IGNORE_PATTERN;
  146. const customIgnoreRegExp = new RegExp(ignorePattern, "u");
  147. const applyDefaultIgnorePatterns =
  148. options.applyDefaultIgnorePatterns !== false;
  149. options.beforeBlockComment =
  150. typeof options.beforeBlockComment !== "undefined"
  151. ? options.beforeBlockComment
  152. : true;
  153. const sourceCode = context.sourceCode;
  154. const lines = sourceCode.lines,
  155. numLines = lines.length + 1,
  156. comments = sourceCode.getAllComments(),
  157. commentLines = getCommentLineNums(comments),
  158. emptyLines = getEmptyLineNums(lines),
  159. commentAndEmptyLines = new Set(commentLines.concat(emptyLines));
  160. /**
  161. * Returns whether or not comments are on lines starting with or ending with code
  162. * @param {token} token The comment token to check.
  163. * @returns {boolean} True if the comment is not alone.
  164. */
  165. function codeAroundComment(token) {
  166. let currentToken = token;
  167. do {
  168. currentToken = sourceCode.getTokenBefore(currentToken, {
  169. includeComments: true,
  170. });
  171. } while (currentToken && astUtils.isCommentToken(currentToken));
  172. if (
  173. currentToken &&
  174. astUtils.isTokenOnSameLine(currentToken, token)
  175. ) {
  176. return true;
  177. }
  178. currentToken = token;
  179. do {
  180. currentToken = sourceCode.getTokenAfter(currentToken, {
  181. includeComments: true,
  182. });
  183. } while (currentToken && astUtils.isCommentToken(currentToken));
  184. if (
  185. currentToken &&
  186. astUtils.isTokenOnSameLine(token, currentToken)
  187. ) {
  188. return true;
  189. }
  190. return false;
  191. }
  192. /**
  193. * Returns whether or not comments are inside a node type or not.
  194. * @param {ASTNode} parent The Comment parent node.
  195. * @param {string} nodeType The parent type to check against.
  196. * @returns {boolean} True if the comment is inside nodeType.
  197. */
  198. function isParentNodeType(parent, nodeType) {
  199. return (
  200. parent.type === nodeType ||
  201. (parent.body && parent.body.type === nodeType) ||
  202. (parent.consequent && parent.consequent.type === nodeType)
  203. );
  204. }
  205. /**
  206. * Returns the parent node that contains the given token.
  207. * @param {token} token The token to check.
  208. * @returns {ASTNode|null} The parent node that contains the given token.
  209. */
  210. function getParentNodeOfToken(token) {
  211. const node = sourceCode.getNodeByRangeIndex(token.range[0]);
  212. /*
  213. * For the purpose of this rule, the comment token is in a `StaticBlock` node only
  214. * if it's inside the braces of that `StaticBlock` node.
  215. *
  216. * Example where this function returns `null`:
  217. *
  218. * static
  219. * // comment
  220. * {
  221. * }
  222. *
  223. * Example where this function returns `StaticBlock` node:
  224. *
  225. * static
  226. * {
  227. * // comment
  228. * }
  229. *
  230. */
  231. if (node && node.type === "StaticBlock") {
  232. const openingBrace = sourceCode.getFirstToken(node, {
  233. skip: 1,
  234. }); // skip the `static` token
  235. return token.range[0] >= openingBrace.range[0] ? node : null;
  236. }
  237. return node;
  238. }
  239. /**
  240. * Returns whether or not comments are at the parent start or not.
  241. * @param {token} token The Comment token.
  242. * @param {string} nodeType The parent type to check against.
  243. * @returns {boolean} True if the comment is at parent start.
  244. */
  245. function isCommentAtParentStart(token, nodeType) {
  246. const parent = getParentNodeOfToken(token);
  247. if (parent && isParentNodeType(parent, nodeType)) {
  248. let parentStartNodeOrToken = parent;
  249. if (parent.type === "StaticBlock") {
  250. parentStartNodeOrToken = sourceCode.getFirstToken(parent, {
  251. skip: 1,
  252. }); // opening brace of the static block
  253. } else if (parent.type === "SwitchStatement") {
  254. parentStartNodeOrToken = sourceCode.getTokenAfter(
  255. parent.discriminant,
  256. {
  257. filter: astUtils.isOpeningBraceToken,
  258. },
  259. ); // opening brace of the switch statement
  260. }
  261. return (
  262. token.loc.start.line -
  263. parentStartNodeOrToken.loc.start.line ===
  264. 1
  265. );
  266. }
  267. return false;
  268. }
  269. /**
  270. * Returns whether or not comments are at the parent end or not.
  271. * @param {token} token The Comment token.
  272. * @param {string} nodeType The parent type to check against.
  273. * @returns {boolean} True if the comment is at parent end.
  274. */
  275. function isCommentAtParentEnd(token, nodeType) {
  276. const parent = getParentNodeOfToken(token);
  277. return (
  278. !!parent &&
  279. isParentNodeType(parent, nodeType) &&
  280. parent.loc.end.line - token.loc.end.line === 1
  281. );
  282. }
  283. /**
  284. * Returns whether or not comments are at the block start or not.
  285. * @param {token} token The Comment token.
  286. * @returns {boolean} True if the comment is at block start.
  287. */
  288. function isCommentAtBlockStart(token) {
  289. return (
  290. isCommentAtParentStart(token, "ClassBody") ||
  291. isCommentAtParentStart(token, "BlockStatement") ||
  292. isCommentAtParentStart(token, "StaticBlock") ||
  293. isCommentAtParentStart(token, "SwitchCase") ||
  294. isCommentAtParentStart(token, "SwitchStatement")
  295. );
  296. }
  297. /**
  298. * Returns whether or not comments are at the block end or not.
  299. * @param {token} token The Comment token.
  300. * @returns {boolean} True if the comment is at block end.
  301. */
  302. function isCommentAtBlockEnd(token) {
  303. return (
  304. isCommentAtParentEnd(token, "ClassBody") ||
  305. isCommentAtParentEnd(token, "BlockStatement") ||
  306. isCommentAtParentEnd(token, "StaticBlock") ||
  307. isCommentAtParentEnd(token, "SwitchCase") ||
  308. isCommentAtParentEnd(token, "SwitchStatement")
  309. );
  310. }
  311. /**
  312. * Returns whether or not comments are at the class start or not.
  313. * @param {token} token The Comment token.
  314. * @returns {boolean} True if the comment is at class start.
  315. */
  316. function isCommentAtClassStart(token) {
  317. return isCommentAtParentStart(token, "ClassBody");
  318. }
  319. /**
  320. * Returns whether or not comments are at the class end or not.
  321. * @param {token} token The Comment token.
  322. * @returns {boolean} True if the comment is at class end.
  323. */
  324. function isCommentAtClassEnd(token) {
  325. return isCommentAtParentEnd(token, "ClassBody");
  326. }
  327. /**
  328. * Returns whether or not comments are at the object start or not.
  329. * @param {token} token The Comment token.
  330. * @returns {boolean} True if the comment is at object start.
  331. */
  332. function isCommentAtObjectStart(token) {
  333. return (
  334. isCommentAtParentStart(token, "ObjectExpression") ||
  335. isCommentAtParentStart(token, "ObjectPattern")
  336. );
  337. }
  338. /**
  339. * Returns whether or not comments are at the object end or not.
  340. * @param {token} token The Comment token.
  341. * @returns {boolean} True if the comment is at object end.
  342. */
  343. function isCommentAtObjectEnd(token) {
  344. return (
  345. isCommentAtParentEnd(token, "ObjectExpression") ||
  346. isCommentAtParentEnd(token, "ObjectPattern")
  347. );
  348. }
  349. /**
  350. * Returns whether or not comments are at the array start or not.
  351. * @param {token} token The Comment token.
  352. * @returns {boolean} True if the comment is at array start.
  353. */
  354. function isCommentAtArrayStart(token) {
  355. return (
  356. isCommentAtParentStart(token, "ArrayExpression") ||
  357. isCommentAtParentStart(token, "ArrayPattern")
  358. );
  359. }
  360. /**
  361. * Returns whether or not comments are at the array end or not.
  362. * @param {token} token The Comment token.
  363. * @returns {boolean} True if the comment is at array end.
  364. */
  365. function isCommentAtArrayEnd(token) {
  366. return (
  367. isCommentAtParentEnd(token, "ArrayExpression") ||
  368. isCommentAtParentEnd(token, "ArrayPattern")
  369. );
  370. }
  371. /**
  372. * Checks if a comment token has lines around it (ignores inline comments)
  373. * @param {token} token The Comment token.
  374. * @param {Object} opts Options to determine the newline.
  375. * @param {boolean} opts.after Should have a newline after this line.
  376. * @param {boolean} opts.before Should have a newline before this line.
  377. * @returns {void}
  378. */
  379. function checkForEmptyLine(token, opts) {
  380. if (
  381. applyDefaultIgnorePatterns &&
  382. defaultIgnoreRegExp.test(token.value)
  383. ) {
  384. return;
  385. }
  386. if (ignorePattern && customIgnoreRegExp.test(token.value)) {
  387. return;
  388. }
  389. let after = opts.after,
  390. before = opts.before;
  391. const prevLineNum = token.loc.start.line - 1,
  392. nextLineNum = token.loc.end.line + 1,
  393. commentIsNotAlone = codeAroundComment(token);
  394. const blockStartAllowed =
  395. options.allowBlockStart &&
  396. isCommentAtBlockStart(token) &&
  397. !(
  398. options.allowClassStart === false &&
  399. isCommentAtClassStart(token)
  400. ),
  401. blockEndAllowed =
  402. options.allowBlockEnd &&
  403. isCommentAtBlockEnd(token) &&
  404. !(
  405. options.allowClassEnd === false &&
  406. isCommentAtClassEnd(token)
  407. ),
  408. classStartAllowed =
  409. options.allowClassStart && isCommentAtClassStart(token),
  410. classEndAllowed =
  411. options.allowClassEnd && isCommentAtClassEnd(token),
  412. objectStartAllowed =
  413. options.allowObjectStart && isCommentAtObjectStart(token),
  414. objectEndAllowed =
  415. options.allowObjectEnd && isCommentAtObjectEnd(token),
  416. arrayStartAllowed =
  417. options.allowArrayStart && isCommentAtArrayStart(token),
  418. arrayEndAllowed =
  419. options.allowArrayEnd && isCommentAtArrayEnd(token);
  420. const exceptionStartAllowed =
  421. blockStartAllowed ||
  422. classStartAllowed ||
  423. objectStartAllowed ||
  424. arrayStartAllowed;
  425. const exceptionEndAllowed =
  426. blockEndAllowed ||
  427. classEndAllowed ||
  428. objectEndAllowed ||
  429. arrayEndAllowed;
  430. // ignore top of the file and bottom of the file
  431. if (prevLineNum < 1) {
  432. before = false;
  433. }
  434. if (nextLineNum >= numLines) {
  435. after = false;
  436. }
  437. // we ignore all inline comments
  438. if (commentIsNotAlone) {
  439. return;
  440. }
  441. const previousTokenOrComment = sourceCode.getTokenBefore(token, {
  442. includeComments: true,
  443. });
  444. const nextTokenOrComment = sourceCode.getTokenAfter(token, {
  445. includeComments: true,
  446. });
  447. // check for newline before
  448. if (
  449. !exceptionStartAllowed &&
  450. before &&
  451. !commentAndEmptyLines.has(prevLineNum) &&
  452. !(
  453. astUtils.isCommentToken(previousTokenOrComment) &&
  454. astUtils.isTokenOnSameLine(previousTokenOrComment, token)
  455. )
  456. ) {
  457. const lineStart = token.range[0] - token.loc.start.column;
  458. const range = [lineStart, lineStart];
  459. context.report({
  460. node: token,
  461. messageId: "before",
  462. fix(fixer) {
  463. return fixer.insertTextBeforeRange(range, "\n");
  464. },
  465. });
  466. }
  467. // check for newline after
  468. if (
  469. !exceptionEndAllowed &&
  470. after &&
  471. !commentAndEmptyLines.has(nextLineNum) &&
  472. !(
  473. astUtils.isCommentToken(nextTokenOrComment) &&
  474. astUtils.isTokenOnSameLine(token, nextTokenOrComment)
  475. )
  476. ) {
  477. context.report({
  478. node: token,
  479. messageId: "after",
  480. fix(fixer) {
  481. return fixer.insertTextAfter(token, "\n");
  482. },
  483. });
  484. }
  485. }
  486. //--------------------------------------------------------------------------
  487. // Public
  488. //--------------------------------------------------------------------------
  489. return {
  490. Program() {
  491. comments.forEach(token => {
  492. if (token.type === "Line") {
  493. if (
  494. options.beforeLineComment ||
  495. options.afterLineComment
  496. ) {
  497. checkForEmptyLine(token, {
  498. after: options.afterLineComment,
  499. before: options.beforeLineComment,
  500. });
  501. }
  502. } else if (token.type === "Block") {
  503. if (
  504. options.beforeBlockComment ||
  505. options.afterBlockComment
  506. ) {
  507. checkForEmptyLine(token, {
  508. after: options.afterBlockComment,
  509. before: options.beforeBlockComment,
  510. });
  511. }
  512. } else if (token.type === "Shebang") {
  513. if (options.afterHashbangComment) {
  514. checkForEmptyLine(token, {
  515. after: options.afterHashbangComment,
  516. before: false,
  517. });
  518. }
  519. }
  520. });
  521. },
  522. };
  523. },
  524. };