require-unicode-regexp.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. /**
  2. * @fileoverview Rule to enforce the use of `u` or `v` flag on regular expressions.
  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('../types').Rule.RuleModule} */
  38. module.exports = {
  39. meta: {
  40. type: "suggestion",
  41. defaultOptions: [{}],
  42. docs: {
  43. description:
  44. "Enforce the use of `u` or `v` flag on regular expressions",
  45. recommended: false,
  46. url: "https://eslint.org/docs/latest/rules/require-unicode-regexp",
  47. },
  48. hasSuggestions: true,
  49. messages: {
  50. addUFlag: "Add the 'u' flag.",
  51. addVFlag: "Add the 'v' flag.",
  52. requireUFlag: "Use the 'u' flag.",
  53. requireVFlag: "Use the 'v' flag.",
  54. },
  55. schema: [
  56. {
  57. type: "object",
  58. properties: {
  59. requireFlag: {
  60. enum: ["u", "v"],
  61. },
  62. },
  63. additionalProperties: false,
  64. },
  65. ],
  66. },
  67. create(context) {
  68. const sourceCode = context.sourceCode;
  69. const [{ requireFlag }] = context.options;
  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:
  77. requireFlag === "v"
  78. ? "requireVFlag"
  79. : "requireUFlag",
  80. node,
  81. suggest: isValidWithUnicodeFlag(
  82. context.languageOptions.ecmaVersion,
  83. node.regex.pattern,
  84. requireFlag,
  85. )
  86. ? [
  87. {
  88. fix(fixer) {
  89. const replaceFlag =
  90. requireFlag ?? "u";
  91. const regex =
  92. sourceCode.getText(node);
  93. const slashPos =
  94. regex.lastIndexOf("/");
  95. if (requireFlag) {
  96. const flag =
  97. requireFlag === "u"
  98. ? "v"
  99. : "u";
  100. if (
  101. regex.includes(
  102. flag,
  103. slashPos,
  104. )
  105. ) {
  106. return fixer.replaceText(
  107. node,
  108. regex.slice(
  109. 0,
  110. slashPos,
  111. ) +
  112. regex
  113. .slice(slashPos)
  114. .replace(
  115. flag,
  116. requireFlag,
  117. ),
  118. );
  119. }
  120. }
  121. return fixer.insertTextAfter(
  122. node,
  123. replaceFlag,
  124. );
  125. },
  126. messageId:
  127. requireFlag === "v"
  128. ? "addVFlag"
  129. : "addUFlag",
  130. },
  131. ]
  132. : null,
  133. });
  134. }
  135. },
  136. Program(node) {
  137. const scope = sourceCode.getScope(node);
  138. const tracker = new ReferenceTracker(scope);
  139. const trackMap = {
  140. RegExp: { [CALL]: true, [CONSTRUCT]: true },
  141. };
  142. for (const { node: refNode } of tracker.iterateGlobalReferences(
  143. trackMap,
  144. )) {
  145. const [patternNode, flagsNode] = refNode.arguments;
  146. if (patternNode && patternNode.type === "SpreadElement") {
  147. continue;
  148. }
  149. const pattern = getStringIfConstant(patternNode, scope);
  150. const flags = getStringIfConstant(flagsNode, scope);
  151. let missingFlag = !flagsNode;
  152. if (typeof flags === "string") {
  153. missingFlag = checkFlags(requireFlag, flags);
  154. }
  155. if (missingFlag) {
  156. context.report({
  157. messageId:
  158. requireFlag === "v"
  159. ? "requireVFlag"
  160. : "requireUFlag",
  161. node: refNode,
  162. suggest:
  163. typeof pattern === "string" &&
  164. isValidWithUnicodeFlag(
  165. context.languageOptions.ecmaVersion,
  166. pattern,
  167. requireFlag,
  168. )
  169. ? [
  170. {
  171. fix(fixer) {
  172. const replaceFlag =
  173. requireFlag ?? "u";
  174. if (flagsNode) {
  175. if (
  176. (flagsNode.type ===
  177. "Literal" &&
  178. typeof flagsNode.value ===
  179. "string") ||
  180. flagsNode.type ===
  181. "TemplateLiteral"
  182. ) {
  183. const flagsNodeText =
  184. sourceCode.getText(
  185. flagsNode,
  186. );
  187. const flag =
  188. requireFlag ===
  189. "u"
  190. ? "v"
  191. : "u";
  192. if (
  193. flags.includes(
  194. flag,
  195. )
  196. ) {
  197. // Avoid replacing "u" in escapes like `\uXXXX`
  198. if (
  199. flagsNode.type ===
  200. "Literal" &&
  201. flagsNode.raw.includes(
  202. "\\",
  203. )
  204. ) {
  205. return null;
  206. }
  207. // Avoid replacing "u" in expressions like "`${regularFlags}g`"
  208. if (
  209. flagsNode.type ===
  210. "TemplateLiteral" &&
  211. (flagsNode
  212. .expressions
  213. .length ||
  214. flagsNode.quasis.some(
  215. ({
  216. value: {
  217. raw,
  218. },
  219. }) =>
  220. raw.includes(
  221. "\\",
  222. ),
  223. ))
  224. ) {
  225. return null;
  226. }
  227. return fixer.replaceText(
  228. flagsNode,
  229. flagsNodeText.replace(
  230. flag,
  231. replaceFlag,
  232. ),
  233. );
  234. }
  235. return fixer.replaceText(
  236. flagsNode,
  237. [
  238. flagsNodeText.slice(
  239. 0,
  240. flagsNodeText.length -
  241. 1,
  242. ),
  243. flagsNodeText.slice(
  244. flagsNodeText.length -
  245. 1,
  246. ),
  247. ].join(
  248. replaceFlag,
  249. ),
  250. );
  251. }
  252. // We intentionally don't suggest concatenating + "u" to non-literals
  253. return null;
  254. }
  255. const penultimateToken =
  256. sourceCode.getLastToken(
  257. refNode,
  258. { skip: 1 },
  259. ); // skip closing parenthesis
  260. return fixer.insertTextAfter(
  261. penultimateToken,
  262. astUtils.isCommaToken(
  263. penultimateToken,
  264. )
  265. ? ` "${replaceFlag}",`
  266. : `, "${replaceFlag}"`,
  267. );
  268. },
  269. messageId:
  270. requireFlag === "v"
  271. ? "addVFlag"
  272. : "addUFlag",
  273. },
  274. ]
  275. : null,
  276. });
  277. }
  278. }
  279. },
  280. };
  281. },
  282. };