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.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. replaceWithLiteralAndFlags: "Replace with an equivalent regular expression literal with flags '{{ flags }}'.",
  134. replaceWithIntendedLiteralAndFlags: "Replace with a regular expression literal with flags '{{ flags }}'.",
  135. unexpectedRedundantRegExp: "Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor.",
  136. unexpectedRedundantRegExpWithFlags: "Use regular expression literal with flags instead of the 'RegExp' constructor."
  137. }
  138. },
  139. create(context) {
  140. const [{ disallowRedundantWrapping = false } = {}] = context.options;
  141. const sourceCode = context.getSourceCode();
  142. /**
  143. * Determines whether the given identifier node is a reference to a global variable.
  144. * @param {ASTNode} node `Identifier` node to check.
  145. * @returns {boolean} True if the identifier is a reference to a global variable.
  146. */
  147. function isGlobalReference(node) {
  148. const scope = sourceCode.getScope(node);
  149. const variable = findVariable(scope, node);
  150. return variable !== null && variable.scope.type === "global" && variable.defs.length === 0;
  151. }
  152. /**
  153. * Determines whether the given node is a String.raw`` tagged template expression
  154. * with a static template literal.
  155. * @param {ASTNode} node Node to check.
  156. * @returns {boolean} True if the node is String.raw`` with a static template.
  157. */
  158. function isStringRawTaggedStaticTemplateLiteral(node) {
  159. return node.type === "TaggedTemplateExpression" &&
  160. astUtils.isSpecificMemberAccess(node.tag, "String", "raw") &&
  161. isGlobalReference(astUtils.skipChainExpression(node.tag).object) &&
  162. isStaticTemplateLiteral(node.quasi);
  163. }
  164. /**
  165. * Gets the value of a string
  166. * @param {ASTNode} node The node to get the string of.
  167. * @returns {string|null} The value of the node.
  168. */
  169. function getStringValue(node) {
  170. if (isStringLiteral(node)) {
  171. return node.value;
  172. }
  173. if (isStaticTemplateLiteral(node)) {
  174. return node.quasis[0].value.cooked;
  175. }
  176. if (isStringRawTaggedStaticTemplateLiteral(node)) {
  177. return node.quasi.quasis[0].value.raw;
  178. }
  179. return null;
  180. }
  181. /**
  182. * Determines whether the given node is considered to be a static string by the logic of this rule.
  183. * @param {ASTNode} node Node to check.
  184. * @returns {boolean} True if the node is a static string.
  185. */
  186. function isStaticString(node) {
  187. return isStringLiteral(node) ||
  188. isStaticTemplateLiteral(node) ||
  189. isStringRawTaggedStaticTemplateLiteral(node);
  190. }
  191. /**
  192. * Determines whether the relevant arguments of the given are all static string literals.
  193. * @param {ASTNode} node Node to check.
  194. * @returns {boolean} True if all arguments are static strings.
  195. */
  196. function hasOnlyStaticStringArguments(node) {
  197. const args = node.arguments;
  198. if ((args.length === 1 || args.length === 2) && args.every(isStaticString)) {
  199. return true;
  200. }
  201. return false;
  202. }
  203. /**
  204. * Determines whether the arguments of the given node indicate that a regex literal is unnecessarily wrapped.
  205. * @param {ASTNode} node Node to check.
  206. * @returns {boolean} True if the node already contains a regex literal argument.
  207. */
  208. function isUnnecessarilyWrappedRegexLiteral(node) {
  209. const args = node.arguments;
  210. if (args.length === 1 && isRegexLiteral(args[0])) {
  211. return true;
  212. }
  213. if (args.length === 2 && isRegexLiteral(args[0]) && isStaticString(args[1])) {
  214. return true;
  215. }
  216. return false;
  217. }
  218. /**
  219. * Returns a ecmaVersion compatible for regexpp.
  220. * @param {number} ecmaVersion The ecmaVersion to convert.
  221. * @returns {import("regexpp/ecma-versions").EcmaVersion} The resulting ecmaVersion compatible for regexpp.
  222. */
  223. function getRegexppEcmaVersion(ecmaVersion) {
  224. if (ecmaVersion <= 5) {
  225. return 5;
  226. }
  227. return Math.min(ecmaVersion, REGEXPP_LATEST_ECMA_VERSION);
  228. }
  229. const regexppEcmaVersion = getRegexppEcmaVersion(context.languageOptions.ecmaVersion);
  230. /**
  231. * Makes a character escaped or else returns null.
  232. * @param {string} character The character to escape.
  233. * @returns {string} The resulting escaped character.
  234. */
  235. function resolveEscapes(character) {
  236. switch (character) {
  237. case "\n":
  238. case "\\\n":
  239. return "\\n";
  240. case "\r":
  241. case "\\\r":
  242. return "\\r";
  243. case "\t":
  244. case "\\\t":
  245. return "\\t";
  246. case "\v":
  247. case "\\\v":
  248. return "\\v";
  249. case "\f":
  250. case "\\\f":
  251. return "\\f";
  252. case "/":
  253. return "\\/";
  254. default:
  255. return null;
  256. }
  257. }
  258. /**
  259. * Checks whether the given regex and flags are valid for the ecma version or not.
  260. * @param {string} pattern The regex pattern to check.
  261. * @param {string | undefined} flags The regex flags to check.
  262. * @returns {boolean} True if the given regex pattern and flags are valid for the ecma version.
  263. */
  264. function isValidRegexForEcmaVersion(pattern, flags) {
  265. const validator = new RegExpValidator({ ecmaVersion: regexppEcmaVersion });
  266. try {
  267. validator.validatePattern(pattern, 0, pattern.length, flags ? flags.includes("u") : false);
  268. if (flags) {
  269. validator.validateFlags(flags);
  270. }
  271. return true;
  272. } catch {
  273. return false;
  274. }
  275. }
  276. /**
  277. * Checks whether two given regex flags contain the same flags or not.
  278. * @param {string} flagsA The regex flags.
  279. * @param {string} flagsB The regex flags.
  280. * @returns {boolean} True if two regex flags contain same flags.
  281. */
  282. function areFlagsEqual(flagsA, flagsB) {
  283. return [...flagsA].sort().join("") === [...flagsB].sort().join("");
  284. }
  285. /**
  286. * Merges two regex flags.
  287. * @param {string} flagsA The regex flags.
  288. * @param {string} flagsB The regex flags.
  289. * @returns {string} The merged regex flags.
  290. */
  291. function mergeRegexFlags(flagsA, flagsB) {
  292. const flagsSet = new Set([
  293. ...flagsA,
  294. ...flagsB
  295. ]);
  296. return [...flagsSet].join("");
  297. }
  298. /**
  299. * Checks whether a give node can be fixed to the given regex pattern and flags.
  300. * @param {ASTNode} node The node to check.
  301. * @param {string} pattern The regex pattern to check.
  302. * @param {string} flags The regex flags
  303. * @returns {boolean} True if a node can be fixed to the given regex pattern and flags.
  304. */
  305. function canFixTo(node, pattern, flags) {
  306. const tokenBefore = sourceCode.getTokenBefore(node);
  307. return sourceCode.getCommentsInside(node).length === 0 &&
  308. (!tokenBefore || validPrecedingTokens.has(tokenBefore.value)) &&
  309. isValidRegexForEcmaVersion(pattern, flags);
  310. }
  311. /**
  312. * Returns a safe output code considering the before and after tokens.
  313. * @param {ASTNode} node The regex node.
  314. * @param {string} newRegExpValue The new regex expression value.
  315. * @returns {string} The output code.
  316. */
  317. function getSafeOutput(node, newRegExpValue) {
  318. const tokenBefore = sourceCode.getTokenBefore(node);
  319. const tokenAfter = sourceCode.getTokenAfter(node);
  320. return (tokenBefore && !canTokensBeAdjacent(tokenBefore, newRegExpValue) && tokenBefore.range[1] === node.range[0] ? " " : "") +
  321. newRegExpValue +
  322. (tokenAfter && !canTokensBeAdjacent(newRegExpValue, tokenAfter) && node.range[1] === tokenAfter.range[0] ? " " : "");
  323. }
  324. return {
  325. Program(node) {
  326. const scope = sourceCode.getScope(node);
  327. const tracker = new ReferenceTracker(scope);
  328. const traceMap = {
  329. RegExp: {
  330. [CALL]: true,
  331. [CONSTRUCT]: true
  332. }
  333. };
  334. for (const { node: refNode } of tracker.iterateGlobalReferences(traceMap)) {
  335. if (disallowRedundantWrapping && isUnnecessarilyWrappedRegexLiteral(refNode)) {
  336. const regexNode = refNode.arguments[0];
  337. if (refNode.arguments.length === 2) {
  338. const suggests = [];
  339. const argFlags = getStringValue(refNode.arguments[1]) || "";
  340. if (canFixTo(refNode, regexNode.regex.pattern, argFlags)) {
  341. suggests.push({
  342. messageId: "replaceWithLiteralAndFlags",
  343. pattern: regexNode.regex.pattern,
  344. flags: argFlags
  345. });
  346. }
  347. const literalFlags = regexNode.regex.flags || "";
  348. const mergedFlags = mergeRegexFlags(literalFlags, argFlags);
  349. if (
  350. !areFlagsEqual(mergedFlags, argFlags) &&
  351. canFixTo(refNode, regexNode.regex.pattern, mergedFlags)
  352. ) {
  353. suggests.push({
  354. messageId: "replaceWithIntendedLiteralAndFlags",
  355. pattern: regexNode.regex.pattern,
  356. flags: mergedFlags
  357. });
  358. }
  359. context.report({
  360. node: refNode,
  361. messageId: "unexpectedRedundantRegExpWithFlags",
  362. suggest: suggests.map(({ flags, pattern, messageId }) => ({
  363. messageId,
  364. data: {
  365. flags
  366. },
  367. fix(fixer) {
  368. return fixer.replaceText(refNode, getSafeOutput(refNode, `/${pattern}/${flags}`));
  369. }
  370. }))
  371. });
  372. } else {
  373. const outputs = [];
  374. if (canFixTo(refNode, regexNode.regex.pattern, regexNode.regex.flags)) {
  375. outputs.push(sourceCode.getText(regexNode));
  376. }
  377. context.report({
  378. node: refNode,
  379. messageId: "unexpectedRedundantRegExp",
  380. suggest: outputs.map(output => ({
  381. messageId: "replaceWithLiteral",
  382. fix(fixer) {
  383. return fixer.replaceText(
  384. refNode,
  385. getSafeOutput(refNode, output)
  386. );
  387. }
  388. }))
  389. });
  390. }
  391. } else if (hasOnlyStaticStringArguments(refNode)) {
  392. let regexContent = getStringValue(refNode.arguments[0]);
  393. let noFix = false;
  394. let flags;
  395. if (refNode.arguments[1]) {
  396. flags = getStringValue(refNode.arguments[1]);
  397. }
  398. if (!canFixTo(refNode, regexContent, flags)) {
  399. noFix = true;
  400. }
  401. if (!/^[-a-zA-Z0-9\\[\](){} \t\r\n\v\f!@#$%^&*+^_=/~`.><?,'"|:;]*$/u.test(regexContent)) {
  402. noFix = true;
  403. }
  404. if (regexContent && !noFix) {
  405. let charIncrease = 0;
  406. const ast = new RegExpParser({ ecmaVersion: regexppEcmaVersion }).parsePattern(regexContent, 0, regexContent.length, flags ? flags.includes("u") : false);
  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. };