max-len.js 13 KB

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