[d565449] | 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 | };
|
---|