max-lines.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  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. module.exports = {
  26. meta: {
  27. type: "suggestion",
  28. docs: {
  29. description: "enforce a maximum number of lines per file",
  30. category: "Stylistic Issues",
  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. /**
  120. * Returns a new array formed by applying a given callback function to each element of the array, and then flattening the result by one level.
  121. * TODO(stephenwade): Replace this with array.flatMap when we drop support for Node v10
  122. * @param {any[]} array The array to process
  123. * @param {Function} fn The function to use
  124. * @returns {any[]} The result array
  125. */
  126. function flatMap(array, fn) {
  127. const mapped = array.map(fn);
  128. const flattened = [].concat(...mapped);
  129. return flattened;
  130. }
  131. return {
  132. "Program:exit"() {
  133. let lines = sourceCode.lines.map((text, i) => ({
  134. lineNumber: i + 1,
  135. text
  136. }));
  137. /*
  138. * If file ends with a linebreak, `sourceCode.lines` will have one extra empty line at the end.
  139. * That isn't a real line, so we shouldn't count it.
  140. */
  141. if (lines.length > 1 && lines[lines.length - 1].text === "") {
  142. lines.pop();
  143. }
  144. if (skipBlankLines) {
  145. lines = lines.filter(l => l.text.trim() !== "");
  146. }
  147. if (skipComments) {
  148. const comments = sourceCode.getAllComments();
  149. const commentLines = flatMap(comments, comment => getLinesWithoutCode(comment));
  150. lines = lines.filter(
  151. l => !commentLines.includes(l.lineNumber)
  152. );
  153. }
  154. if (lines.length > max) {
  155. const loc = {
  156. start: {
  157. line: lines[max].lineNumber,
  158. column: 0
  159. },
  160. end: {
  161. line: sourceCode.lines.length,
  162. column: sourceCode.lines[sourceCode.lines.length - 1].length
  163. }
  164. };
  165. context.report({
  166. loc,
  167. messageId: "exceed",
  168. data: {
  169. max,
  170. actual: lines.length
  171. }
  172. });
  173. }
  174. }
  175. };
  176. }
  177. };