lines-around-comment.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. /**
  2. * @fileoverview Enforces empty lines around comments.
  3. * @author Jamund Ferguson
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const lodash = require("lodash"),
  10. astUtils = require("./utils/ast-utils");
  11. //------------------------------------------------------------------------------
  12. // Helpers
  13. //------------------------------------------------------------------------------
  14. /**
  15. * Return an array with 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.map((line, i) => ({
  21. code: line.trim(),
  22. num: i + 1
  23. })).filter(line => !line.code).map(line => line.num);
  24. return emptyLines;
  25. }
  26. /**
  27. * Return an array with with any line numbers that contain comments.
  28. * @param {Array} comments An array of comment tokens.
  29. * @returns {Array} An array of line numbers.
  30. */
  31. function getCommentLineNums(comments) {
  32. const lines = [];
  33. comments.forEach(token => {
  34. const start = token.loc.start.line;
  35. const end = token.loc.end.line;
  36. lines.push(start, end);
  37. });
  38. return lines;
  39. }
  40. //------------------------------------------------------------------------------
  41. // Rule Definition
  42. //------------------------------------------------------------------------------
  43. module.exports = {
  44. meta: {
  45. type: "layout",
  46. docs: {
  47. description: "require empty lines around comments",
  48. category: "Stylistic Issues",
  49. recommended: false,
  50. url: "https://eslint.org/docs/rules/lines-around-comment"
  51. },
  52. fixable: "whitespace",
  53. schema: [
  54. {
  55. type: "object",
  56. properties: {
  57. beforeBlockComment: {
  58. type: "boolean",
  59. default: true
  60. },
  61. afterBlockComment: {
  62. type: "boolean",
  63. default: false
  64. },
  65. beforeLineComment: {
  66. type: "boolean",
  67. default: false
  68. },
  69. afterLineComment: {
  70. type: "boolean",
  71. default: false
  72. },
  73. allowBlockStart: {
  74. type: "boolean",
  75. default: false
  76. },
  77. allowBlockEnd: {
  78. type: "boolean",
  79. default: false
  80. },
  81. allowClassStart: {
  82. type: "boolean"
  83. },
  84. allowClassEnd: {
  85. type: "boolean"
  86. },
  87. allowObjectStart: {
  88. type: "boolean"
  89. },
  90. allowObjectEnd: {
  91. type: "boolean"
  92. },
  93. allowArrayStart: {
  94. type: "boolean"
  95. },
  96. allowArrayEnd: {
  97. type: "boolean"
  98. },
  99. ignorePattern: {
  100. type: "string"
  101. },
  102. applyDefaultIgnorePatterns: {
  103. type: "boolean"
  104. }
  105. },
  106. additionalProperties: false
  107. }
  108. ],
  109. messages: {
  110. after: "Expected line after comment.",
  111. before: "Expected line before comment."
  112. }
  113. },
  114. create(context) {
  115. const options = Object.assign({}, context.options[0]);
  116. const ignorePattern = options.ignorePattern;
  117. const defaultIgnoreRegExp = astUtils.COMMENTS_IGNORE_PATTERN;
  118. const customIgnoreRegExp = new RegExp(ignorePattern, "u");
  119. const applyDefaultIgnorePatterns = options.applyDefaultIgnorePatterns !== false;
  120. options.beforeBlockComment = typeof options.beforeBlockComment !== "undefined" ? options.beforeBlockComment : true;
  121. const sourceCode = context.getSourceCode();
  122. const lines = sourceCode.lines,
  123. numLines = lines.length + 1,
  124. comments = sourceCode.getAllComments(),
  125. commentLines = getCommentLineNums(comments),
  126. emptyLines = getEmptyLineNums(lines),
  127. commentAndEmptyLines = commentLines.concat(emptyLines);
  128. /**
  129. * Returns whether or not comments are on lines starting with or ending with code
  130. * @param {token} token The comment token to check.
  131. * @returns {boolean} True if the comment is not alone.
  132. */
  133. function codeAroundComment(token) {
  134. let currentToken = token;
  135. do {
  136. currentToken = sourceCode.getTokenBefore(currentToken, { includeComments: true });
  137. } while (currentToken && astUtils.isCommentToken(currentToken));
  138. if (currentToken && astUtils.isTokenOnSameLine(currentToken, token)) {
  139. return true;
  140. }
  141. currentToken = token;
  142. do {
  143. currentToken = sourceCode.getTokenAfter(currentToken, { includeComments: true });
  144. } while (currentToken && astUtils.isCommentToken(currentToken));
  145. if (currentToken && astUtils.isTokenOnSameLine(token, currentToken)) {
  146. return true;
  147. }
  148. return false;
  149. }
  150. /**
  151. * Returns whether or not comments are inside a node type or not.
  152. * @param {ASTNode} parent The Comment parent node.
  153. * @param {string} nodeType The parent type to check against.
  154. * @returns {boolean} True if the comment is inside nodeType.
  155. */
  156. function isParentNodeType(parent, nodeType) {
  157. return parent.type === nodeType ||
  158. (parent.body && parent.body.type === nodeType) ||
  159. (parent.consequent && parent.consequent.type === nodeType);
  160. }
  161. /**
  162. * Returns the parent node that contains the given token.
  163. * @param {token} token The token to check.
  164. * @returns {ASTNode} The parent node that contains the given token.
  165. */
  166. function getParentNodeOfToken(token) {
  167. return sourceCode.getNodeByRangeIndex(token.range[0]);
  168. }
  169. /**
  170. * Returns whether or not comments are at the parent start or not.
  171. * @param {token} token The Comment token.
  172. * @param {string} nodeType The parent type to check against.
  173. * @returns {boolean} True if the comment is at parent start.
  174. */
  175. function isCommentAtParentStart(token, nodeType) {
  176. const parent = getParentNodeOfToken(token);
  177. return parent && isParentNodeType(parent, nodeType) &&
  178. token.loc.start.line - parent.loc.start.line === 1;
  179. }
  180. /**
  181. * Returns whether or not comments are at the parent end or not.
  182. * @param {token} token The Comment token.
  183. * @param {string} nodeType The parent type to check against.
  184. * @returns {boolean} True if the comment is at parent end.
  185. */
  186. function isCommentAtParentEnd(token, nodeType) {
  187. const parent = getParentNodeOfToken(token);
  188. return parent && isParentNodeType(parent, nodeType) &&
  189. parent.loc.end.line - token.loc.end.line === 1;
  190. }
  191. /**
  192. * Returns whether or not comments are at the block start or not.
  193. * @param {token} token The Comment token.
  194. * @returns {boolean} True if the comment is at block start.
  195. */
  196. function isCommentAtBlockStart(token) {
  197. return isCommentAtParentStart(token, "ClassBody") || isCommentAtParentStart(token, "BlockStatement") || isCommentAtParentStart(token, "SwitchCase");
  198. }
  199. /**
  200. * Returns whether or not comments are at the block end or not.
  201. * @param {token} token The Comment token.
  202. * @returns {boolean} True if the comment is at block end.
  203. */
  204. function isCommentAtBlockEnd(token) {
  205. return isCommentAtParentEnd(token, "ClassBody") || isCommentAtParentEnd(token, "BlockStatement") || isCommentAtParentEnd(token, "SwitchCase") || isCommentAtParentEnd(token, "SwitchStatement");
  206. }
  207. /**
  208. * Returns whether or not comments are at the class start or not.
  209. * @param {token} token The Comment token.
  210. * @returns {boolean} True if the comment is at class start.
  211. */
  212. function isCommentAtClassStart(token) {
  213. return isCommentAtParentStart(token, "ClassBody");
  214. }
  215. /**
  216. * Returns whether or not comments are at the class end or not.
  217. * @param {token} token The Comment token.
  218. * @returns {boolean} True if the comment is at class end.
  219. */
  220. function isCommentAtClassEnd(token) {
  221. return isCommentAtParentEnd(token, "ClassBody");
  222. }
  223. /**
  224. * Returns whether or not comments are at the object start or not.
  225. * @param {token} token The Comment token.
  226. * @returns {boolean} True if the comment is at object start.
  227. */
  228. function isCommentAtObjectStart(token) {
  229. return isCommentAtParentStart(token, "ObjectExpression") || isCommentAtParentStart(token, "ObjectPattern");
  230. }
  231. /**
  232. * Returns whether or not comments are at the object end or not.
  233. * @param {token} token The Comment token.
  234. * @returns {boolean} True if the comment is at object end.
  235. */
  236. function isCommentAtObjectEnd(token) {
  237. return isCommentAtParentEnd(token, "ObjectExpression") || isCommentAtParentEnd(token, "ObjectPattern");
  238. }
  239. /**
  240. * Returns whether or not comments are at the array start or not.
  241. * @param {token} token The Comment token.
  242. * @returns {boolean} True if the comment is at array start.
  243. */
  244. function isCommentAtArrayStart(token) {
  245. return isCommentAtParentStart(token, "ArrayExpression") || isCommentAtParentStart(token, "ArrayPattern");
  246. }
  247. /**
  248. * Returns whether or not comments are at the array end or not.
  249. * @param {token} token The Comment token.
  250. * @returns {boolean} True if the comment is at array end.
  251. */
  252. function isCommentAtArrayEnd(token) {
  253. return isCommentAtParentEnd(token, "ArrayExpression") || isCommentAtParentEnd(token, "ArrayPattern");
  254. }
  255. /**
  256. * Checks if a comment token has lines around it (ignores inline comments)
  257. * @param {token} token The Comment token.
  258. * @param {Object} opts Options to determine the newline.
  259. * @param {boolean} opts.after Should have a newline after this line.
  260. * @param {boolean} opts.before Should have a newline before this line.
  261. * @returns {void}
  262. */
  263. function checkForEmptyLine(token, opts) {
  264. if (applyDefaultIgnorePatterns && defaultIgnoreRegExp.test(token.value)) {
  265. return;
  266. }
  267. if (ignorePattern && customIgnoreRegExp.test(token.value)) {
  268. return;
  269. }
  270. let after = opts.after,
  271. before = opts.before;
  272. const prevLineNum = token.loc.start.line - 1,
  273. nextLineNum = token.loc.end.line + 1,
  274. commentIsNotAlone = codeAroundComment(token);
  275. const blockStartAllowed = options.allowBlockStart &&
  276. isCommentAtBlockStart(token) &&
  277. !(options.allowClassStart === false &&
  278. isCommentAtClassStart(token)),
  279. blockEndAllowed = options.allowBlockEnd && isCommentAtBlockEnd(token) && !(options.allowClassEnd === false && isCommentAtClassEnd(token)),
  280. classStartAllowed = options.allowClassStart && isCommentAtClassStart(token),
  281. classEndAllowed = options.allowClassEnd && isCommentAtClassEnd(token),
  282. objectStartAllowed = options.allowObjectStart && isCommentAtObjectStart(token),
  283. objectEndAllowed = options.allowObjectEnd && isCommentAtObjectEnd(token),
  284. arrayStartAllowed = options.allowArrayStart && isCommentAtArrayStart(token),
  285. arrayEndAllowed = options.allowArrayEnd && isCommentAtArrayEnd(token);
  286. const exceptionStartAllowed = blockStartAllowed || classStartAllowed || objectStartAllowed || arrayStartAllowed;
  287. const exceptionEndAllowed = blockEndAllowed || classEndAllowed || objectEndAllowed || arrayEndAllowed;
  288. // ignore top of the file and bottom of the file
  289. if (prevLineNum < 1) {
  290. before = false;
  291. }
  292. if (nextLineNum >= numLines) {
  293. after = false;
  294. }
  295. // we ignore all inline comments
  296. if (commentIsNotAlone) {
  297. return;
  298. }
  299. const previousTokenOrComment = sourceCode.getTokenBefore(token, { includeComments: true });
  300. const nextTokenOrComment = sourceCode.getTokenAfter(token, { includeComments: true });
  301. // check for newline before
  302. if (!exceptionStartAllowed && before && !lodash.includes(commentAndEmptyLines, prevLineNum) &&
  303. !(astUtils.isCommentToken(previousTokenOrComment) && astUtils.isTokenOnSameLine(previousTokenOrComment, token))) {
  304. const lineStart = token.range[0] - token.loc.start.column;
  305. const range = [lineStart, lineStart];
  306. context.report({
  307. node: token,
  308. messageId: "before",
  309. fix(fixer) {
  310. return fixer.insertTextBeforeRange(range, "\n");
  311. }
  312. });
  313. }
  314. // check for newline after
  315. if (!exceptionEndAllowed && after && !lodash.includes(commentAndEmptyLines, nextLineNum) &&
  316. !(astUtils.isCommentToken(nextTokenOrComment) && astUtils.isTokenOnSameLine(token, nextTokenOrComment))) {
  317. context.report({
  318. node: token,
  319. messageId: "after",
  320. fix(fixer) {
  321. return fixer.insertTextAfter(token, "\n");
  322. }
  323. });
  324. }
  325. }
  326. //--------------------------------------------------------------------------
  327. // Public
  328. //--------------------------------------------------------------------------
  329. return {
  330. Program() {
  331. comments.forEach(token => {
  332. if (token.type === "Line") {
  333. if (options.beforeLineComment || options.afterLineComment) {
  334. checkForEmptyLine(token, {
  335. after: options.afterLineComment,
  336. before: options.beforeLineComment
  337. });
  338. }
  339. } else if (token.type === "Block") {
  340. if (options.beforeBlockComment || options.afterBlockComment) {
  341. checkForEmptyLine(token, {
  342. after: options.afterBlockComment,
  343. before: options.beforeBlockComment
  344. });
  345. }
  346. }
  347. });
  348. }
  349. };
  350. }
  351. };