prefer-regex-literals.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  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-community/eslint-utils");
  11. const { RegExpValidator, visitRegExpAST, RegExpParser } = require("@eslint-community/regexpp");
  12. const { canTokensBeAdjacent } = require("./utils/ast-utils");
  13. const { REGEXPP_LATEST_ECMA_VERSION } = require("./utils/regular-expressions");
  14. //------------------------------------------------------------------------------
  15. // Helpers
  16. //------------------------------------------------------------------------------
  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.hasOwn(node, "regex");
  32. }
  33. const validPrecedingTokens = new Set([
  34. "(",
  35. ";",
  36. "[",
  37. ",",
  38. "=",
  39. "+",
  40. "*",
  41. "-",
  42. "?",
  43. "~",
  44. "%",
  45. "**",
  46. "!",
  47. "typeof",
  48. "instanceof",
  49. "&&",
  50. "||",
  51. "??",
  52. "return",
  53. "...",
  54. "delete",
  55. "void",
  56. "in",
  57. "<",
  58. ">",
  59. "<=",
  60. ">=",
  61. "==",
  62. "===",
  63. "!=",
  64. "!==",
  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. "do",
  91. "break",
  92. "continue",
  93. "debugger",
  94. "case",
  95. "throw"
  96. ]);
  97. //------------------------------------------------------------------------------
  98. // Rule Definition
  99. //------------------------------------------------------------------------------
  100. /** @type {import('../shared/types').Rule} */
  101. module.exports = {
  102. meta: {
  103. type: "suggestion",
  104. defaultOptions: [{
  105. disallowRedundantWrapping: false
  106. }],
  107. docs: {
  108. description: "Disallow use of the `RegExp` constructor in favor of regular expression literals",
  109. recommended: false,
  110. url: "https://eslint.org/docs/latest/rules/prefer-regex-literals"
  111. },
  112. hasSuggestions: true,
  113. schema: [
  114. {
  115. type: "object",
  116. properties: {
  117. disallowRedundantWrapping: {
  118. type: "boolean"
  119. }
  120. },
  121. additionalProperties: false
  122. }
  123. ],
  124. messages: {
  125. unexpectedRegExp: "Use a regular expression literal instead of the 'RegExp' constructor.",
  126. replaceWithLiteral: "Replace with an equivalent regular expression literal.",
  127. replaceWithLiteralAndFlags: "Replace with an equivalent regular expression literal with flags '{{ flags }}'.",
  128. replaceWithIntendedLiteralAndFlags: "Replace with a regular expression literal with flags '{{ flags }}'.",
  129. unexpectedRedundantRegExp: "Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor.",
  130. unexpectedRedundantRegExpWithFlags: "Use regular expression literal with flags instead of the 'RegExp' constructor."
  131. }
  132. },
  133. create(context) {
  134. const [{ disallowRedundantWrapping }] = context.options;
  135. const sourceCode = context.sourceCode;
  136. /**
  137. * Determines whether the given identifier node is a reference to a global variable.
  138. * @param {ASTNode} node `Identifier` node to check.
  139. * @returns {boolean} True if the identifier is a reference to a global variable.
  140. */
  141. function isGlobalReference(node) {
  142. const scope = sourceCode.getScope(node);
  143. const variable = findVariable(scope, node);
  144. return variable !== null && variable.scope.type === "global" && variable.defs.length === 0;
  145. }
  146. /**
  147. * Determines whether the given node is a String.raw`` tagged template expression
  148. * with a static template literal.
  149. * @param {ASTNode} node Node to check.
  150. * @returns {boolean} True if the node is String.raw`` with a static template.
  151. */
  152. function isStringRawTaggedStaticTemplateLiteral(node) {
  153. return node.type === "TaggedTemplateExpression" &&
  154. astUtils.isSpecificMemberAccess(node.tag, "String", "raw") &&
  155. isGlobalReference(astUtils.skipChainExpression(node.tag).object) &&
  156. astUtils.isStaticTemplateLiteral(node.quasi);
  157. }
  158. /**
  159. * Gets the value of a string
  160. * @param {ASTNode} node The node to get the string of.
  161. * @returns {string|null} The value of the node.
  162. */
  163. function getStringValue(node) {
  164. if (isStringLiteral(node)) {
  165. return node.value;
  166. }
  167. if (astUtils.isStaticTemplateLiteral(node)) {
  168. return node.quasis[0].value.cooked;
  169. }
  170. if (isStringRawTaggedStaticTemplateLiteral(node)) {
  171. return node.quasi.quasis[0].value.raw;
  172. }
  173. return null;
  174. }
  175. /**
  176. * Determines whether the given node is considered to be a static string by the logic of this rule.
  177. * @param {ASTNode} node Node to check.
  178. * @returns {boolean} True if the node is a static string.
  179. */
  180. function isStaticString(node) {
  181. return isStringLiteral(node) ||
  182. astUtils.isStaticTemplateLiteral(node) ||
  183. isStringRawTaggedStaticTemplateLiteral(node);
  184. }
  185. /**
  186. * Determines whether the relevant arguments of the given are all static string literals.
  187. * @param {ASTNode} node Node to check.
  188. * @returns {boolean} True if all arguments are static strings.
  189. */
  190. function hasOnlyStaticStringArguments(node) {
  191. const args = node.arguments;
  192. if ((args.length === 1 || args.length === 2) && args.every(isStaticString)) {
  193. return true;
  194. }
  195. return false;
  196. }
  197. /**
  198. * Determines whether the arguments of the given node indicate that a regex literal is unnecessarily wrapped.
  199. * @param {ASTNode} node Node to check.
  200. * @returns {boolean} True if the node already contains a regex literal argument.
  201. */
  202. function isUnnecessarilyWrappedRegexLiteral(node) {
  203. const args = node.arguments;
  204. if (args.length === 1 && isRegexLiteral(args[0])) {
  205. return true;
  206. }
  207. if (args.length === 2 && isRegexLiteral(args[0]) && isStaticString(args[1])) {
  208. return true;
  209. }
  210. return false;
  211. }
  212. /**
  213. * Returns a ecmaVersion compatible for regexpp.
  214. * @param {number} ecmaVersion The ecmaVersion to convert.
  215. * @returns {import("@eslint-community/regexpp/ecma-versions").EcmaVersion} The resulting ecmaVersion compatible for regexpp.
  216. */
  217. function getRegexppEcmaVersion(ecmaVersion) {
  218. if (ecmaVersion <= 5) {
  219. return 5;
  220. }
  221. return Math.min(ecmaVersion, REGEXPP_LATEST_ECMA_VERSION);
  222. }
  223. const regexppEcmaVersion = getRegexppEcmaVersion(context.languageOptions.ecmaVersion);
  224. /**
  225. * Makes a character escaped or else returns null.
  226. * @param {string} character The character to escape.
  227. * @returns {string} The resulting escaped character.
  228. */
  229. function resolveEscapes(character) {
  230. switch (character) {
  231. case "\n":
  232. case "\\\n":
  233. return "\\n";
  234. case "\r":
  235. case "\\\r":
  236. return "\\r";
  237. case "\t":
  238. case "\\\t":
  239. return "\\t";
  240. case "\v":
  241. case "\\\v":
  242. return "\\v";
  243. case "\f":
  244. case "\\\f":
  245. return "\\f";
  246. case "/":
  247. return "\\/";
  248. default:
  249. return null;
  250. }
  251. }
  252. /**
  253. * Checks whether the given regex and flags are valid for the ecma version or not.
  254. * @param {string} pattern The regex pattern to check.
  255. * @param {string | undefined} flags The regex flags to check.
  256. * @returns {boolean} True if the given regex pattern and flags are valid for the ecma version.
  257. */
  258. function isValidRegexForEcmaVersion(pattern, flags) {
  259. const validator = new RegExpValidator({ ecmaVersion: regexppEcmaVersion });
  260. try {
  261. validator.validatePattern(pattern, 0, pattern.length, {
  262. unicode: flags ? flags.includes("u") : false,
  263. unicodeSets: flags ? flags.includes("v") : false
  264. });
  265. if (flags) {
  266. validator.validateFlags(flags);
  267. }
  268. return true;
  269. } catch {
  270. return false;
  271. }
  272. }
  273. /**
  274. * Checks whether two given regex flags contain the same flags or not.
  275. * @param {string} flagsA The regex flags.
  276. * @param {string} flagsB The regex flags.
  277. * @returns {boolean} True if two regex flags contain same flags.
  278. */
  279. function areFlagsEqual(flagsA, flagsB) {
  280. return [...flagsA].sort().join("") === [...flagsB].sort().join("");
  281. }
  282. /**
  283. * Merges two regex flags.
  284. * @param {string} flagsA The regex flags.
  285. * @param {string} flagsB The regex flags.
  286. * @returns {string} The merged regex flags.
  287. */
  288. function mergeRegexFlags(flagsA, flagsB) {
  289. const flagsSet = new Set([
  290. ...flagsA,
  291. ...flagsB
  292. ]);
  293. return [...flagsSet].join("");
  294. }
  295. /**
  296. * Checks whether a give node can be fixed to the given regex pattern and flags.
  297. * @param {ASTNode} node The node to check.
  298. * @param {string} pattern The regex pattern to check.
  299. * @param {string} flags The regex flags
  300. * @returns {boolean} True if a node can be fixed to the given regex pattern and flags.
  301. */
  302. function canFixTo(node, pattern, flags) {
  303. const tokenBefore = sourceCode.getTokenBefore(node);
  304. return sourceCode.getCommentsInside(node).length === 0 &&
  305. (!tokenBefore || validPrecedingTokens.has(tokenBefore.value)) &&
  306. isValidRegexForEcmaVersion(pattern, flags);
  307. }
  308. /**
  309. * Returns a safe output code considering the before and after tokens.
  310. * @param {ASTNode} node The regex node.
  311. * @param {string} newRegExpValue The new regex expression value.
  312. * @returns {string} The output code.
  313. */
  314. function getSafeOutput(node, newRegExpValue) {
  315. const tokenBefore = sourceCode.getTokenBefore(node);
  316. const tokenAfter = sourceCode.getTokenAfter(node);
  317. return (tokenBefore && !canTokensBeAdjacent(tokenBefore, newRegExpValue) && tokenBefore.range[1] === node.range[0] ? " " : "") +
  318. newRegExpValue +
  319. (tokenAfter && !canTokensBeAdjacent(newRegExpValue, tokenAfter) && node.range[1] === tokenAfter.range[0] ? " " : "");
  320. }
  321. return {
  322. Program(node) {
  323. const scope = sourceCode.getScope(node);
  324. const tracker = new ReferenceTracker(scope);
  325. const traceMap = {
  326. RegExp: {
  327. [CALL]: true,
  328. [CONSTRUCT]: true
  329. }
  330. };
  331. for (const { node: refNode } of tracker.iterateGlobalReferences(traceMap)) {
  332. if (disallowRedundantWrapping && isUnnecessarilyWrappedRegexLiteral(refNode)) {
  333. const regexNode = refNode.arguments[0];
  334. if (refNode.arguments.length === 2) {
  335. const suggests = [];
  336. const argFlags = getStringValue(refNode.arguments[1]) || "";
  337. if (canFixTo(refNode, regexNode.regex.pattern, argFlags)) {
  338. suggests.push({
  339. messageId: "replaceWithLiteralAndFlags",
  340. pattern: regexNode.regex.pattern,
  341. flags: argFlags
  342. });
  343. }
  344. const literalFlags = regexNode.regex.flags || "";
  345. const mergedFlags = mergeRegexFlags(literalFlags, argFlags);
  346. if (
  347. !areFlagsEqual(mergedFlags, argFlags) &&
  348. canFixTo(refNode, regexNode.regex.pattern, mergedFlags)
  349. ) {
  350. suggests.push({
  351. messageId: "replaceWithIntendedLiteralAndFlags",
  352. pattern: regexNode.regex.pattern,
  353. flags: mergedFlags
  354. });
  355. }
  356. context.report({
  357. node: refNode,
  358. messageId: "unexpectedRedundantRegExpWithFlags",
  359. suggest: suggests.map(({ flags, pattern, messageId }) => ({
  360. messageId,
  361. data: {
  362. flags
  363. },
  364. fix(fixer) {
  365. return fixer.replaceText(refNode, getSafeOutput(refNode, `/${pattern}/${flags}`));
  366. }
  367. }))
  368. });
  369. } else {
  370. const outputs = [];
  371. if (canFixTo(refNode, regexNode.regex.pattern, regexNode.regex.flags)) {
  372. outputs.push(sourceCode.getText(regexNode));
  373. }
  374. context.report({
  375. node: refNode,
  376. messageId: "unexpectedRedundantRegExp",
  377. suggest: outputs.map(output => ({
  378. messageId: "replaceWithLiteral",
  379. fix(fixer) {
  380. return fixer.replaceText(
  381. refNode,
  382. getSafeOutput(refNode, output)
  383. );
  384. }
  385. }))
  386. });
  387. }
  388. } else if (hasOnlyStaticStringArguments(refNode)) {
  389. let regexContent = getStringValue(refNode.arguments[0]);
  390. let noFix = false;
  391. let flags;
  392. if (refNode.arguments[1]) {
  393. flags = getStringValue(refNode.arguments[1]);
  394. }
  395. if (!canFixTo(refNode, regexContent, flags)) {
  396. noFix = true;
  397. }
  398. if (!/^[-a-zA-Z0-9\\[\](){} \t\r\n\v\f!@#$%^&*+^_=/~`.><?,'"|:;]*$/u.test(regexContent)) {
  399. noFix = true;
  400. }
  401. if (regexContent && !noFix) {
  402. let charIncrease = 0;
  403. const ast = new RegExpParser({ ecmaVersion: regexppEcmaVersion }).parsePattern(regexContent, 0, regexContent.length, {
  404. unicode: flags ? flags.includes("u") : false,
  405. unicodeSets: flags ? flags.includes("v") : false
  406. });
  407. visitRegExpAST(ast, {
  408. onCharacterEnter(characterNode) {
  409. const escaped = resolveEscapes(characterNode.raw);
  410. if (escaped) {
  411. regexContent =
  412. regexContent.slice(0, characterNode.start + charIncrease) +
  413. escaped +
  414. regexContent.slice(characterNode.end + charIncrease);
  415. if (characterNode.raw.length === 1) {
  416. charIncrease += 1;
  417. }
  418. }
  419. }
  420. });
  421. }
  422. const newRegExpValue = `/${regexContent || "(?:)"}/${flags || ""}`;
  423. context.report({
  424. node: refNode,
  425. messageId: "unexpectedRegExp",
  426. suggest: noFix ? [] : [{
  427. messageId: "replaceWithLiteral",
  428. fix(fixer) {
  429. return fixer.replaceText(refNode, getSafeOutput(refNode, newRegExpValue));
  430. }
  431. }]
  432. });
  433. }
  434. }
  435. }
  436. };
  437. }
  438. };