2 * @fileoverview Rule to check for max length on a line.
3 * @author Matt DuVall <http://www.mattduvall.com>
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const OPTIONS_SCHEMA
= {
39 ignoreTemplateLiterals
: {
42 ignoreRegExpLiterals
: {
45 ignoreTrailingComments
: {
49 additionalProperties
: false
52 const OPTIONS_OR_INTEGER_SCHEMA
= {
62 //------------------------------------------------------------------------------
64 //------------------------------------------------------------------------------
71 description
: "enforce a maximum line length",
72 category
: "Stylistic Issues",
74 url
: "https://eslint.org/docs/rules/max-len"
78 OPTIONS_OR_INTEGER_SCHEMA
,
79 OPTIONS_OR_INTEGER_SCHEMA
,
83 max
: "This line has a length of {{lineLength}}. Maximum allowed is {{maxLength}}.",
84 maxComment
: "This line has a comment length of {{lineLength}}. Maximum allowed is {{maxCommentLength}}."
91 * Inspired by http://tools.ietf.org/html/rfc3986#appendix-B, however:
92 * - They're matching an entire string that we know is a URI
93 * - We're matching part of a string where we think there *might* be a URL
94 * - We're only concerned about URLs, as picking out any URI would cause
95 * too many false positives
96 * - We don't care about matching the entire URL, any small segment is fine
98 const URL_REGEXP
= /[^:/?#]:\/\/[^?#]/u;
100 const sourceCode
= context
.getSourceCode();
103 * Computes the length of a line that may contain tabs. The width of each
104 * tab will be the number of spaces to the next tab stop.
105 * @param {string} line The line.
106 * @param {int} tabWidth The width of each tab stop in spaces.
107 * @returns {int} The computed line length.
110 function computeLineLength(line
, tabWidth
) {
111 let extraCharacterCount
= 0;
113 line
.replace(/\t/gu, (match
, offset
) => {
114 const totalOffset
= offset
+ extraCharacterCount
,
115 previousTabStopOffset
= tabWidth
? totalOffset
% tabWidth
: 0,
116 spaceCount
= tabWidth
- previousTabStopOffset
;
118 extraCharacterCount
+= spaceCount
- 1; // -1 for the replaced tab
120 return Array
.from(line
).length
+ extraCharacterCount
;
123 // The options object must be the last option specified…
124 const options
= Object
.assign({}, context
.options
[context
.options
.length
- 1]);
126 // …but max code length…
127 if (typeof context
.options
[0] === "number") {
128 options
.code
= context
.options
[0];
131 // …and tabWidth can be optionally specified directly as integers.
132 if (typeof context
.options
[1] === "number") {
133 options
.tabWidth
= context
.options
[1];
136 const maxLength
= typeof options
.code
=== "number" ? options
.code
: 80,
137 tabWidth
= typeof options
.tabWidth
=== "number" ? options
.tabWidth
: 4,
138 ignoreComments
= !!options
.ignoreComments
,
139 ignoreStrings
= !!options
.ignoreStrings
,
140 ignoreTemplateLiterals
= !!options
.ignoreTemplateLiterals
,
141 ignoreRegExpLiterals
= !!options
.ignoreRegExpLiterals
,
142 ignoreTrailingComments
= !!options
.ignoreTrailingComments
|| !!options
.ignoreComments
,
143 ignoreUrls
= !!options
.ignoreUrls
,
144 maxCommentLength
= options
.comments
;
145 let ignorePattern
= options
.ignorePattern
|| null;
148 ignorePattern
= new RegExp(ignorePattern
, "u");
151 //--------------------------------------------------------------------------
153 //--------------------------------------------------------------------------
156 * Tells if a given comment is trailing: it starts on the current line and
157 * extends to or past the end of the current line.
158 * @param {string} line The source line we want to check for a trailing comment on
159 * @param {number} lineNumber The one-indexed line number for line
160 * @param {ASTNode} comment The comment to inspect
161 * @returns {boolean} If the comment is trailing on the given line
163 function isTrailingComment(line
, lineNumber
, comment
) {
165 (comment
.loc
.start
.line
=== lineNumber
&& lineNumber
<= comment
.loc
.end
.line
) &&
166 (comment
.loc
.end
.line
> lineNumber
|| comment
.loc
.end
.column
=== line
.length
);
170 * Tells if a comment encompasses the entire line.
171 * @param {string} line The source line with a trailing comment
172 * @param {number} lineNumber The one-indexed line number this is on
173 * @param {ASTNode} comment The comment to remove
174 * @returns {boolean} If the comment covers the entire line
176 function isFullLineComment(line
, lineNumber
, comment
) {
177 const start
= comment
.loc
.start
,
178 end
= comment
.loc
.end
,
179 isFirstTokenOnLine
= !line
.slice(0, comment
.loc
.start
.column
).trim();
182 (start
.line
< lineNumber
|| (start
.line
=== lineNumber
&& isFirstTokenOnLine
)) &&
183 (end
.line
> lineNumber
|| (end
.line
=== lineNumber
&& end
.column
=== line
.length
));
187 * Check if a node is a JSXEmptyExpression contained in a single line JSXExpressionContainer.
188 * @param {ASTNode} node A node to check.
189 * @returns {boolean} True if the node is a JSXEmptyExpression contained in a single line JSXExpressionContainer.
191 function isJSXEmptyExpressionInSingleLineContainer(node
) {
192 if (!node
|| !node
.parent
|| node
.type
!== "JSXEmptyExpression" || node
.parent
.type
!== "JSXExpressionContainer") {
196 const parent
= node
.parent
;
198 return parent
.loc
.start
.line
=== parent
.loc
.end
.line
;
202 * Gets the line after the comment and any remaining trailing whitespace is
204 * @param {string} line The source line with a trailing comment
205 * @param {ASTNode} comment The comment to remove
206 * @returns {string} Line without comment and trailing whitespace
208 function stripTrailingComment(line
, comment
) {
210 // loc.column is zero-indexed
211 return line
.slice(0, comment
.loc
.start
.column
).replace(/\s+$/u, "");
215 * Ensure that an array exists at [key] on `object`, and add `value` to it.
216 * @param {Object} object the object to mutate
217 * @param {string} key the object's key
218 * @param {*} value the value to add
222 function ensureArrayAndPush(object
, key
, value
) {
223 if (!Array
.isArray(object
[key
])) {
226 object
[key
].push(value
);
230 * Retrieves an array containing all strings (" or ') in the source code.
231 * @returns {ASTNode[]} An array of string nodes.
233 function getAllStrings() {
234 return sourceCode
.ast
.tokens
.filter(token
=> (token
.type
=== "String" ||
235 (token
.type
=== "JSXText" && sourceCode
.getNodeByRangeIndex(token
.range
[0] - 1).type
=== "JSXAttribute")));
239 * Retrieves an array containing all template literals in the source code.
240 * @returns {ASTNode[]} An array of template literal nodes.
242 function getAllTemplateLiterals() {
243 return sourceCode
.ast
.tokens
.filter(token
=> token
.type
=== "Template");
248 * Retrieves an array containing all RegExp literals in the source code.
249 * @returns {ASTNode[]} An array of RegExp literal nodes.
251 function getAllRegExpLiterals() {
252 return sourceCode
.ast
.tokens
.filter(token
=> token
.type
=== "RegularExpression");
257 * A reducer to group an AST node by line number, both start and end.
258 * @param {Object} acc the accumulator
259 * @param {ASTNode} node the AST node in question
260 * @returns {Object} the modified accumulator
263 function groupByLineNumber(acc
, node
) {
264 for (let i
= node
.loc
.start
.line
; i
<= node
.loc
.end
.line
; ++i
) {
265 ensureArrayAndPush(acc
, i
, node
);
271 * Returns an array of all comments in the source code.
272 * If the element in the array is a JSXEmptyExpression contained with a single line JSXExpressionContainer,
273 * the element is changed with JSXExpressionContainer node.
274 * @returns {ASTNode[]} An array of comment nodes
276 function getAllComments() {
279 sourceCode
.getAllComments()
280 .forEach(commentNode
=> {
281 const containingNode
= sourceCode
.getNodeByRangeIndex(commentNode
.range
[0]);
283 if (isJSXEmptyExpressionInSingleLineContainer(containingNode
)) {
285 // push a unique node only
286 if (comments
[comments
.length
- 1] !== containingNode
.parent
) {
287 comments
.push(containingNode
.parent
);
290 comments
.push(commentNode
);
298 * Check the program for max length
299 * @param {ASTNode} node Node to examine
303 function checkProgramForMaxLength(node
) {
305 // split (honors line-ending)
306 const lines
= sourceCode
.lines
,
308 // list of comments to ignore
309 comments
= ignoreComments
|| maxCommentLength
|| ignoreTrailingComments
? getAllComments() : [];
311 // we iterate over comments in parallel with the lines
312 let commentsIndex
= 0;
314 const strings
= getAllStrings();
315 const stringsByLine
= strings
.reduce(groupByLineNumber
, {});
317 const templateLiterals
= getAllTemplateLiterals();
318 const templateLiteralsByLine
= templateLiterals
.reduce(groupByLineNumber
, {});
320 const regExpLiterals
= getAllRegExpLiterals();
321 const regExpLiteralsByLine
= regExpLiterals
.reduce(groupByLineNumber
, {});
323 lines
.forEach((line
, i
) => {
325 // i is zero-indexed, line numbers are one-indexed
326 const lineNumber
= i
+ 1;
329 * if we're checking comment length; we need to know whether this
332 let lineIsComment
= false;
336 * We can short-circuit the comment checks if we're already out of
339 if (commentsIndex
< comments
.length
) {
342 // iterate over comments until we find one past the current line
344 comment
= comments
[++commentsIndex
];
345 } while (comment
&& comment
.loc
.start
.line
<= lineNumber
);
347 // and step back by one
348 comment
= comments
[--commentsIndex
];
350 if (isFullLineComment(line
, lineNumber
, comment
)) {
351 lineIsComment
= true;
352 textToMeasure
= line
;
353 } else if (ignoreTrailingComments
&& isTrailingComment(line
, lineNumber
, comment
)) {
354 textToMeasure
= stripTrailingComment(line
, comment
);
356 // ignore multiple trailing comments in the same line
357 let lastIndex
= commentsIndex
;
359 while (isTrailingComment(textToMeasure
, lineNumber
, comments
[--lastIndex
])) {
360 textToMeasure
= stripTrailingComment(textToMeasure
, comments
[lastIndex
]);
363 textToMeasure
= line
;
366 textToMeasure
= line
;
368 if (ignorePattern
&& ignorePattern
.test(textToMeasure
) ||
369 ignoreUrls
&& URL_REGEXP
.test(textToMeasure
) ||
370 ignoreStrings
&& stringsByLine
[lineNumber
] ||
371 ignoreTemplateLiterals
&& templateLiteralsByLine
[lineNumber
] ||
372 ignoreRegExpLiterals
&& regExpLiteralsByLine
[lineNumber
]
379 const lineLength
= computeLineLength(textToMeasure
, tabWidth
);
380 const commentLengthApplies
= lineIsComment
&& maxCommentLength
;
382 if (lineIsComment
&& ignoreComments
) {
393 column
: textToMeasure
.length
397 if (commentLengthApplies
) {
398 if (lineLength
> maxCommentLength
) {
402 messageId
: "maxComment",
409 } else if (lineLength
> maxLength
) {
424 //--------------------------------------------------------------------------
426 //--------------------------------------------------------------------------
429 Program
: checkProgramForMaxLength