max-lines.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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('../shared/types').Rule} */
  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/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:
  62. "File has too many lines ({{actual}}). Maximum allowed is {{max}}."
  63. }
  64. },
  65. create(context) {
  66. const option = context.options[0];
  67. let max = 300;
  68. if (
  69. typeof option === "object" &&
  70. Object.prototype.hasOwnProperty.call(option, "max")
  71. ) {
  72. max = option.max;
  73. } else if (typeof option === "number") {
  74. max = option;
  75. }
  76. const skipComments = option && option.skipComments;
  77. const skipBlankLines = option && option.skipBlankLines;
  78. const sourceCode = context.getSourceCode();
  79. /**
  80. * Returns whether or not a token is a comment node type
  81. * @param {Token} token The token to check
  82. * @returns {boolean} True if the token is a comment node
  83. */
  84. function isCommentNodeType(token) {
  85. return token && (token.type === "Block" || token.type === "Line");
  86. }
  87. /**
  88. * Returns the line numbers of a comment that don't have any code on the same line
  89. * @param {Node} comment The comment node to check
  90. * @returns {number[]} The line numbers
  91. */
  92. function getLinesWithoutCode(comment) {
  93. let start = comment.loc.start.line;
  94. let end = comment.loc.end.line;
  95. let token;
  96. token = comment;
  97. do {
  98. token = sourceCode.getTokenBefore(token, {
  99. includeComments: true
  100. });
  101. } while (isCommentNodeType(token));
  102. if (token && astUtils.isTokenOnSameLine(token, comment)) {
  103. start += 1;
  104. }
  105. token = comment;
  106. do {
  107. token = sourceCode.getTokenAfter(token, {
  108. includeComments: true
  109. });
  110. } while (isCommentNodeType(token));
  111. if (token && astUtils.isTokenOnSameLine(comment, token)) {
  112. end -= 1;
  113. }
  114. if (start <= end) {
  115. return range(start, end + 1);
  116. }
  117. return [];
  118. }
  119. return {
  120. "Program:exit"() {
  121. let lines = sourceCode.lines.map((text, i) => ({
  122. lineNumber: i + 1,
  123. text
  124. }));
  125. /*
  126. * If file ends with a linebreak, `sourceCode.lines` will have one extra empty line at the end.
  127. * That isn't a real line, so we shouldn't count it.
  128. */
  129. if (lines.length > 1 && lines[lines.length - 1].text === "") {
  130. lines.pop();
  131. }
  132. if (skipBlankLines) {
  133. lines = lines.filter(l => l.text.trim() !== "");
  134. }
  135. if (skipComments) {
  136. const comments = sourceCode.getAllComments();
  137. const commentLines = new Set(comments.flatMap(getLinesWithoutCode));
  138. lines = lines.filter(
  139. l => !commentLines.has(l.lineNumber)
  140. );
  141. }
  142. if (lines.length > max) {
  143. const loc = {
  144. start: {
  145. line: lines[max].lineNumber,
  146. column: 0
  147. },
  148. end: {
  149. line: sourceCode.lines.length,
  150. column: sourceCode.lines[sourceCode.lines.length - 1].length
  151. }
  152. };
  153. context.report({
  154. loc,
  155. messageId: "exceed",
  156. data: {
  157. max,
  158. actual: lines.length
  159. }
  160. });
  161. }
  162. }
  163. };
  164. }
  165. };