| /** | |
|  * @fileoverview enforce a maximum file length | |
|  * @author Alberto Rodríguez | |
|  */ | |
| "use strict"; | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Requirements | |
| //------------------------------------------------------------------------------ | |
|  | |
| const astUtils = require("./utils/ast-utils"); | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Helpers | |
| //------------------------------------------------------------------------------ | |
|  | |
| /** | |
|  * Creates an array of numbers from `start` up to, but not including, `end` | |
|  * @param {number} start The start of the range | |
|  * @param {number} end The end of the range | |
|  * @returns {number[]} The range of numbers | |
|  */ | |
| function range(start, end) { | |
|     return [...Array(end - start).keys()].map(x => x + start); | |
| } | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Rule Definition | |
| //------------------------------------------------------------------------------ | |
|  | |
| /** @type {import('../shared/types').Rule} */ | |
| module.exports = { | |
|     meta: { | |
|         type: "suggestion", | |
| 
 | |
|         docs: { | |
|             description: "Enforce a maximum number of lines per file", | |
|             recommended: false, | |
|             url: "https://eslint.org/docs/latest/rules/max-lines" | |
|         }, | |
| 
 | |
|         schema: [ | |
|             { | |
|                 oneOf: [ | |
|                     { | |
|                         type: "integer", | |
|                         minimum: 0 | |
|                     }, | |
|                     { | |
|                         type: "object", | |
|                         properties: { | |
|                             max: { | |
|                                 type: "integer", | |
|                                 minimum: 0 | |
|                             }, | |
|                             skipComments: { | |
|                                 type: "boolean" | |
|                             }, | |
|                             skipBlankLines: { | |
|                                 type: "boolean" | |
|                             } | |
|                         }, | |
|                         additionalProperties: false | |
|                     } | |
|                 ] | |
|             } | |
|         ], | |
|         messages: { | |
|             exceed: | |
|                 "File has too many lines ({{actual}}). Maximum allowed is {{max}}." | |
|         } | |
|     }, | |
| 
 | |
|     create(context) { | |
|         const option = context.options[0]; | |
|         let max = 300; | |
| 
 | |
|         if ( | |
|             typeof option === "object" && | |
|             Object.prototype.hasOwnProperty.call(option, "max") | |
|         ) { | |
|             max = option.max; | |
|         } else if (typeof option === "number") { | |
|             max = option; | |
|         } | |
| 
 | |
|         const skipComments = option && option.skipComments; | |
|         const skipBlankLines = option && option.skipBlankLines; | |
| 
 | |
|         const sourceCode = context.sourceCode; | |
| 
 | |
|         /** | |
|          * Returns whether or not a token is a comment node type | |
|          * @param {Token} token The token to check | |
|          * @returns {boolean} True if the token is a comment node | |
|          */ | |
|         function isCommentNodeType(token) { | |
|             return token && (token.type === "Block" || token.type === "Line"); | |
|         } | |
| 
 | |
|         /** | |
|          * Returns the line numbers of a comment that don't have any code on the same line | |
|          * @param {Node} comment The comment node to check | |
|          * @returns {number[]} The line numbers | |
|          */ | |
|         function getLinesWithoutCode(comment) { | |
|             let start = comment.loc.start.line; | |
|             let end = comment.loc.end.line; | |
| 
 | |
|             let token; | |
| 
 | |
|             token = comment; | |
|             do { | |
|                 token = sourceCode.getTokenBefore(token, { | |
|                     includeComments: true | |
|                 }); | |
|             } while (isCommentNodeType(token)); | |
| 
 | |
|             if (token && astUtils.isTokenOnSameLine(token, comment)) { | |
|                 start += 1; | |
|             } | |
| 
 | |
|             token = comment; | |
|             do { | |
|                 token = sourceCode.getTokenAfter(token, { | |
|                     includeComments: true | |
|                 }); | |
|             } while (isCommentNodeType(token)); | |
| 
 | |
|             if (token && astUtils.isTokenOnSameLine(comment, token)) { | |
|                 end -= 1; | |
|             } | |
| 
 | |
|             if (start <= end) { | |
|                 return range(start, end + 1); | |
|             } | |
|             return []; | |
|         } | |
| 
 | |
|         return { | |
|             "Program:exit"() { | |
|                 let lines = sourceCode.lines.map((text, i) => ({ | |
|                     lineNumber: i + 1, | |
|                     text | |
|                 })); | |
| 
 | |
|                 /* | |
|                  * If file ends with a linebreak, `sourceCode.lines` will have one extra empty line at the end. | |
|                  * That isn't a real line, so we shouldn't count it. | |
|                  */ | |
|                 if (lines.length > 1 && lines[lines.length - 1].text === "") { | |
|                     lines.pop(); | |
|                 } | |
| 
 | |
|                 if (skipBlankLines) { | |
|                     lines = lines.filter(l => l.text.trim() !== ""); | |
|                 } | |
| 
 | |
|                 if (skipComments) { | |
|                     const comments = sourceCode.getAllComments(); | |
| 
 | |
|                     const commentLines = new Set(comments.flatMap(getLinesWithoutCode)); | |
| 
 | |
|                     lines = lines.filter( | |
|                         l => !commentLines.has(l.lineNumber) | |
|                     ); | |
|                 } | |
| 
 | |
|                 if (lines.length > max) { | |
|                     const loc = { | |
|                         start: { | |
|                             line: lines[max].lineNumber, | |
|                             column: 0 | |
|                         }, | |
|                         end: { | |
|                             line: sourceCode.lines.length, | |
|                             column: sourceCode.lines[sourceCode.lines.length - 1].length | |
|                         } | |
|                     }; | |
| 
 | |
|                     context.report({ | |
|                         loc, | |
|                         messageId: "exceed", | |
|                         data: { | |
|                             max, | |
|                             actual: lines.length | |
|                         } | |
|                     }); | |
|                 } | |
|             } | |
|         }; | |
|     } | |
| };
 |