prefer-regex-literals.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  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 {
  11. CALL,
  12. CONSTRUCT,
  13. ReferenceTracker,
  14. } = require("@eslint-community/eslint-utils");
  15. const {
  16. RegExpValidator,
  17. visitRegExpAST,
  18. RegExpParser,
  19. } = require("@eslint-community/regexpp");
  20. const { canTokensBeAdjacent } = require("./utils/ast-utils");
  21. const { REGEXPP_LATEST_ECMA_VERSION } = require("./utils/regular-expressions");
  22. //------------------------------------------------------------------------------
  23. // Helpers
  24. //------------------------------------------------------------------------------
  25. /**
  26. * Determines whether the given node is a string literal.
  27. * @param {ASTNode} node Node to check.
  28. * @returns {boolean} True if the node is a string literal.
  29. */
  30. function isStringLiteral(node) {
  31. return node.type === "Literal" && typeof node.value === "string";
  32. }
  33. /**
  34. * Determines whether the given node is a regex literal.
  35. * @param {ASTNode} node Node to check.
  36. * @returns {boolean} True if the node is a regex literal.
  37. */
  38. function isRegexLiteral(node) {
  39. return node.type === "Literal" && Object.hasOwn(node, "regex");
  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('../types').Rule.RuleModule} */
  109. module.exports = {
  110. meta: {
  111. type: "suggestion",
  112. defaultOptions: [
  113. {
  114. disallowRedundantWrapping: false,
  115. },
  116. ],
  117. docs: {
  118. description:
  119. "Disallow use of the `RegExp` constructor in favor of regular expression literals",
  120. recommended: false,
  121. url: "https://eslint.org/docs/latest/rules/prefer-regex-literals",
  122. },
  123. hasSuggestions: true,
  124. schema: [
  125. {
  126. type: "object",
  127. properties: {
  128. disallowRedundantWrapping: {
  129. type: "boolean",
  130. },
  131. },
  132. additionalProperties: false,
  133. },
  134. ],
  135. messages: {
  136. unexpectedRegExp:
  137. "Use a regular expression literal instead of the 'RegExp' constructor.",
  138. replaceWithLiteral:
  139. "Replace with an equivalent regular expression literal.",
  140. replaceWithLiteralAndFlags:
  141. "Replace with an equivalent regular expression literal with flags '{{ flags }}'.",
  142. replaceWithIntendedLiteralAndFlags:
  143. "Replace with a regular expression literal with flags '{{ flags }}'.",
  144. unexpectedRedundantRegExp:
  145. "Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor.",
  146. unexpectedRedundantRegExpWithFlags:
  147. "Use regular expression literal with flags instead of the 'RegExp' constructor.",
  148. },
  149. },
  150. create(context) {
  151. const [{ disallowRedundantWrapping }] = context.options;
  152. const sourceCode = context.sourceCode;
  153. /**
  154. * Determines whether the given node is a String.raw`` tagged template expression
  155. * with a static template literal.
  156. * @param {ASTNode} node Node to check.
  157. * @returns {boolean} True if the node is String.raw`` with a static template.
  158. */
  159. function isStringRawTaggedStaticTemplateLiteral(node) {
  160. return (
  161. node.type === "TaggedTemplateExpression" &&
  162. astUtils.isSpecificMemberAccess(node.tag, "String", "raw") &&
  163. sourceCode.isGlobalReference(
  164. astUtils.skipChainExpression(node.tag).object,
  165. ) &&
  166. astUtils.isStaticTemplateLiteral(node.quasi)
  167. );
  168. }
  169. /**
  170. * Gets the value of a string
  171. * @param {ASTNode} node The node to get the string of.
  172. * @returns {string|null} The value of the node.
  173. */
  174. function getStringValue(node) {
  175. if (isStringLiteral(node)) {
  176. return node.value;
  177. }
  178. if (astUtils.isStaticTemplateLiteral(node)) {
  179. return node.quasis[0].value.cooked;
  180. }
  181. if (isStringRawTaggedStaticTemplateLiteral(node)) {
  182. return node.quasi.quasis[0].value.raw;
  183. }
  184. return null;
  185. }
  186. /**
  187. * Determines whether the given node is considered to be a static string by the logic of this rule.
  188. * @param {ASTNode} node Node to check.
  189. * @returns {boolean} True if the node is a static string.
  190. */
  191. function isStaticString(node) {
  192. return (
  193. isStringLiteral(node) ||
  194. astUtils.isStaticTemplateLiteral(node) ||
  195. isStringRawTaggedStaticTemplateLiteral(node)
  196. );
  197. }
  198. /**
  199. * Determines whether the relevant arguments of the given are all static string literals.
  200. * @param {ASTNode} node Node to check.
  201. * @returns {boolean} True if all arguments are static strings.
  202. */
  203. function hasOnlyStaticStringArguments(node) {
  204. const args = node.arguments;
  205. if (
  206. (args.length === 1 || args.length === 2) &&
  207. args.every(isStaticString)
  208. ) {
  209. return true;
  210. }
  211. return false;
  212. }
  213. /**
  214. * Determines whether the arguments of the given node indicate that a regex literal is unnecessarily wrapped.
  215. * @param {ASTNode} node Node to check.
  216. * @returns {boolean} True if the node already contains a regex literal argument.
  217. */
  218. function isUnnecessarilyWrappedRegexLiteral(node) {
  219. const args = node.arguments;
  220. if (args.length === 1 && isRegexLiteral(args[0])) {
  221. return true;
  222. }
  223. if (
  224. args.length === 2 &&
  225. isRegexLiteral(args[0]) &&
  226. isStaticString(args[1])
  227. ) {
  228. return true;
  229. }
  230. return false;
  231. }
  232. /**
  233. * Returns a ecmaVersion compatible for regexpp.
  234. * @param {number} ecmaVersion The ecmaVersion to convert.
  235. * @returns {import("@eslint-community/regexpp/ecma-versions").EcmaVersion} The resulting ecmaVersion compatible for regexpp.
  236. */
  237. function getRegexppEcmaVersion(ecmaVersion) {
  238. if (ecmaVersion <= 5) {
  239. return 5;
  240. }
  241. return Math.min(ecmaVersion, REGEXPP_LATEST_ECMA_VERSION);
  242. }
  243. const regexppEcmaVersion = getRegexppEcmaVersion(
  244. context.languageOptions.ecmaVersion,
  245. );
  246. /**
  247. * Makes a character escaped or else returns null.
  248. * @param {string} character The character to escape.
  249. * @returns {string} The resulting escaped character.
  250. */
  251. function resolveEscapes(character) {
  252. switch (character) {
  253. case "\n":
  254. case "\\\n":
  255. return "\\n";
  256. case "\r":
  257. case "\\\r":
  258. return "\\r";
  259. case "\t":
  260. case "\\\t":
  261. return "\\t";
  262. case "\v":
  263. case "\\\v":
  264. return "\\v";
  265. case "\f":
  266. case "\\\f":
  267. return "\\f";
  268. case "/":
  269. return "\\/";
  270. default:
  271. return null;
  272. }
  273. }
  274. /**
  275. * Checks whether the given regex and flags are valid for the ecma version or not.
  276. * @param {string} pattern The regex pattern to check.
  277. * @param {string | undefined} flags The regex flags to check.
  278. * @returns {boolean} True if the given regex pattern and flags are valid for the ecma version.
  279. */
  280. function isValidRegexForEcmaVersion(pattern, flags) {
  281. const validator = new RegExpValidator({
  282. ecmaVersion: regexppEcmaVersion,
  283. });
  284. try {
  285. validator.validatePattern(pattern, 0, pattern.length, {
  286. unicode: flags ? flags.includes("u") : false,
  287. unicodeSets: flags ? flags.includes("v") : false,
  288. });
  289. if (flags) {
  290. validator.validateFlags(flags);
  291. }
  292. return true;
  293. } catch {
  294. return false;
  295. }
  296. }
  297. /**
  298. * Checks whether two given regex flags contain the same flags or not.
  299. * @param {string} flagsA The regex flags.
  300. * @param {string} flagsB The regex flags.
  301. * @returns {boolean} True if two regex flags contain same flags.
  302. */
  303. function areFlagsEqual(flagsA, flagsB) {
  304. return [...flagsA].sort().join("") === [...flagsB].sort().join("");
  305. }
  306. /**
  307. * Merges two regex flags.
  308. * @param {string} flagsA The regex flags.
  309. * @param {string} flagsB The regex flags.
  310. * @returns {string} The merged regex flags.
  311. */
  312. function mergeRegexFlags(flagsA, flagsB) {
  313. const flagsSet = new Set([...flagsA, ...flagsB]);
  314. return [...flagsSet].join("");
  315. }
  316. /**
  317. * Checks whether a give node can be fixed to the given regex pattern and flags.
  318. * @param {ASTNode} node The node to check.
  319. * @param {string} pattern The regex pattern to check.
  320. * @param {string} flags The regex flags
  321. * @returns {boolean} True if a node can be fixed to the given regex pattern and flags.
  322. */
  323. function canFixTo(node, pattern, flags) {
  324. const tokenBefore = sourceCode.getTokenBefore(node);
  325. return (
  326. sourceCode.getCommentsInside(node).length === 0 &&
  327. (!tokenBefore || validPrecedingTokens.has(tokenBefore.value)) &&
  328. isValidRegexForEcmaVersion(pattern, flags)
  329. );
  330. }
  331. /**
  332. * Returns a safe output code considering the before and after tokens.
  333. * @param {ASTNode} node The regex node.
  334. * @param {string} newRegExpValue The new regex expression value.
  335. * @returns {string} The output code.
  336. */
  337. function getSafeOutput(node, newRegExpValue) {
  338. const tokenBefore = sourceCode.getTokenBefore(node);
  339. const tokenAfter = sourceCode.getTokenAfter(node);
  340. return (
  341. (tokenBefore &&
  342. !canTokensBeAdjacent(tokenBefore, newRegExpValue) &&
  343. tokenBefore.range[1] === node.range[0]
  344. ? " "
  345. : "") +
  346. newRegExpValue +
  347. (tokenAfter &&
  348. !canTokensBeAdjacent(newRegExpValue, tokenAfter) &&
  349. node.range[1] === tokenAfter.range[0]
  350. ? " "
  351. : "")
  352. );
  353. }
  354. return {
  355. Program(node) {
  356. const scope = sourceCode.getScope(node);
  357. const tracker = new ReferenceTracker(scope);
  358. const traceMap = {
  359. RegExp: {
  360. [CALL]: true,
  361. [CONSTRUCT]: true,
  362. },
  363. };
  364. for (const { node: refNode } of tracker.iterateGlobalReferences(
  365. traceMap,
  366. )) {
  367. if (
  368. disallowRedundantWrapping &&
  369. isUnnecessarilyWrappedRegexLiteral(refNode)
  370. ) {
  371. const regexNode = refNode.arguments[0];
  372. if (refNode.arguments.length === 2) {
  373. const suggests = [];
  374. const argFlags =
  375. getStringValue(refNode.arguments[1]) || "";
  376. if (
  377. canFixTo(
  378. refNode,
  379. regexNode.regex.pattern,
  380. argFlags,
  381. )
  382. ) {
  383. suggests.push({
  384. messageId: "replaceWithLiteralAndFlags",
  385. pattern: regexNode.regex.pattern,
  386. flags: argFlags,
  387. });
  388. }
  389. const literalFlags = regexNode.regex.flags || "";
  390. const mergedFlags = mergeRegexFlags(
  391. literalFlags,
  392. argFlags,
  393. );
  394. if (
  395. !areFlagsEqual(mergedFlags, argFlags) &&
  396. canFixTo(
  397. refNode,
  398. regexNode.regex.pattern,
  399. mergedFlags,
  400. )
  401. ) {
  402. suggests.push({
  403. messageId:
  404. "replaceWithIntendedLiteralAndFlags",
  405. pattern: regexNode.regex.pattern,
  406. flags: mergedFlags,
  407. });
  408. }
  409. context.report({
  410. node: refNode,
  411. messageId: "unexpectedRedundantRegExpWithFlags",
  412. suggest: suggests.map(
  413. ({ flags, pattern, messageId }) => ({
  414. messageId,
  415. data: {
  416. flags,
  417. },
  418. fix(fixer) {
  419. return fixer.replaceText(
  420. refNode,
  421. getSafeOutput(
  422. refNode,
  423. `/${pattern}/${flags}`,
  424. ),
  425. );
  426. },
  427. }),
  428. ),
  429. });
  430. } else {
  431. const outputs = [];
  432. if (
  433. canFixTo(
  434. refNode,
  435. regexNode.regex.pattern,
  436. regexNode.regex.flags,
  437. )
  438. ) {
  439. outputs.push(sourceCode.getText(regexNode));
  440. }
  441. context.report({
  442. node: refNode,
  443. messageId: "unexpectedRedundantRegExp",
  444. suggest: outputs.map(output => ({
  445. messageId: "replaceWithLiteral",
  446. fix(fixer) {
  447. return fixer.replaceText(
  448. refNode,
  449. getSafeOutput(refNode, output),
  450. );
  451. },
  452. })),
  453. });
  454. }
  455. } else if (hasOnlyStaticStringArguments(refNode)) {
  456. let regexContent = getStringValue(refNode.arguments[0]);
  457. let noFix = false;
  458. let flags;
  459. if (refNode.arguments[1]) {
  460. flags = getStringValue(refNode.arguments[1]);
  461. }
  462. if (!canFixTo(refNode, regexContent, flags)) {
  463. noFix = true;
  464. }
  465. if (
  466. !/^[-\w\\[\](){} \t\r\n\v\f!@#$%^&*+=/~`.><?,'"|:;]*$/u.test(
  467. regexContent,
  468. )
  469. ) {
  470. noFix = true;
  471. }
  472. if (regexContent && !noFix) {
  473. let charIncrease = 0;
  474. const ast = new RegExpParser({
  475. ecmaVersion: regexppEcmaVersion,
  476. }).parsePattern(
  477. regexContent,
  478. 0,
  479. regexContent.length,
  480. {
  481. unicode: flags
  482. ? flags.includes("u")
  483. : false,
  484. unicodeSets: flags
  485. ? flags.includes("v")
  486. : false,
  487. },
  488. );
  489. visitRegExpAST(ast, {
  490. onCharacterEnter(characterNode) {
  491. const escaped = resolveEscapes(
  492. characterNode.raw,
  493. );
  494. if (escaped) {
  495. regexContent =
  496. regexContent.slice(
  497. 0,
  498. characterNode.start +
  499. charIncrease,
  500. ) +
  501. escaped +
  502. regexContent.slice(
  503. characterNode.end +
  504. charIncrease,
  505. );
  506. if (characterNode.raw.length === 1) {
  507. charIncrease += 1;
  508. }
  509. }
  510. },
  511. });
  512. }
  513. const newRegExpValue = `/${regexContent || "(?:)"}/${flags || ""}`;
  514. context.report({
  515. node: refNode,
  516. messageId: "unexpectedRegExp",
  517. suggest: noFix
  518. ? []
  519. : [
  520. {
  521. messageId: "replaceWithLiteral",
  522. fix(fixer) {
  523. return fixer.replaceText(
  524. refNode,
  525. getSafeOutput(
  526. refNode,
  527. newRegExpValue,
  528. ),
  529. );
  530. },
  531. },
  532. ],
  533. });
  534. }
  535. }
  536. },
  537. };
  538. },
  539. };