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",
73 url
: "https://eslint.org/docs/rules/max-len"
77 OPTIONS_OR_INTEGER_SCHEMA
,
78 OPTIONS_OR_INTEGER_SCHEMA
,
82 max
: "This line has a length of {{lineLength}}. Maximum allowed is {{maxLength}}.",
83 maxComment
: "This line has a comment length of {{lineLength}}. Maximum allowed is {{maxCommentLength}}."
90 * Inspired by http://tools.ietf.org/html/rfc3986#appendix-B, however:
91 * - They're matching an entire string that we know is a URI
92 * - We're matching part of a string where we think there *might* be a URL
93 * - We're only concerned about URLs, as picking out any URI would cause
94 * too many false positives
95 * - We don't care about matching the entire URL, any small segment is fine
97 const URL_REGEXP
= /[^:/?#]:\/\/[^?#]/u;
99 const sourceCode
= context
.getSourceCode();
102 * Computes the length of a line that may contain tabs. The width of each
103 * tab will be the number of spaces to the next tab stop.
104 * @param {string} line The line.
105 * @param {int} tabWidth The width of each tab stop in spaces.
106 * @returns {int} The computed line length.
109 function computeLineLength(line
, tabWidth
) {
110 let extraCharacterCount
= 0;
112 line
.replace(/\t/gu, (match
, offset
) => {
113 const totalOffset
= offset
+ extraCharacterCount
,
114 previousTabStopOffset
= tabWidth
? totalOffset
% tabWidth
: 0,
115 spaceCount
= tabWidth
- previousTabStopOffset
;
117 extraCharacterCount
+= spaceCount
- 1; // -1 for the replaced tab
119 return Array
.from(line
).length
+ extraCharacterCount
;
122 // The options object must be the last option specified…
123 const options
= Object
.assign({}, context
.options
[context
.options
.length
- 1]);
125 // …but max code length…
126 if (typeof context
.options
[0] === "number") {
127 options
.code
= context
.options
[0];
130 // …and tabWidth can be optionally specified directly as integers.
131 if (typeof context
.options
[1] === "number") {
132 options
.tabWidth
= context
.options
[1];
135 const maxLength
= typeof options
.code
=== "number" ? options
.code
: 80,
136 tabWidth
= typeof options
.tabWidth
=== "number" ? options
.tabWidth
: 4,
137 ignoreComments
= !!options
.ignoreComments
,
138 ignoreStrings
= !!options
.ignoreStrings
,
139 ignoreTemplateLiterals
= !!options
.ignoreTemplateLiterals
,
140 ignoreRegExpLiterals
= !!options
.ignoreRegExpLiterals
,
141 ignoreTrailingComments
= !!options
.ignoreTrailingComments
|| !!options
.ignoreComments
,
142 ignoreUrls
= !!options
.ignoreUrls
,
143 maxCommentLength
= options
.comments
;
144 let ignorePattern
= options
.ignorePattern
|| null;
147 ignorePattern
= new RegExp(ignorePattern
, "u");
150 //--------------------------------------------------------------------------
152 //--------------------------------------------------------------------------
155 * Tells if a given comment is trailing: it starts on the current line and
156 * extends to or past the end of the current line.
157 * @param {string} line The source line we want to check for a trailing comment on
158 * @param {number} lineNumber The one-indexed line number for line
159 * @param {ASTNode} comment The comment to inspect
160 * @returns {boolean} If the comment is trailing on the given line
162 function isTrailingComment(line
, lineNumber
, comment
) {
164 (comment
.loc
.start
.line
=== lineNumber
&& lineNumber
<= comment
.loc
.end
.line
) &&
165 (comment
.loc
.end
.line
> lineNumber
|| comment
.loc
.end
.column
=== line
.length
);
169 * Tells if a comment encompasses the entire line.
170 * @param {string} line The source line with a trailing comment
171 * @param {number} lineNumber The one-indexed line number this is on
172 * @param {ASTNode} comment The comment to remove
173 * @returns {boolean} If the comment covers the entire line
175 function isFullLineComment(line
, lineNumber
, comment
) {
176 const start
= comment
.loc
.start
,
177 end
= comment
.loc
.end
,
178 isFirstTokenOnLine
= !line
.slice(0, comment
.loc
.start
.column
).trim();
181 (start
.line
< lineNumber
|| (start
.line
=== lineNumber
&& isFirstTokenOnLine
)) &&
182 (end
.line
> lineNumber
|| (end
.line
=== lineNumber
&& end
.column
=== line
.length
));
186 * Check if a node is a JSXEmptyExpression contained in a single line JSXExpressionContainer.
187 * @param {ASTNode} node A node to check.
188 * @returns {boolean} True if the node is a JSXEmptyExpression contained in a single line JSXExpressionContainer.
190 function isJSXEmptyExpressionInSingleLineContainer(node
) {
191 if (!node
|| !node
.parent
|| node
.type
!== "JSXEmptyExpression" || node
.parent
.type
!== "JSXExpressionContainer") {
195 const parent
= node
.parent
;
197 return parent
.loc
.start
.line
=== parent
.loc
.end
.line
;
201 * Gets the line after the comment and any remaining trailing whitespace is
203 * @param {string} line The source line with a trailing comment
204 * @param {ASTNode} comment The comment to remove
205 * @returns {string} Line without comment and trailing whitespace
207 function stripTrailingComment(line
, comment
) {
209 // loc.column is zero-indexed
210 return line
.slice(0, comment
.loc
.start
.column
).replace(/\s+$/u, "");
214 * Ensure that an array exists at [key] on `object`, and add `value` to it.
215 * @param {Object} object the object to mutate
216 * @param {string} key the object's key
217 * @param {any} value the value to add
221 function ensureArrayAndPush(object
, key
, value
) {
222 if (!Array
.isArray(object
[key
])) {
225 object
[key
].push(value
);
229 * Retrieves an array containing all strings (" or ') in the source code.
230 * @returns {ASTNode[]} An array of string nodes.
232 function getAllStrings() {
233 return sourceCode
.ast
.tokens
.filter(token
=> (token
.type
=== "String" ||
234 (token
.type
=== "JSXText" && sourceCode
.getNodeByRangeIndex(token
.range
[0] - 1).type
=== "JSXAttribute")));
238 * Retrieves an array containing all template literals in the source code.
239 * @returns {ASTNode[]} An array of template literal nodes.
241 function getAllTemplateLiterals() {
242 return sourceCode
.ast
.tokens
.filter(token
=> token
.type
=== "Template");
247 * Retrieves an array containing all RegExp literals in the source code.
248 * @returns {ASTNode[]} An array of RegExp literal nodes.
250 function getAllRegExpLiterals() {
251 return sourceCode
.ast
.tokens
.filter(token
=> token
.type
=== "RegularExpression");
256 * A reducer to group an AST node by line number, both start and end.
257 * @param {Object} acc the accumulator
258 * @param {ASTNode} node the AST node in question
259 * @returns {Object} the modified accumulator
262 function groupByLineNumber(acc
, node
) {
263 for (let i
= node
.loc
.start
.line
; i
<= node
.loc
.end
.line
; ++i
) {
264 ensureArrayAndPush(acc
, i
, node
);
270 * Returns an array of all comments in the source code.
271 * If the element in the array is a JSXEmptyExpression contained with a single line JSXExpressionContainer,
272 * the element is changed with JSXExpressionContainer node.
273 * @returns {ASTNode[]} An array of comment nodes
275 function getAllComments() {
278 sourceCode
.getAllComments()
279 .forEach(commentNode
=> {
280 const containingNode
= sourceCode
.getNodeByRangeIndex(commentNode
.range
[0]);
282 if (isJSXEmptyExpressionInSingleLineContainer(containingNode
)) {
284 // push a unique node only
285 if (comments
[comments
.length
- 1] !== containingNode
.parent
) {
286 comments
.push(containingNode
.parent
);
289 comments
.push(commentNode
);
297 * Check the program for max length
298 * @param {ASTNode} node Node to examine
302 function checkProgramForMaxLength(node
) {
304 // split (honors line-ending)
305 const lines
= sourceCode
.lines
,
307 // list of comments to ignore
308 comments
= ignoreComments
|| maxCommentLength
|| ignoreTrailingComments
? getAllComments() : [];
310 // we iterate over comments in parallel with the lines
311 let commentsIndex
= 0;
313 const strings
= getAllStrings();
314 const stringsByLine
= strings
.reduce(groupByLineNumber
, {});
316 const templateLiterals
= getAllTemplateLiterals();
317 const templateLiteralsByLine
= templateLiterals
.reduce(groupByLineNumber
, {});
319 const regExpLiterals
= getAllRegExpLiterals();
320 const regExpLiteralsByLine
= regExpLiterals
.reduce(groupByLineNumber
, {});
322 lines
.forEach((line
, i
) => {
324 // i is zero-indexed, line numbers are one-indexed
325 const lineNumber
= i
+ 1;
328 * if we're checking comment length; we need to know whether this
331 let lineIsComment
= false;
335 * We can short-circuit the comment checks if we're already out of
338 if (commentsIndex
< comments
.length
) {
341 // iterate over comments until we find one past the current line
343 comment
= comments
[++commentsIndex
];
344 } while (comment
&& comment
.loc
.start
.line
<= lineNumber
);
346 // and step back by one
347 comment
= comments
[--commentsIndex
];
349 if (isFullLineComment(line
, lineNumber
, comment
)) {
350 lineIsComment
= true;
351 textToMeasure
= line
;
352 } else if (ignoreTrailingComments
&& isTrailingComment(line
, lineNumber
, comment
)) {
353 textToMeasure
= stripTrailingComment(line
, comment
);
355 // ignore multiple trailing comments in the same line
356 let lastIndex
= commentsIndex
;
358 while (isTrailingComment(textToMeasure
, lineNumber
, comments
[--lastIndex
])) {
359 textToMeasure
= stripTrailingComment(textToMeasure
, comments
[lastIndex
]);
362 textToMeasure
= line
;
365 textToMeasure
= line
;
367 if (ignorePattern
&& ignorePattern
.test(textToMeasure
) ||
368 ignoreUrls
&& URL_REGEXP
.test(textToMeasure
) ||
369 ignoreStrings
&& stringsByLine
[lineNumber
] ||
370 ignoreTemplateLiterals
&& templateLiteralsByLine
[lineNumber
] ||
371 ignoreRegExpLiterals
&& regExpLiteralsByLine
[lineNumber
]
378 const lineLength
= computeLineLength(textToMeasure
, tabWidth
);
379 const commentLengthApplies
= lineIsComment
&& maxCommentLength
;
381 if (lineIsComment
&& ignoreComments
) {
392 column
: textToMeasure
.length
396 if (commentLengthApplies
) {
397 if (lineLength
> maxCommentLength
) {
401 messageId
: "maxComment",
408 } else if (lineLength
> maxLength
) {
423 //--------------------------------------------------------------------------
425 //--------------------------------------------------------------------------
428 Program
: checkProgramForMaxLength