no-regex-spaces.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. /**
  2. * @fileoverview Rule to count multiple spaces in regular expressions
  3. * @author Matt DuVall <http://www.mattduvall.com/>
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. const regexpp = require("regexpp");
  11. //------------------------------------------------------------------------------
  12. // Helpers
  13. //------------------------------------------------------------------------------
  14. const regExpParser = new regexpp.RegExpParser();
  15. const DOUBLE_SPACE = / {2}/u;
  16. /**
  17. * Check if node is a string
  18. * @param {ASTNode} node node to evaluate
  19. * @returns {boolean} True if its a string
  20. * @private
  21. */
  22. function isString(node) {
  23. return node && node.type === "Literal" && typeof node.value === "string";
  24. }
  25. //------------------------------------------------------------------------------
  26. // Rule Definition
  27. //------------------------------------------------------------------------------
  28. /** @type {import('../shared/types').Rule} */
  29. module.exports = {
  30. meta: {
  31. type: "suggestion",
  32. docs: {
  33. description: "Disallow multiple spaces in regular expressions",
  34. recommended: true,
  35. url: "https://eslint.org/docs/rules/no-regex-spaces"
  36. },
  37. schema: [],
  38. fixable: "code",
  39. messages: {
  40. multipleSpaces: "Spaces are hard to count. Use {{{length}}}."
  41. }
  42. },
  43. create(context) {
  44. /**
  45. * Validate regular expression
  46. * @param {ASTNode} nodeToReport Node to report.
  47. * @param {string} pattern Regular expression pattern to validate.
  48. * @param {string} rawPattern Raw representation of the pattern in the source code.
  49. * @param {number} rawPatternStartRange Start range of the pattern in the source code.
  50. * @param {string} flags Regular expression flags.
  51. * @returns {void}
  52. * @private
  53. */
  54. function checkRegex(nodeToReport, pattern, rawPattern, rawPatternStartRange, flags) {
  55. // Skip if there are no consecutive spaces in the source code, to avoid reporting e.g., RegExp(' \ ').
  56. if (!DOUBLE_SPACE.test(rawPattern)) {
  57. return;
  58. }
  59. const characterClassNodes = [];
  60. let regExpAST;
  61. try {
  62. regExpAST = regExpParser.parsePattern(pattern, 0, pattern.length, flags.includes("u"));
  63. } catch {
  64. // Ignore regular expressions with syntax errors
  65. return;
  66. }
  67. regexpp.visitRegExpAST(regExpAST, {
  68. onCharacterClassEnter(ccNode) {
  69. characterClassNodes.push(ccNode);
  70. }
  71. });
  72. const spacesPattern = /( {2,})(?: [+*{?]|[^+*{?]|$)/gu;
  73. let match;
  74. while ((match = spacesPattern.exec(pattern))) {
  75. const { 1: { length }, index } = match;
  76. // Report only consecutive spaces that are not in character classes.
  77. if (
  78. characterClassNodes.every(({ start, end }) => index < start || end <= index)
  79. ) {
  80. context.report({
  81. node: nodeToReport,
  82. messageId: "multipleSpaces",
  83. data: { length },
  84. fix(fixer) {
  85. if (pattern !== rawPattern) {
  86. return null;
  87. }
  88. return fixer.replaceTextRange(
  89. [rawPatternStartRange + index, rawPatternStartRange + index + length],
  90. ` {${length}}`
  91. );
  92. }
  93. });
  94. // Report only the first occurrence of consecutive spaces
  95. return;
  96. }
  97. }
  98. }
  99. /**
  100. * Validate regular expression literals
  101. * @param {ASTNode} node node to validate
  102. * @returns {void}
  103. * @private
  104. */
  105. function checkLiteral(node) {
  106. if (node.regex) {
  107. const pattern = node.regex.pattern;
  108. const rawPattern = node.raw.slice(1, node.raw.lastIndexOf("/"));
  109. const rawPatternStartRange = node.range[0] + 1;
  110. const flags = node.regex.flags;
  111. checkRegex(
  112. node,
  113. pattern,
  114. rawPattern,
  115. rawPatternStartRange,
  116. flags
  117. );
  118. }
  119. }
  120. /**
  121. * Validate strings passed to the RegExp constructor
  122. * @param {ASTNode} node node to validate
  123. * @returns {void}
  124. * @private
  125. */
  126. function checkFunction(node) {
  127. const scope = context.getScope();
  128. const regExpVar = astUtils.getVariableByName(scope, "RegExp");
  129. const shadowed = regExpVar && regExpVar.defs.length > 0;
  130. const patternNode = node.arguments[0];
  131. const flagsNode = node.arguments[1];
  132. if (node.callee.type === "Identifier" && node.callee.name === "RegExp" && isString(patternNode) && !shadowed) {
  133. const pattern = patternNode.value;
  134. const rawPattern = patternNode.raw.slice(1, -1);
  135. const rawPatternStartRange = patternNode.range[0] + 1;
  136. const flags = isString(flagsNode) ? flagsNode.value : "";
  137. checkRegex(
  138. node,
  139. pattern,
  140. rawPattern,
  141. rawPatternStartRange,
  142. flags
  143. );
  144. }
  145. }
  146. return {
  147. Literal: checkLiteral,
  148. CallExpression: checkFunction,
  149. NewExpression: checkFunction
  150. };
  151. }
  152. };