multiline-comment-style.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. /**
  2. * @fileoverview enforce a particular style for multiline comments
  3. * @author Teddy Katz
  4. * @deprecated in ESLint v9.3.0
  5. */
  6. "use strict";
  7. const astUtils = require("./utils/ast-utils");
  8. //------------------------------------------------------------------------------
  9. // Rule Definition
  10. //------------------------------------------------------------------------------
  11. /** @type {import('../types').Rule.RuleModule} */
  12. module.exports = {
  13. meta: {
  14. deprecated: {
  15. message: "Formatting rules are being moved out of ESLint core.",
  16. url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
  17. deprecatedSince: "9.3.0",
  18. availableUntil: "11.0.0",
  19. replacedBy: [
  20. {
  21. message:
  22. "ESLint Stylistic now maintains deprecated stylistic core rules.",
  23. url: "https://eslint.style/guide/migration",
  24. plugin: {
  25. name: "@stylistic/eslint-plugin",
  26. url: "https://eslint.style",
  27. },
  28. rule: {
  29. name: "multiline-comment-style",
  30. url: "https://eslint.style/rules/multiline-comment-style",
  31. },
  32. },
  33. ],
  34. },
  35. type: "suggestion",
  36. docs: {
  37. description: "Enforce a particular style for multiline comments",
  38. recommended: false,
  39. url: "https://eslint.org/docs/latest/rules/multiline-comment-style",
  40. },
  41. fixable: "whitespace",
  42. schema: {
  43. anyOf: [
  44. {
  45. type: "array",
  46. items: [
  47. {
  48. enum: ["starred-block", "bare-block"],
  49. },
  50. ],
  51. additionalItems: false,
  52. },
  53. {
  54. type: "array",
  55. items: [
  56. {
  57. enum: ["separate-lines"],
  58. },
  59. {
  60. type: "object",
  61. properties: {
  62. checkJSDoc: {
  63. type: "boolean",
  64. },
  65. },
  66. additionalProperties: false,
  67. },
  68. ],
  69. additionalItems: false,
  70. },
  71. ],
  72. },
  73. messages: {
  74. expectedBlock:
  75. "Expected a block comment instead of consecutive line comments.",
  76. expectedBareBlock:
  77. "Expected a block comment without padding stars.",
  78. startNewline: "Expected a linebreak after '/*'.",
  79. endNewline: "Expected a linebreak before '*/'.",
  80. missingStar: "Expected a '*' at the start of this line.",
  81. alignment:
  82. "Expected this line to be aligned with the start of the comment.",
  83. expectedLines:
  84. "Expected multiple line comments instead of a block comment.",
  85. },
  86. },
  87. create(context) {
  88. const sourceCode = context.sourceCode;
  89. const option = context.options[0] || "starred-block";
  90. const params = context.options[1] || {};
  91. const checkJSDoc = !!params.checkJSDoc;
  92. //----------------------------------------------------------------------
  93. // Helpers
  94. //----------------------------------------------------------------------
  95. /**
  96. * Checks if a comment line is starred.
  97. * @param {string} line A string representing a comment line.
  98. * @returns {boolean} Whether or not the comment line is starred.
  99. */
  100. function isStarredCommentLine(line) {
  101. return /^\s*\*/u.test(line);
  102. }
  103. /**
  104. * Checks if a comment group is in starred-block form.
  105. * @param {Token[]} commentGroup A group of comments, containing either multiple line comments or a single block comment.
  106. * @returns {boolean} Whether or not the comment group is in starred block form.
  107. */
  108. function isStarredBlockComment([firstComment]) {
  109. if (firstComment.type !== "Block") {
  110. return false;
  111. }
  112. const lines = firstComment.value.split(astUtils.LINEBREAK_MATCHER);
  113. // The first and last lines can only contain whitespace.
  114. return (
  115. lines.length > 0 &&
  116. lines.every((line, i) =>
  117. (i === 0 || i === lines.length - 1
  118. ? /^\s*$/u
  119. : /^\s*\*/u
  120. ).test(line),
  121. )
  122. );
  123. }
  124. /**
  125. * Checks if a comment group is in JSDoc form.
  126. * @param {Token[]} commentGroup A group of comments, containing either multiple line comments or a single block comment.
  127. * @returns {boolean} Whether or not the comment group is in JSDoc form.
  128. */
  129. function isJSDocComment([firstComment]) {
  130. if (firstComment.type !== "Block") {
  131. return false;
  132. }
  133. const lines = firstComment.value.split(astUtils.LINEBREAK_MATCHER);
  134. return (
  135. /^\*\s*$/u.test(lines[0]) &&
  136. lines.slice(1, -1).every(line => /^\s* /u.test(line)) &&
  137. /^\s*$/u.test(lines.at(-1))
  138. );
  139. }
  140. /**
  141. * Processes a comment group that is currently in separate-line form, calculating the offset for each line.
  142. * @param {Token[]} commentGroup A group of comments containing multiple line comments.
  143. * @returns {string[]} An array of the processed lines.
  144. */
  145. function processSeparateLineComments(commentGroup) {
  146. const allLinesHaveLeadingSpace = commentGroup
  147. .map(({ value }) => value)
  148. .filter(line => line.trim().length)
  149. .every(line => line.startsWith(" "));
  150. return commentGroup.map(({ value }) =>
  151. allLinesHaveLeadingSpace ? value.replace(/^ /u, "") : value,
  152. );
  153. }
  154. /**
  155. * Processes a comment group that is currently in starred-block form, calculating the offset for each line.
  156. * @param {Token} comment A single block comment token in starred-block form.
  157. * @returns {string[]} An array of the processed lines.
  158. */
  159. function processStarredBlockComment(comment) {
  160. const lines = comment.value
  161. .split(astUtils.LINEBREAK_MATCHER)
  162. .filter(
  163. (line, i, linesArr) =>
  164. !(i === 0 || i === linesArr.length - 1),
  165. )
  166. .map(line => line.replace(/^\s*$/u, ""));
  167. const allLinesHaveLeadingSpace = lines
  168. .map(line => line.replace(/\s*\*/u, ""))
  169. .filter(line => line.trim().length)
  170. .every(line => line.startsWith(" "));
  171. return lines.map(line =>
  172. line.replace(
  173. allLinesHaveLeadingSpace ? /\s*\* ?/u : /\s*\*/u,
  174. "",
  175. ),
  176. );
  177. }
  178. /**
  179. * Processes a comment group that is currently in bare-block form, calculating the offset for each line.
  180. * @param {Token} comment A single block comment token in bare-block form.
  181. * @returns {string[]} An array of the processed lines.
  182. */
  183. function processBareBlockComment(comment) {
  184. const lines = comment.value
  185. .split(astUtils.LINEBREAK_MATCHER)
  186. .map(line => line.replace(/^\s*$/u, ""));
  187. const leadingWhitespace = `${sourceCode.text.slice(comment.range[0] - comment.loc.start.column, comment.range[0])} `;
  188. let offset = "";
  189. /*
  190. * Calculate the offset of the least indented line and use that as the basis for offsetting all the lines.
  191. * The first line should not be checked because it is inline with the opening block comment delimiter.
  192. */
  193. for (const [i, line] of lines.entries()) {
  194. if (!line.trim().length || i === 0) {
  195. continue;
  196. }
  197. const [, lineOffset] = line.match(/^(\s*\*?\s*)/u);
  198. if (lineOffset.length < leadingWhitespace.length) {
  199. const newOffset = leadingWhitespace.slice(
  200. lineOffset.length - leadingWhitespace.length,
  201. );
  202. if (newOffset.length > offset.length) {
  203. offset = newOffset;
  204. }
  205. }
  206. }
  207. return lines.map(line => {
  208. const match = line.match(/^(\s*\*?\s*)(.*)/u);
  209. const [, lineOffset, lineContents] = match;
  210. if (lineOffset.length > leadingWhitespace.length) {
  211. return `${lineOffset.slice(leadingWhitespace.length - (offset.length + lineOffset.length))}${lineContents}`;
  212. }
  213. if (lineOffset.length < leadingWhitespace.length) {
  214. return `${lineOffset.slice(leadingWhitespace.length)}${lineContents}`;
  215. }
  216. return lineContents;
  217. });
  218. }
  219. /**
  220. * Gets a list of comment lines in a group, formatting leading whitespace as necessary.
  221. * @param {Token[]} commentGroup A group of comments containing either multiple line comments or a single block comment.
  222. * @returns {string[]} A list of comment lines.
  223. */
  224. function getCommentLines(commentGroup) {
  225. const [firstComment] = commentGroup;
  226. if (firstComment.type === "Line") {
  227. return processSeparateLineComments(commentGroup);
  228. }
  229. if (isStarredBlockComment(commentGroup)) {
  230. return processStarredBlockComment(firstComment);
  231. }
  232. return processBareBlockComment(firstComment);
  233. }
  234. /**
  235. * Gets the initial offset (whitespace) from the beginning of a line to a given comment token.
  236. * @param {Token} comment The token to check.
  237. * @returns {string} The offset from the beginning of a line to the token.
  238. */
  239. function getInitialOffset(comment) {
  240. return sourceCode.text.slice(
  241. comment.range[0] - comment.loc.start.column,
  242. comment.range[0],
  243. );
  244. }
  245. /**
  246. * Converts a comment into starred-block form
  247. * @param {Token} firstComment The first comment of the group being converted
  248. * @param {string[]} commentLinesList A list of lines to appear in the new starred-block comment
  249. * @returns {string} A representation of the comment value in starred-block form, excluding start and end markers
  250. */
  251. function convertToStarredBlock(firstComment, commentLinesList) {
  252. const initialOffset = getInitialOffset(firstComment);
  253. return `/*\n${commentLinesList.map(line => `${initialOffset} * ${line}`).join("\n")}\n${initialOffset} */`;
  254. }
  255. /**
  256. * Converts a comment into separate-line form
  257. * @param {Token} firstComment The first comment of the group being converted
  258. * @param {string[]} commentLinesList A list of lines to appear in the new starred-block comment
  259. * @returns {string} A representation of the comment value in separate-line form
  260. */
  261. function convertToSeparateLines(firstComment, commentLinesList) {
  262. return commentLinesList
  263. .map(line => `// ${line}`)
  264. .join(`\n${getInitialOffset(firstComment)}`);
  265. }
  266. /**
  267. * Converts a comment into bare-block form
  268. * @param {Token} firstComment The first comment of the group being converted
  269. * @param {string[]} commentLinesList A list of lines to appear in the new starred-block comment
  270. * @returns {string} A representation of the comment value in bare-block form
  271. */
  272. function convertToBlock(firstComment, commentLinesList) {
  273. return `/* ${commentLinesList.join(`\n${getInitialOffset(firstComment)} `)} */`;
  274. }
  275. /**
  276. * Each method checks a group of comments to see if it's valid according to the given option.
  277. * @param {Token[]} commentGroup A list of comments that appear together. This will either contain a single
  278. * block comment or multiple line comments.
  279. * @returns {void}
  280. */
  281. const commentGroupCheckers = {
  282. "starred-block"(commentGroup) {
  283. const [firstComment] = commentGroup;
  284. const commentLines = getCommentLines(commentGroup);
  285. if (commentLines.some(value => value.includes("*/"))) {
  286. return;
  287. }
  288. if (commentGroup.length > 1) {
  289. context.report({
  290. loc: {
  291. start: firstComment.loc.start,
  292. end: commentGroup.at(-1).loc.end,
  293. },
  294. messageId: "expectedBlock",
  295. fix(fixer) {
  296. const range = [
  297. firstComment.range[0],
  298. commentGroup.at(-1).range[1],
  299. ];
  300. return commentLines.some(value =>
  301. value.startsWith("/"),
  302. )
  303. ? null
  304. : fixer.replaceTextRange(
  305. range,
  306. convertToStarredBlock(
  307. firstComment,
  308. commentLines,
  309. ),
  310. );
  311. },
  312. });
  313. } else {
  314. const lines = firstComment.value.split(
  315. astUtils.LINEBREAK_MATCHER,
  316. );
  317. const expectedLeadingWhitespace =
  318. getInitialOffset(firstComment);
  319. const expectedLinePrefix = `${expectedLeadingWhitespace} *`;
  320. if (!/^\*?\s*$/u.test(lines[0])) {
  321. const start = firstComment.value.startsWith("*")
  322. ? firstComment.range[0] + 1
  323. : firstComment.range[0];
  324. context.report({
  325. loc: {
  326. start: firstComment.loc.start,
  327. end: {
  328. line: firstComment.loc.start.line,
  329. column: firstComment.loc.start.column + 2,
  330. },
  331. },
  332. messageId: "startNewline",
  333. fix: fixer =>
  334. fixer.insertTextAfterRange(
  335. [start, start + 2],
  336. `\n${expectedLinePrefix}`,
  337. ),
  338. });
  339. }
  340. if (!/^\s*$/u.test(lines.at(-1))) {
  341. context.report({
  342. loc: {
  343. start: {
  344. line: firstComment.loc.end.line,
  345. column: firstComment.loc.end.column - 2,
  346. },
  347. end: firstComment.loc.end,
  348. },
  349. messageId: "endNewline",
  350. fix: fixer =>
  351. fixer.replaceTextRange(
  352. [
  353. firstComment.range[1] - 2,
  354. firstComment.range[1],
  355. ],
  356. `\n${expectedLinePrefix}/`,
  357. ),
  358. });
  359. }
  360. for (
  361. let lineNumber = firstComment.loc.start.line + 1;
  362. lineNumber <= firstComment.loc.end.line;
  363. lineNumber++
  364. ) {
  365. const lineText = sourceCode.lines[lineNumber - 1];
  366. const errorType = isStarredCommentLine(lineText)
  367. ? "alignment"
  368. : "missingStar";
  369. if (!lineText.startsWith(expectedLinePrefix)) {
  370. context.report({
  371. loc: {
  372. start: { line: lineNumber, column: 0 },
  373. end: {
  374. line: lineNumber,
  375. column: lineText.length,
  376. },
  377. },
  378. messageId: errorType,
  379. fix(fixer) {
  380. const lineStartIndex =
  381. sourceCode.getIndexFromLoc({
  382. line: lineNumber,
  383. column: 0,
  384. });
  385. if (errorType === "alignment") {
  386. const [, commentTextPrefix = ""] =
  387. lineText.match(/^(\s*\*)/u) || [];
  388. const commentTextStartIndex =
  389. lineStartIndex +
  390. commentTextPrefix.length;
  391. return fixer.replaceTextRange(
  392. [
  393. lineStartIndex,
  394. commentTextStartIndex,
  395. ],
  396. expectedLinePrefix,
  397. );
  398. }
  399. const [, commentTextPrefix = ""] =
  400. lineText.match(/^(\s*)/u) || [];
  401. const commentTextStartIndex =
  402. lineStartIndex +
  403. commentTextPrefix.length;
  404. let offset;
  405. for (const [idx, line] of lines.entries()) {
  406. if (!/\S+/u.test(line)) {
  407. continue;
  408. }
  409. const lineTextToAlignWith =
  410. sourceCode.lines[
  411. firstComment.loc.start.line -
  412. 1 +
  413. idx
  414. ];
  415. const [
  416. ,
  417. prefix = "",
  418. initialOffset = "",
  419. ] =
  420. lineTextToAlignWith.match(
  421. /^(\s*(?:\/?\*)?(\s*))/u,
  422. ) || [];
  423. offset = `${commentTextPrefix.slice(prefix.length)}${initialOffset}`;
  424. if (
  425. /^\s*\//u.test(lineText) &&
  426. offset.length === 0
  427. ) {
  428. offset += " ";
  429. }
  430. break;
  431. }
  432. return fixer.replaceTextRange(
  433. [lineStartIndex, commentTextStartIndex],
  434. `${expectedLinePrefix}${offset}`,
  435. );
  436. },
  437. });
  438. }
  439. }
  440. }
  441. },
  442. "separate-lines"(commentGroup) {
  443. const [firstComment] = commentGroup;
  444. const isJSDoc = isJSDocComment(commentGroup);
  445. if (firstComment.type !== "Block" || (!checkJSDoc && isJSDoc)) {
  446. return;
  447. }
  448. let commentLines = getCommentLines(commentGroup);
  449. if (isJSDoc) {
  450. commentLines = commentLines.slice(
  451. 1,
  452. commentLines.length - 1,
  453. );
  454. }
  455. const tokenAfter = sourceCode.getTokenAfter(firstComment, {
  456. includeComments: true,
  457. });
  458. if (
  459. tokenAfter &&
  460. firstComment.loc.end.line === tokenAfter.loc.start.line
  461. ) {
  462. return;
  463. }
  464. context.report({
  465. loc: {
  466. start: firstComment.loc.start,
  467. end: {
  468. line: firstComment.loc.start.line,
  469. column: firstComment.loc.start.column + 2,
  470. },
  471. },
  472. messageId: "expectedLines",
  473. fix(fixer) {
  474. return fixer.replaceText(
  475. firstComment,
  476. convertToSeparateLines(firstComment, commentLines),
  477. );
  478. },
  479. });
  480. },
  481. "bare-block"(commentGroup) {
  482. if (isJSDocComment(commentGroup)) {
  483. return;
  484. }
  485. const [firstComment] = commentGroup;
  486. const commentLines = getCommentLines(commentGroup);
  487. // Disallows consecutive line comments in favor of using a block comment.
  488. if (
  489. firstComment.type === "Line" &&
  490. commentLines.length > 1 &&
  491. !commentLines.some(value => value.includes("*/"))
  492. ) {
  493. context.report({
  494. loc: {
  495. start: firstComment.loc.start,
  496. end: commentGroup.at(-1).loc.end,
  497. },
  498. messageId: "expectedBlock",
  499. fix(fixer) {
  500. return fixer.replaceTextRange(
  501. [
  502. firstComment.range[0],
  503. commentGroup.at(-1).range[1],
  504. ],
  505. convertToBlock(firstComment, commentLines),
  506. );
  507. },
  508. });
  509. }
  510. // Prohibits block comments from having a * at the beginning of each line.
  511. if (isStarredBlockComment(commentGroup)) {
  512. context.report({
  513. loc: {
  514. start: firstComment.loc.start,
  515. end: {
  516. line: firstComment.loc.start.line,
  517. column: firstComment.loc.start.column + 2,
  518. },
  519. },
  520. messageId: "expectedBareBlock",
  521. fix(fixer) {
  522. return fixer.replaceText(
  523. firstComment,
  524. convertToBlock(firstComment, commentLines),
  525. );
  526. },
  527. });
  528. }
  529. },
  530. };
  531. //----------------------------------------------------------------------
  532. // Public
  533. //----------------------------------------------------------------------
  534. return {
  535. Program() {
  536. return sourceCode
  537. .getAllComments()
  538. .filter(comment => comment.type !== "Shebang")
  539. .filter(
  540. comment =>
  541. !astUtils.COMMENTS_IGNORE_PATTERN.test(
  542. comment.value,
  543. ),
  544. )
  545. .filter(comment => {
  546. const tokenBefore = sourceCode.getTokenBefore(comment, {
  547. includeComments: true,
  548. });
  549. return (
  550. !tokenBefore ||
  551. tokenBefore.loc.end.line < comment.loc.start.line
  552. );
  553. })
  554. .reduce((commentGroups, comment, index, commentList) => {
  555. const tokenBefore = sourceCode.getTokenBefore(comment, {
  556. includeComments: true,
  557. });
  558. if (
  559. comment.type === "Line" &&
  560. index &&
  561. commentList[index - 1].type === "Line" &&
  562. tokenBefore &&
  563. tokenBefore.loc.end.line ===
  564. comment.loc.start.line - 1 &&
  565. tokenBefore === commentList[index - 1]
  566. ) {
  567. commentGroups.at(-1).push(comment);
  568. } else {
  569. commentGroups.push([comment]);
  570. }
  571. return commentGroups;
  572. }, [])
  573. .filter(
  574. commentGroup =>
  575. !(
  576. commentGroup.length === 1 &&
  577. commentGroup[0].loc.start.line ===
  578. commentGroup[0].loc.end.line
  579. ),
  580. )
  581. .forEach(commentGroupCheckers[option]);
  582. },
  583. };
  584. },
  585. };