max-lines.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. /**
  2. * @fileoverview enforce a maximum file length
  3. * @author Alberto Rodríguez
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Helpers
  12. //------------------------------------------------------------------------------
  13. /**
  14. * Creates an array of numbers from `start` up to, but not including, `end`
  15. * @param {number} start The start of the range
  16. * @param {number} end The end of the range
  17. * @returns {number[]} The range of numbers
  18. */
  19. function range(start, end) {
  20. return [...Array(end - start).keys()].map(x => x + start);
  21. }
  22. //------------------------------------------------------------------------------
  23. // Rule Definition
  24. //------------------------------------------------------------------------------
  25. /** @type {import('../types').Rule.RuleModule} */
  26. module.exports = {
  27. meta: {
  28. type: "suggestion",
  29. docs: {
  30. description: "Enforce a maximum number of lines per file",
  31. recommended: false,
  32. url: "https://eslint.org/docs/latest/rules/max-lines",
  33. },
  34. schema: [
  35. {
  36. oneOf: [
  37. {
  38. type: "integer",
  39. minimum: 0,
  40. },
  41. {
  42. type: "object",
  43. properties: {
  44. max: {
  45. type: "integer",
  46. minimum: 0,
  47. },
  48. skipComments: {
  49. type: "boolean",
  50. },
  51. skipBlankLines: {
  52. type: "boolean",
  53. },
  54. },
  55. additionalProperties: false,
  56. },
  57. ],
  58. },
  59. ],
  60. messages: {
  61. exceed: "File has too many lines ({{actual}}). Maximum allowed is {{max}}.",
  62. },
  63. },
  64. create(context) {
  65. const option = context.options[0];
  66. let max = 300;
  67. if (typeof option === "object" && Object.hasOwn(option, "max")) {
  68. max = option.max;
  69. } else if (typeof option === "number") {
  70. max = option;
  71. }
  72. const skipComments = option && option.skipComments;
  73. const skipBlankLines = option && option.skipBlankLines;
  74. const sourceCode = context.sourceCode;
  75. /**
  76. * Returns whether or not a token is a comment node type
  77. * @param {Token} token The token to check
  78. * @returns {boolean} True if the token is a comment node
  79. */
  80. function isCommentNodeType(token) {
  81. return token && (token.type === "Block" || token.type === "Line");
  82. }
  83. /**
  84. * Returns the line numbers of a comment that don't have any code on the same line
  85. * @param {Node} comment The comment node to check
  86. * @returns {number[]} The line numbers
  87. */
  88. function getLinesWithoutCode(comment) {
  89. let start = comment.loc.start.line;
  90. let end = comment.loc.end.line;
  91. let token;
  92. token = comment;
  93. do {
  94. token = sourceCode.getTokenBefore(token, {
  95. includeComments: true,
  96. });
  97. } while (isCommentNodeType(token));
  98. if (token && astUtils.isTokenOnSameLine(token, comment)) {
  99. start += 1;
  100. }
  101. token = comment;
  102. do {
  103. token = sourceCode.getTokenAfter(token, {
  104. includeComments: true,
  105. });
  106. } while (isCommentNodeType(token));
  107. if (token && astUtils.isTokenOnSameLine(comment, token)) {
  108. end -= 1;
  109. }
  110. if (start <= end) {
  111. return range(start, end + 1);
  112. }
  113. return [];
  114. }
  115. return {
  116. "Program:exit"() {
  117. let lines = sourceCode.lines.map((text, i) => ({
  118. lineNumber: i + 1,
  119. text,
  120. }));
  121. /*
  122. * If file ends with a linebreak, `sourceCode.lines` will have one extra empty line at the end.
  123. * That isn't a real line, so we shouldn't count it.
  124. */
  125. if (lines.length > 1 && lines.at(-1).text === "") {
  126. lines.pop();
  127. }
  128. if (skipBlankLines) {
  129. lines = lines.filter(l => l.text.trim() !== "");
  130. }
  131. if (skipComments) {
  132. const comments = sourceCode.getAllComments();
  133. const commentLines = new Set(
  134. comments.flatMap(getLinesWithoutCode),
  135. );
  136. lines = lines.filter(l => !commentLines.has(l.lineNumber));
  137. }
  138. if (lines.length > max) {
  139. const loc = {
  140. start: {
  141. line: lines[max].lineNumber,
  142. column: 0,
  143. },
  144. end: {
  145. line: sourceCode.lines.length,
  146. column: sourceCode.lines.at(-1).length,
  147. },
  148. };
  149. context.report({
  150. loc,
  151. messageId: "exceed",
  152. data: {
  153. max,
  154. actual: lines.length,
  155. },
  156. });
  157. }
  158. },
  159. };
  160. },
  161. };