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