UNPKG

13.3 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to check for max length on a line.
3 * @author Matt DuVall <http://www.mattduvall.com>
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Constants
10//------------------------------------------------------------------------------
11
12const OPTIONS_SCHEMA = {
13 type: "object",
14 properties: {
15 code: {
16 type: "integer",
17 minimum: 0
18 },
19 comments: {
20 type: "integer",
21 minimum: 0
22 },
23 tabWidth: {
24 type: "integer",
25 minimum: 0
26 },
27 ignorePattern: {
28 type: "string"
29 },
30 ignoreComments: {
31 type: "boolean"
32 },
33 ignoreStrings: {
34 type: "boolean"
35 },
36 ignoreUrls: {
37 type: "boolean"
38 },
39 ignoreTemplateLiterals: {
40 type: "boolean"
41 },
42 ignoreRegExpLiterals: {
43 type: "boolean"
44 },
45 ignoreTrailingComments: {
46 type: "boolean"
47 }
48 },
49 additionalProperties: false
50};
51
52const OPTIONS_OR_INTEGER_SCHEMA = {
53 anyOf: [
54 OPTIONS_SCHEMA,
55 {
56 type: "integer",
57 minimum: 0
58 }
59 ]
60};
61
62//------------------------------------------------------------------------------
63// Rule Definition
64//------------------------------------------------------------------------------
65
66module.exports = {
67 meta: {
68 docs: {
69 description: "enforce a maximum line length",
70 category: "Stylistic Issues",
71 recommended: false,
72 url: "https://eslint.org/docs/rules/max-len"
73 },
74
75 schema: [
76 OPTIONS_OR_INTEGER_SCHEMA,
77 OPTIONS_OR_INTEGER_SCHEMA,
78 OPTIONS_SCHEMA
79 ]
80 },
81
82 create(context) {
83
84 /*
85 * Inspired by http://tools.ietf.org/html/rfc3986#appendix-B, however:
86 * - They're matching an entire string that we know is a URI
87 * - We're matching part of a string where we think there *might* be a URL
88 * - We're only concerned about URLs, as picking out any URI would cause
89 * too many false positives
90 * - We don't care about matching the entire URL, any small segment is fine
91 */
92 const URL_REGEXP = /[^:/?#]:\/\/[^?#]/;
93
94 const sourceCode = context.getSourceCode();
95
96 /**
97 * Computes the length of a line that may contain tabs. The width of each
98 * tab will be the number of spaces to the next tab stop.
99 * @param {string} line The line.
100 * @param {int} tabWidth The width of each tab stop in spaces.
101 * @returns {int} The computed line length.
102 * @private
103 */
104 function computeLineLength(line, tabWidth) {
105 let extraCharacterCount = 0;
106
107 line.replace(/\t/g, (match, offset) => {
108 const totalOffset = offset + extraCharacterCount,
109 previousTabStopOffset = tabWidth ? totalOffset % tabWidth : 0,
110 spaceCount = tabWidth - previousTabStopOffset;
111
112 extraCharacterCount += spaceCount - 1; // -1 for the replaced tab
113 });
114 return Array.from(line).length + extraCharacterCount;
115 }
116
117 // The options object must be the last option specified…
118 const lastOption = context.options[context.options.length - 1];
119 const options = typeof lastOption === "object" ? Object.create(lastOption) : {};
120
121 // …but max code length…
122 if (typeof context.options[0] === "number") {
123 options.code = context.options[0];
124 }
125
126 // …and tabWidth can be optionally specified directly as integers.
127 if (typeof context.options[1] === "number") {
128 options.tabWidth = context.options[1];
129 }
130
131 const maxLength = options.code || 80,
132 tabWidth = options.tabWidth || 4,
133 ignoreComments = options.ignoreComments || false,
134 ignoreStrings = options.ignoreStrings || false,
135 ignoreTemplateLiterals = options.ignoreTemplateLiterals || false,
136 ignoreRegExpLiterals = options.ignoreRegExpLiterals || false,
137 ignoreTrailingComments = options.ignoreTrailingComments || options.ignoreComments || false,
138 ignoreUrls = options.ignoreUrls || false,
139 maxCommentLength = options.comments;
140 let ignorePattern = options.ignorePattern || null;
141
142 if (ignorePattern) {
143 ignorePattern = new RegExp(ignorePattern);
144 }
145
146 //--------------------------------------------------------------------------
147 // Helpers
148 //--------------------------------------------------------------------------
149
150 /**
151 * Tells if a given comment is trailing: it starts on the current line and
152 * extends to or past the end of the current line.
153 * @param {string} line The source line we want to check for a trailing comment on
154 * @param {number} lineNumber The one-indexed line number for line
155 * @param {ASTNode} comment The comment to inspect
156 * @returns {boolean} If the comment is trailing on the given line
157 */
158 function isTrailingComment(line, lineNumber, comment) {
159 return comment &&
160 (comment.loc.start.line === lineNumber && lineNumber <= comment.loc.end.line) &&
161 (comment.loc.end.line > lineNumber || comment.loc.end.column === line.length);
162 }
163
164 /**
165 * Tells if a comment encompasses the entire line.
166 * @param {string} line The source line with a trailing comment
167 * @param {number} lineNumber The one-indexed line number this is on
168 * @param {ASTNode} comment The comment to remove
169 * @returns {boolean} If the comment covers the entire line
170 */
171 function isFullLineComment(line, lineNumber, comment) {
172 const start = comment.loc.start,
173 end = comment.loc.end,
174 isFirstTokenOnLine = !line.slice(0, comment.loc.start.column).trim();
175
176 return comment &&
177 (start.line < lineNumber || (start.line === lineNumber && isFirstTokenOnLine)) &&
178 (end.line > lineNumber || (end.line === lineNumber && end.column === line.length));
179 }
180
181 /**
182 * Gets the line after the comment and any remaining trailing whitespace is
183 * stripped.
184 * @param {string} line The source line with a trailing comment
185 * @param {ASTNode} comment The comment to remove
186 * @returns {string} Line without comment and trailing whitepace
187 */
188 function stripTrailingComment(line, comment) {
189
190 // loc.column is zero-indexed
191 return line.slice(0, comment.loc.start.column).replace(/\s+$/, "");
192 }
193
194 /**
195 * Ensure that an array exists at [key] on `object`, and add `value` to it.
196 *
197 * @param {Object} object the object to mutate
198 * @param {string} key the object's key
199 * @param {*} value the value to add
200 * @returns {void}
201 * @private
202 */
203 function ensureArrayAndPush(object, key, value) {
204 if (!Array.isArray(object[key])) {
205 object[key] = [];
206 }
207 object[key].push(value);
208 }
209
210 /**
211 * Retrieves an array containing all strings (" or ') in the source code.
212 *
213 * @returns {ASTNode[]} An array of string nodes.
214 */
215 function getAllStrings() {
216 return sourceCode.ast.tokens.filter(token => token.type === "String");
217 }
218
219 /**
220 * Retrieves an array containing all template literals in the source code.
221 *
222 * @returns {ASTNode[]} An array of template literal nodes.
223 */
224 function getAllTemplateLiterals() {
225 return sourceCode.ast.tokens.filter(token => token.type === "Template");
226 }
227
228
229 /**
230 * Retrieves an array containing all RegExp literals in the source code.
231 *
232 * @returns {ASTNode[]} An array of RegExp literal nodes.
233 */
234 function getAllRegExpLiterals() {
235 return sourceCode.ast.tokens.filter(token => token.type === "RegularExpression");
236 }
237
238
239 /**
240 * A reducer to group an AST node by line number, both start and end.
241 *
242 * @param {Object} acc the accumulator
243 * @param {ASTNode} node the AST node in question
244 * @returns {Object} the modified accumulator
245 * @private
246 */
247 function groupByLineNumber(acc, node) {
248 for (let i = node.loc.start.line; i <= node.loc.end.line; ++i) {
249 ensureArrayAndPush(acc, i, node);
250 }
251 return acc;
252 }
253
254 /**
255 * Check the program for max length
256 * @param {ASTNode} node Node to examine
257 * @returns {void}
258 * @private
259 */
260 function checkProgramForMaxLength(node) {
261
262 // split (honors line-ending)
263 const lines = sourceCode.lines,
264
265 // list of comments to ignore
266 comments = ignoreComments || maxCommentLength || ignoreTrailingComments ? sourceCode.getAllComments() : [];
267
268 // we iterate over comments in parallel with the lines
269 let commentsIndex = 0;
270
271 const strings = getAllStrings();
272 const stringsByLine = strings.reduce(groupByLineNumber, {});
273
274 const templateLiterals = getAllTemplateLiterals();
275 const templateLiteralsByLine = templateLiterals.reduce(groupByLineNumber, {});
276
277 const regExpLiterals = getAllRegExpLiterals();
278 const regExpLiteralsByLine = regExpLiterals.reduce(groupByLineNumber, {});
279
280 lines.forEach((line, i) => {
281
282 // i is zero-indexed, line numbers are one-indexed
283 const lineNumber = i + 1;
284
285 /*
286 * if we're checking comment length; we need to know whether this
287 * line is a comment
288 */
289 let lineIsComment = false;
290
291 /*
292 * We can short-circuit the comment checks if we're already out of
293 * comments to check.
294 */
295 if (commentsIndex < comments.length) {
296 let comment = null;
297
298 // iterate over comments until we find one past the current line
299 do {
300 comment = comments[++commentsIndex];
301 } while (comment && comment.loc.start.line <= lineNumber);
302
303 // and step back by one
304 comment = comments[--commentsIndex];
305
306 if (isFullLineComment(line, lineNumber, comment)) {
307 lineIsComment = true;
308 } else if (ignoreTrailingComments && isTrailingComment(line, lineNumber, comment)) {
309 line = stripTrailingComment(line, comment);
310 }
311 }
312 if (ignorePattern && ignorePattern.test(line) ||
313 ignoreUrls && URL_REGEXP.test(line) ||
314 ignoreStrings && stringsByLine[lineNumber] ||
315 ignoreTemplateLiterals && templateLiteralsByLine[lineNumber] ||
316 ignoreRegExpLiterals && regExpLiteralsByLine[lineNumber]
317 ) {
318
319 // ignore this line
320 return;
321 }
322
323 const lineLength = computeLineLength(line, tabWidth);
324 const commentLengthApplies = lineIsComment && maxCommentLength;
325
326 if (lineIsComment && ignoreComments) {
327 return;
328 }
329
330 if (commentLengthApplies) {
331 if (lineLength > maxCommentLength) {
332 context.report({
333 node,
334 loc: { line: lineNumber, column: 0 },
335 message: "Line {{lineNumber}} exceeds the maximum comment line length of {{maxCommentLength}}.",
336 data: {
337 lineNumber: i + 1,
338 maxCommentLength
339 }
340 });
341 }
342 } else if (lineLength > maxLength) {
343 context.report({
344 node,
345 loc: { line: lineNumber, column: 0 },
346 message: "Line {{lineNumber}} exceeds the maximum line length of {{maxLength}}.",
347 data: {
348 lineNumber: i + 1,
349 maxLength
350 }
351 });
352 }
353 });
354 }
355
356
357 //--------------------------------------------------------------------------
358 // Public API
359 //--------------------------------------------------------------------------
360
361 return {
362 Program: checkProgramForMaxLength
363 };
364
365 }
366};