1/** 2 * @fileoverview A rule to set the maximum number of line of code in a function. 3 * @author Pete Ward <peteward44@gmail.com> 4 */ 5"use strict"; 6 7//------------------------------------------------------------------------------ 8// Requirements 9//------------------------------------------------------------------------------ 10 11const astUtils = require("./utils/ast-utils"); 12 13const lodash = require("lodash"); 14 15//------------------------------------------------------------------------------ 16// Constants 17//------------------------------------------------------------------------------ 18 19const OPTIONS_SCHEMA = { 20 type: "object", 21 properties: { 22 max: { 23 type: "integer", 24 minimum: 0 25 }, 26 skipComments: { 27 type: "boolean" 28 }, 29 skipBlankLines: { 30 type: "boolean" 31 }, 32 IIFEs: { 33 type: "boolean" 34 } 35 }, 36 additionalProperties: false 37}; 38 39const OPTIONS_OR_INTEGER_SCHEMA = { 40 oneOf: [ 41 OPTIONS_SCHEMA, 42 { 43 type: "integer", 44 minimum: 1 45 } 46 ] 47}; 48 49/** 50 * Given a list of comment nodes, return a map with numeric keys (source code line numbers) and comment token values. 51 * @param {Array} comments An array of comment nodes. 52 * @returns {Map.<string,Node>} A map with numeric keys (source code line numbers) and comment token values. 53 */ 54function getCommentLineNumbers(comments) { 55 const map = new Map(); 56 57 comments.forEach(comment => { 58 for (let i = comment.loc.start.line; i <= comment.loc.end.line; i++) { 59 map.set(i, comment); 60 } 61 }); 62 return map; 63} 64 65//------------------------------------------------------------------------------ 66// Rule Definition 67//------------------------------------------------------------------------------ 68 69module.exports = { 70 meta: { 71 type: "suggestion", 72 73 docs: { 74 description: "enforce a maximum number of line of code in a function", 75 category: "Stylistic Issues", 76 recommended: false, 77 url: "https://eslint.org/docs/rules/max-lines-per-function" 78 }, 79 80 schema: [ 81 OPTIONS_OR_INTEGER_SCHEMA 82 ], 83 messages: { 84 exceed: "{{name}} has too many lines ({{lineCount}}). Maximum allowed is {{maxLines}}." 85 } 86 }, 87 88 create(context) { 89 const sourceCode = context.getSourceCode(); 90 const lines = sourceCode.lines; 91 92 const option = context.options[0]; 93 let maxLines = 50; 94 let skipComments = false; 95 let skipBlankLines = false; 96 let IIFEs = false; 97 98 if (typeof option === "object") { 99 maxLines = typeof option.max === "number" ? option.max : 50; 100 skipComments = !!option.skipComments; 101 skipBlankLines = !!option.skipBlankLines; 102 IIFEs = !!option.IIFEs; 103 } else if (typeof option === "number") { 104 maxLines = option; 105 } 106 107 const commentLineNumbers = getCommentLineNumbers(sourceCode.getAllComments()); 108 109 //-------------------------------------------------------------------------- 110 // Helpers 111 //-------------------------------------------------------------------------- 112 113 /** 114 * Tells if a comment encompasses the entire line. 115 * @param {string} line The source line with a trailing comment 116 * @param {number} lineNumber The one-indexed line number this is on 117 * @param {ASTNode} comment The comment to remove 118 * @returns {boolean} If the comment covers the entire line 119 */ 120 function isFullLineComment(line, lineNumber, comment) { 121 const start = comment.loc.start, 122 end = comment.loc.end, 123 isFirstTokenOnLine = start.line === lineNumber && !line.slice(0, start.column).trim(), 124 isLastTokenOnLine = end.line === lineNumber && !line.slice(end.column).trim(); 125 126 return comment && 127 (start.line < lineNumber || isFirstTokenOnLine) && 128 (end.line > lineNumber || isLastTokenOnLine); 129 } 130 131 /** 132 * Identifies is a node is a FunctionExpression which is part of an IIFE 133 * @param {ASTNode} node Node to test 134 * @returns {boolean} True if it's an IIFE 135 */ 136 function isIIFE(node) { 137 return (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && node.parent && node.parent.type === "CallExpression" && node.parent.callee === node; 138 } 139 140 /** 141 * Identifies is a node is a FunctionExpression which is embedded within a MethodDefinition or Property 142 * @param {ASTNode} node Node to test 143 * @returns {boolean} True if it's a FunctionExpression embedded within a MethodDefinition or Property 144 */ 145 function isEmbedded(node) { 146 if (!node.parent) { 147 return false; 148 } 149 if (node !== node.parent.value) { 150 return false; 151 } 152 if (node.parent.type === "MethodDefinition") { 153 return true; 154 } 155 if (node.parent.type === "Property") { 156 return node.parent.method === true || node.parent.kind === "get" || node.parent.kind === "set"; 157 } 158 return false; 159 } 160 161 /** 162 * Count the lines in the function 163 * @param {ASTNode} funcNode Function AST node 164 * @returns {void} 165 * @private 166 */ 167 function processFunction(funcNode) { 168 const node = isEmbedded(funcNode) ? funcNode.parent : funcNode; 169 170 if (!IIFEs && isIIFE(node)) { 171 return; 172 } 173 let lineCount = 0; 174 175 for (let i = node.loc.start.line - 1; i < node.loc.end.line; ++i) { 176 const line = lines[i]; 177 178 if (skipComments) { 179 if (commentLineNumbers.has(i + 1) && isFullLineComment(line, i + 1, commentLineNumbers.get(i + 1))) { 180 continue; 181 } 182 } 183 184 if (skipBlankLines) { 185 if (line.match(/^\s*$/u)) { 186 continue; 187 } 188 } 189 190 lineCount++; 191 } 192 193 if (lineCount > maxLines) { 194 const name = lodash.upperFirst(astUtils.getFunctionNameWithKind(funcNode)); 195 196 context.report({ 197 node, 198 messageId: "exceed", 199 data: { name, lineCount, maxLines } 200 }); 201 } 202 } 203 204 //-------------------------------------------------------------------------- 205 // Public API 206 //-------------------------------------------------------------------------- 207 208 return { 209 FunctionDeclaration: processFunction, 210 FunctionExpression: processFunction, 211 ArrowFunctionExpression: processFunction 212 }; 213 } 214}; 215