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