no-regex-spaces.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  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. module.exports = {
  29. meta: {
  30. type: "suggestion",
  31. docs: {
  32. description: "disallow multiple spaces in regular expressions",
  33. category: "Possible Errors",
  34. recommended: true,
  35. url: "https://eslint.org/docs/rules/no-regex-spaces"
  36. },
  37. schema: [],
  38. fixable: "code"
  39. },
  40. create(context) {
  41. /**
  42. * Validate regular expression
  43. * @param {ASTNode} nodeToReport Node to report.
  44. * @param {string} pattern Regular expression pattern to validate.
  45. * @param {string} rawPattern Raw representation of the pattern in the source code.
  46. * @param {number} rawPatternStartRange Start range of the pattern in the source code.
  47. * @param {string} flags Regular expression flags.
  48. * @returns {void}
  49. * @private
  50. */
  51. function checkRegex(nodeToReport, pattern, rawPattern, rawPatternStartRange, flags) {
  52. // Skip if there are no consecutive spaces in the source code, to avoid reporting e.g., RegExp(' \ ').
  53. if (!DOUBLE_SPACE.test(rawPattern)) {
  54. return;
  55. }
  56. const characterClassNodes = [];
  57. let regExpAST;
  58. try {
  59. regExpAST = regExpParser.parsePattern(pattern, 0, pattern.length, flags.includes("u"));
  60. } catch (e) {
  61. // Ignore regular expressions with syntax errors
  62. return;
  63. }
  64. regexpp.visitRegExpAST(regExpAST, {
  65. onCharacterClassEnter(ccNode) {
  66. characterClassNodes.push(ccNode);
  67. }
  68. });
  69. const spacesPattern = /( {2,})(?: [+*{?]|[^+*{?]|$)/gu;
  70. let match;
  71. while ((match = spacesPattern.exec(pattern))) {
  72. const { 1: { length }, index } = match;
  73. // Report only consecutive spaces that are not in character classes.
  74. if (
  75. characterClassNodes.every(({ start, end }) => index < start || end <= index)
  76. ) {
  77. context.report({
  78. node: nodeToReport,
  79. message: "Spaces are hard to count. Use {{{length}}}.",
  80. data: { length },
  81. fix(fixer) {
  82. if (pattern !== rawPattern) {
  83. return null;
  84. }
  85. return fixer.replaceTextRange(
  86. [rawPatternStartRange + index, rawPatternStartRange + index + length],
  87. ` {${length}}`
  88. );
  89. }
  90. });
  91. // Report only the first occurence of consecutive spaces
  92. return;
  93. }
  94. }
  95. }
  96. /**
  97. * Validate regular expression literals
  98. * @param {ASTNode} node node to validate
  99. * @returns {void}
  100. * @private
  101. */
  102. function checkLiteral(node) {
  103. if (node.regex) {
  104. const pattern = node.regex.pattern;
  105. const rawPattern = node.raw.slice(1, node.raw.lastIndexOf("/"));
  106. const rawPatternStartRange = node.range[0] + 1;
  107. const flags = node.regex.flags;
  108. checkRegex(
  109. node,
  110. pattern,
  111. rawPattern,
  112. rawPatternStartRange,
  113. flags
  114. );
  115. }
  116. }
  117. /**
  118. * Validate strings passed to the RegExp constructor
  119. * @param {ASTNode} node node to validate
  120. * @returns {void}
  121. * @private
  122. */
  123. function checkFunction(node) {
  124. const scope = context.getScope();
  125. const regExpVar = astUtils.getVariableByName(scope, "RegExp");
  126. const shadowed = regExpVar && regExpVar.defs.length > 0;
  127. const patternNode = node.arguments[0];
  128. const flagsNode = node.arguments[1];
  129. if (node.callee.type === "Identifier" && node.callee.name === "RegExp" && isString(patternNode) && !shadowed) {
  130. const pattern = patternNode.value;
  131. const rawPattern = patternNode.raw.slice(1, -1);
  132. const rawPatternStartRange = patternNode.range[0] + 1;
  133. const flags = isString(flagsNode) ? flagsNode.value : "";
  134. checkRegex(
  135. node,
  136. pattern,
  137. rawPattern,
  138. rawPatternStartRange,
  139. flags
  140. );
  141. }
  142. }
  143. return {
  144. Literal: checkLiteral,
  145. CallExpression: checkFunction,
  146. NewExpression: checkFunction
  147. };
  148. }
  149. };