require-unicode-regexp.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. /**
  2. * @fileoverview Rule to enforce the use of `u` flag on RegExp.
  3. * @author Toru Nagashima
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const {
  10. CALL,
  11. CONSTRUCT,
  12. ReferenceTracker,
  13. getStringIfConstant
  14. } = require("@eslint-community/eslint-utils");
  15. const astUtils = require("./utils/ast-utils.js");
  16. const { isValidWithUnicodeFlag } = require("./utils/regular-expressions");
  17. /**
  18. * Checks whether the flag configuration should be treated as a missing flag.
  19. * @param {"u"|"v"|undefined} requireFlag A particular flag to require
  20. * @param {string} flags The regex flags
  21. * @returns {boolean} Whether the flag configuration results in a missing flag.
  22. */
  23. function checkFlags(requireFlag, flags) {
  24. let missingFlag;
  25. if (requireFlag === "v") {
  26. missingFlag = !flags.includes("v");
  27. } else if (requireFlag === "u") {
  28. missingFlag = !flags.includes("u");
  29. } else {
  30. missingFlag = !flags.includes("u") && !flags.includes("v");
  31. }
  32. return missingFlag;
  33. }
  34. //------------------------------------------------------------------------------
  35. // Rule Definition
  36. //------------------------------------------------------------------------------
  37. /** @type {import('../shared/types').Rule} */
  38. module.exports = {
  39. meta: {
  40. type: "suggestion",
  41. docs: {
  42. description: "Enforce the use of `u` or `v` flag on RegExp",
  43. recommended: false,
  44. url: "https://eslint.org/docs/latest/rules/require-unicode-regexp"
  45. },
  46. hasSuggestions: true,
  47. messages: {
  48. addUFlag: "Add the 'u' flag.",
  49. addVFlag: "Add the 'v' flag.",
  50. requireUFlag: "Use the 'u' flag.",
  51. requireVFlag: "Use the 'v' flag."
  52. },
  53. schema: [
  54. {
  55. type: "object",
  56. properties: {
  57. requireFlag: {
  58. enum: ["u", "v"]
  59. }
  60. },
  61. additionalProperties: false
  62. }
  63. ]
  64. },
  65. create(context) {
  66. const sourceCode = context.sourceCode;
  67. const {
  68. requireFlag
  69. } = context.options[0] ?? {};
  70. return {
  71. "Literal[regex]"(node) {
  72. const flags = node.regex.flags || "";
  73. const missingFlag = checkFlags(requireFlag, flags);
  74. if (missingFlag) {
  75. context.report({
  76. messageId: requireFlag === "v" ? "requireVFlag" : "requireUFlag",
  77. node,
  78. suggest: isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, node.regex.pattern, requireFlag)
  79. ? [
  80. {
  81. fix(fixer) {
  82. const replaceFlag = requireFlag ?? "u";
  83. const regex = sourceCode.getText(node);
  84. const slashPos = regex.lastIndexOf("/");
  85. if (requireFlag) {
  86. const flag = requireFlag === "u" ? "v" : "u";
  87. if (regex.includes(flag, slashPos)) {
  88. return fixer.replaceText(
  89. node,
  90. regex.slice(0, slashPos) +
  91. regex.slice(slashPos).replace(flag, requireFlag)
  92. );
  93. }
  94. }
  95. return fixer.insertTextAfter(node, replaceFlag);
  96. },
  97. messageId: requireFlag === "v" ? "addVFlag" : "addUFlag"
  98. }
  99. ]
  100. : null
  101. });
  102. }
  103. },
  104. Program(node) {
  105. const scope = sourceCode.getScope(node);
  106. const tracker = new ReferenceTracker(scope);
  107. const trackMap = {
  108. RegExp: { [CALL]: true, [CONSTRUCT]: true }
  109. };
  110. for (const { node: refNode } of tracker.iterateGlobalReferences(trackMap)) {
  111. const [patternNode, flagsNode] = refNode.arguments;
  112. if (patternNode && patternNode.type === "SpreadElement") {
  113. continue;
  114. }
  115. const pattern = getStringIfConstant(patternNode, scope);
  116. const flags = getStringIfConstant(flagsNode, scope);
  117. let missingFlag = !flagsNode;
  118. if (typeof flags === "string") {
  119. missingFlag = checkFlags(requireFlag, flags);
  120. }
  121. if (missingFlag) {
  122. context.report({
  123. messageId: requireFlag === "v" ? "requireVFlag" : "requireUFlag",
  124. node: refNode,
  125. suggest: typeof pattern === "string" && isValidWithUnicodeFlag(context.languageOptions.ecmaVersion, pattern, requireFlag)
  126. ? [
  127. {
  128. fix(fixer) {
  129. const replaceFlag = requireFlag ?? "u";
  130. if (flagsNode) {
  131. if ((flagsNode.type === "Literal" && typeof flagsNode.value === "string") || flagsNode.type === "TemplateLiteral") {
  132. const flagsNodeText = sourceCode.getText(flagsNode);
  133. const flag = requireFlag === "u" ? "v" : "u";
  134. if (flags.includes(flag)) {
  135. // Avoid replacing "u" in escapes like `\uXXXX`
  136. if (flagsNode.type === "Literal" && flagsNode.raw.includes("\\")) {
  137. return null;
  138. }
  139. // Avoid replacing "u" in expressions like "`${regularFlags}g`"
  140. if (flagsNode.type === "TemplateLiteral" && (
  141. flagsNode.expressions.length ||
  142. flagsNode.quasis.some(({ value: { raw } }) => raw.includes("\\"))
  143. )) {
  144. return null;
  145. }
  146. return fixer.replaceText(flagsNode, flagsNodeText.replace(flag, replaceFlag));
  147. }
  148. return fixer.replaceText(flagsNode, [
  149. flagsNodeText.slice(0, flagsNodeText.length - 1),
  150. flagsNodeText.slice(flagsNodeText.length - 1)
  151. ].join(replaceFlag));
  152. }
  153. // We intentionally don't suggest concatenating + "u" to non-literals
  154. return null;
  155. }
  156. const penultimateToken = sourceCode.getLastToken(refNode, { skip: 1 }); // skip closing parenthesis
  157. return fixer.insertTextAfter(
  158. penultimateToken,
  159. astUtils.isCommaToken(penultimateToken)
  160. ? ` "${replaceFlag}",`
  161. : `, "${replaceFlag}"`
  162. );
  163. },
  164. messageId: requireFlag === "v" ? "addVFlag" : "addUFlag"
  165. }
  166. ]
  167. : null
  168. });
  169. }
  170. }
  171. }
  172. };
  173. }
  174. };