prefer-regex-literals.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. /**
  2. * @fileoverview Rule to disallow use of the `RegExp` constructor in favor of regular expression literals
  3. * @author Milos Djermanovic
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. const { CALL, CONSTRUCT, ReferenceTracker, findVariable } = require("eslint-utils");
  11. const { RegExpValidator, visitRegExpAST, RegExpParser } = require("regexpp");
  12. const { canTokensBeAdjacent } = require("./utils/ast-utils");
  13. //------------------------------------------------------------------------------
  14. // Helpers
  15. //------------------------------------------------------------------------------
  16. const REGEXPP_LATEST_ECMA_VERSION = 2022;
  17. /**
  18. * Determines whether the given node is a string literal.
  19. * @param {ASTNode} node Node to check.
  20. * @returns {boolean} True if the node is a string literal.
  21. */
  22. function isStringLiteral(node) {
  23. return node.type === "Literal" && typeof node.value === "string";
  24. }
  25. /**
  26. * Determines whether the given node is a regex literal.
  27. * @param {ASTNode} node Node to check.
  28. * @returns {boolean} True if the node is a regex literal.
  29. */
  30. function isRegexLiteral(node) {
  31. return node.type === "Literal" && Object.prototype.hasOwnProperty.call(node, "regex");
  32. }
  33. /**
  34. * Determines whether the given node is a template literal without expressions.
  35. * @param {ASTNode} node Node to check.
  36. * @returns {boolean} True if the node is a template literal without expressions.
  37. */
  38. function isStaticTemplateLiteral(node) {
  39. return node.type === "TemplateLiteral" && node.expressions.length === 0;
  40. }
  41. const validPrecedingTokens = new Set([
  42. "(",
  43. ";",
  44. "[",
  45. ",",
  46. "=",
  47. "+",
  48. "*",
  49. "-",
  50. "?",
  51. "~",
  52. "%",
  53. "**",
  54. "!",
  55. "typeof",
  56. "instanceof",
  57. "&&",
  58. "||",
  59. "??",
  60. "return",
  61. "...",
  62. "delete",
  63. "void",
  64. "in",
  65. "<",
  66. ">",
  67. "<=",
  68. ">=",
  69. "==",
  70. "===",
  71. "!=",
  72. "!==",
  73. "<<",
  74. ">>",
  75. ">>>",
  76. "&",
  77. "|",
  78. "^",
  79. ":",
  80. "{",
  81. "=>",
  82. "*=",
  83. "<<=",
  84. ">>=",
  85. ">>>=",
  86. "^=",
  87. "|=",
  88. "&=",
  89. "??=",
  90. "||=",
  91. "&&=",
  92. "**=",
  93. "+=",
  94. "-=",
  95. "/=",
  96. "%=",
  97. "/",
  98. "do",
  99. "break",
  100. "continue",
  101. "debugger",
  102. "case",
  103. "throw"
  104. ]);
  105. //------------------------------------------------------------------------------
  106. // Rule Definition
  107. //------------------------------------------------------------------------------
  108. /** @type {import('../shared/types').Rule} */
  109. module.exports = {
  110. meta: {
  111. type: "suggestion",
  112. docs: {
  113. description: "Disallow use of the `RegExp` constructor in favor of regular expression literals",
  114. recommended: false,
  115. url: "https://eslint.org/docs/rules/prefer-regex-literals"
  116. },
  117. hasSuggestions: true,
  118. schema: [
  119. {
  120. type: "object",
  121. properties: {
  122. disallowRedundantWrapping: {
  123. type: "boolean",
  124. default: false
  125. }
  126. },
  127. additionalProperties: false
  128. }
  129. ],
  130. messages: {
  131. unexpectedRegExp: "Use a regular expression literal instead of the 'RegExp' constructor.",
  132. replaceWithLiteral: "Replace with an equivalent regular expression literal.",
  133. unexpectedRedundantRegExp: "Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor.",
  134. unexpectedRedundantRegExpWithFlags: "Use regular expression literal with flags instead of the 'RegExp' constructor."
  135. }
  136. },
  137. create(context) {
  138. const [{ disallowRedundantWrapping = false } = {}] = context.options;
  139. const sourceCode = context.getSourceCode();
  140. /**
  141. * Determines whether the given identifier node is a reference to a global variable.
  142. * @param {ASTNode} node `Identifier` node to check.
  143. * @returns {boolean} True if the identifier is a reference to a global variable.
  144. */
  145. function isGlobalReference(node) {
  146. const scope = context.getScope();
  147. const variable = findVariable(scope, node);
  148. return variable !== null && variable.scope.type === "global" && variable.defs.length === 0;
  149. }
  150. /**
  151. * Determines whether the given node is a String.raw`` tagged template expression
  152. * with a static template literal.
  153. * @param {ASTNode} node Node to check.
  154. * @returns {boolean} True if the node is String.raw`` with a static template.
  155. */
  156. function isStringRawTaggedStaticTemplateLiteral(node) {
  157. return node.type === "TaggedTemplateExpression" &&
  158. astUtils.isSpecificMemberAccess(node.tag, "String", "raw") &&
  159. isGlobalReference(astUtils.skipChainExpression(node.tag).object) &&
  160. isStaticTemplateLiteral(node.quasi);
  161. }
  162. /**
  163. * Gets the value of a string
  164. * @param {ASTNode} node The node to get the string of.
  165. * @returns {string|null} The value of the node.
  166. */
  167. function getStringValue(node) {
  168. if (isStringLiteral(node)) {
  169. return node.value;
  170. }
  171. if (isStaticTemplateLiteral(node)) {
  172. return node.quasis[0].value.cooked;
  173. }
  174. if (isStringRawTaggedStaticTemplateLiteral(node)) {
  175. return node.quasi.quasis[0].value.raw;
  176. }
  177. return null;
  178. }
  179. /**
  180. * Determines whether the given node is considered to be a static string by the logic of this rule.
  181. * @param {ASTNode} node Node to check.
  182. * @returns {boolean} True if the node is a static string.
  183. */
  184. function isStaticString(node) {
  185. return isStringLiteral(node) ||
  186. isStaticTemplateLiteral(node) ||
  187. isStringRawTaggedStaticTemplateLiteral(node);
  188. }
  189. /**
  190. * Determines whether the relevant arguments of the given are all static string literals.
  191. * @param {ASTNode} node Node to check.
  192. * @returns {boolean} True if all arguments are static strings.
  193. */
  194. function hasOnlyStaticStringArguments(node) {
  195. const args = node.arguments;
  196. if ((args.length === 1 || args.length === 2) && args.every(isStaticString)) {
  197. return true;
  198. }
  199. return false;
  200. }
  201. /**
  202. * Determines whether the arguments of the given node indicate that a regex literal is unnecessarily wrapped.
  203. * @param {ASTNode} node Node to check.
  204. * @returns {boolean} True if the node already contains a regex literal argument.
  205. */
  206. function isUnnecessarilyWrappedRegexLiteral(node) {
  207. const args = node.arguments;
  208. if (args.length === 1 && isRegexLiteral(args[0])) {
  209. return true;
  210. }
  211. if (args.length === 2 && isRegexLiteral(args[0]) && isStaticString(args[1])) {
  212. return true;
  213. }
  214. return false;
  215. }
  216. /**
  217. * Returns a ecmaVersion compatible for regexpp.
  218. * @param {number} ecmaVersion The ecmaVersion to convert.
  219. * @returns {import("regexpp/ecma-versions").EcmaVersion} The resulting ecmaVersion compatible for regexpp.
  220. */
  221. function getRegexppEcmaVersion(ecmaVersion) {
  222. if (ecmaVersion <= 5) {
  223. return 5;
  224. }
  225. return Math.min(ecmaVersion, REGEXPP_LATEST_ECMA_VERSION);
  226. }
  227. /**
  228. * Makes a character escaped or else returns null.
  229. * @param {string} character The character to escape.
  230. * @returns {string} The resulting escaped character.
  231. */
  232. function resolveEscapes(character) {
  233. switch (character) {
  234. case "\n":
  235. case "\\\n":
  236. return "\\n";
  237. case "\r":
  238. case "\\\r":
  239. return "\\r";
  240. case "\t":
  241. case "\\\t":
  242. return "\\t";
  243. case "\v":
  244. case "\\\v":
  245. return "\\v";
  246. case "\f":
  247. case "\\\f":
  248. return "\\f";
  249. case "/":
  250. return "\\/";
  251. default:
  252. return null;
  253. }
  254. }
  255. return {
  256. Program() {
  257. const scope = context.getScope();
  258. const tracker = new ReferenceTracker(scope);
  259. const traceMap = {
  260. RegExp: {
  261. [CALL]: true,
  262. [CONSTRUCT]: true
  263. }
  264. };
  265. for (const { node } of tracker.iterateGlobalReferences(traceMap)) {
  266. if (disallowRedundantWrapping && isUnnecessarilyWrappedRegexLiteral(node)) {
  267. if (node.arguments.length === 2) {
  268. context.report({ node, messageId: "unexpectedRedundantRegExpWithFlags" });
  269. } else {
  270. context.report({ node, messageId: "unexpectedRedundantRegExp" });
  271. }
  272. } else if (hasOnlyStaticStringArguments(node)) {
  273. let regexContent = getStringValue(node.arguments[0]);
  274. let noFix = false;
  275. let flags;
  276. if (node.arguments[1]) {
  277. flags = getStringValue(node.arguments[1]);
  278. }
  279. const regexppEcmaVersion = getRegexppEcmaVersion(context.languageOptions.ecmaVersion);
  280. const RegExpValidatorInstance = new RegExpValidator({ ecmaVersion: regexppEcmaVersion });
  281. try {
  282. RegExpValidatorInstance.validatePattern(regexContent, 0, regexContent.length, flags ? flags.includes("u") : false);
  283. if (flags) {
  284. RegExpValidatorInstance.validateFlags(flags);
  285. }
  286. } catch {
  287. noFix = true;
  288. }
  289. const tokenBefore = sourceCode.getTokenBefore(node);
  290. if (tokenBefore && !validPrecedingTokens.has(tokenBefore.value)) {
  291. noFix = true;
  292. }
  293. if (!/^[-a-zA-Z0-9\\[\](){} \t\r\n\v\f!@#$%^&*+^_=/~`.><?,'"|:;]*$/u.test(regexContent)) {
  294. noFix = true;
  295. }
  296. if (sourceCode.getCommentsInside(node).length > 0) {
  297. noFix = true;
  298. }
  299. if (regexContent && !noFix) {
  300. let charIncrease = 0;
  301. const ast = new RegExpParser({ ecmaVersion: regexppEcmaVersion }).parsePattern(regexContent, 0, regexContent.length, flags ? flags.includes("u") : false);
  302. visitRegExpAST(ast, {
  303. onCharacterEnter(characterNode) {
  304. const escaped = resolveEscapes(characterNode.raw);
  305. if (escaped) {
  306. regexContent =
  307. regexContent.slice(0, characterNode.start + charIncrease) +
  308. escaped +
  309. regexContent.slice(characterNode.end + charIncrease);
  310. if (characterNode.raw.length === 1) {
  311. charIncrease += 1;
  312. }
  313. }
  314. }
  315. });
  316. }
  317. const newRegExpValue = `/${regexContent || "(?:)"}/${flags || ""}`;
  318. context.report({
  319. node,
  320. messageId: "unexpectedRegExp",
  321. suggest: noFix ? [] : [{
  322. messageId: "replaceWithLiteral",
  323. fix(fixer) {
  324. const tokenAfter = sourceCode.getTokenAfter(node);
  325. return fixer.replaceText(
  326. node,
  327. (tokenBefore && !canTokensBeAdjacent(tokenBefore, newRegExpValue) && tokenBefore.range[1] === node.range[0] ? " " : "") +
  328. newRegExpValue +
  329. (tokenAfter && !canTokensBeAdjacent(newRegExpValue, tokenAfter) && node.range[1] === tokenAfter.range[0] ? " " : "")
  330. );
  331. }
  332. }]
  333. });
  334. }
  335. }
  336. }
  337. };
  338. }
  339. };