no-invalid-regexp.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. /**
  2. * @fileoverview Validate strings passed to the RegExp constructor
  3. * @author Michael Ficarra
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const RegExpValidator = require("@eslint-community/regexpp").RegExpValidator;
  10. const validator = new RegExpValidator();
  11. const validFlags = "dgimsuvy";
  12. const undefined1 = void 0;
  13. //------------------------------------------------------------------------------
  14. // Rule Definition
  15. //------------------------------------------------------------------------------
  16. /** @type {import('../shared/types').Rule} */
  17. module.exports = {
  18. meta: {
  19. type: "problem",
  20. docs: {
  21. description: "Disallow invalid regular expression strings in `RegExp` constructors",
  22. recommended: true,
  23. url: "https://eslint.org/docs/latest/rules/no-invalid-regexp"
  24. },
  25. schema: [{
  26. type: "object",
  27. properties: {
  28. allowConstructorFlags: {
  29. type: "array",
  30. items: {
  31. type: "string"
  32. }
  33. }
  34. },
  35. additionalProperties: false
  36. }],
  37. messages: {
  38. regexMessage: "{{message}}."
  39. }
  40. },
  41. create(context) {
  42. const options = context.options[0];
  43. let allowedFlags = [];
  44. if (options && options.allowConstructorFlags) {
  45. const temp = options.allowConstructorFlags.join("").replace(new RegExp(`[${validFlags}]`, "gu"), "");
  46. if (temp) {
  47. allowedFlags = [...new Set(temp)];
  48. }
  49. }
  50. /**
  51. * Reports error with the provided message.
  52. * @param {ASTNode} node The node holding the invalid RegExp
  53. * @param {string} message The message to report.
  54. * @returns {void}
  55. */
  56. function report(node, message) {
  57. context.report({
  58. node,
  59. messageId: "regexMessage",
  60. data: { message }
  61. });
  62. }
  63. /**
  64. * Check if node is a string
  65. * @param {ASTNode} node node to evaluate
  66. * @returns {boolean} True if its a string
  67. * @private
  68. */
  69. function isString(node) {
  70. return node && node.type === "Literal" && typeof node.value === "string";
  71. }
  72. /**
  73. * Gets flags of a regular expression created by the given `RegExp()` or `new RegExp()` call
  74. * Examples:
  75. * new RegExp(".") // => ""
  76. * new RegExp(".", "gu") // => "gu"
  77. * new RegExp(".", flags) // => null
  78. * @param {ASTNode} node `CallExpression` or `NewExpression` node
  79. * @returns {string|null} flags if they can be determined, `null` otherwise
  80. * @private
  81. */
  82. function getFlags(node) {
  83. if (node.arguments.length < 2) {
  84. return "";
  85. }
  86. if (isString(node.arguments[1])) {
  87. return node.arguments[1].value;
  88. }
  89. return null;
  90. }
  91. /**
  92. * Check syntax error in a given pattern.
  93. * @param {string} pattern The RegExp pattern to validate.
  94. * @param {Object} flags The RegExp flags to validate.
  95. * @param {boolean} [flags.unicode] The Unicode flag.
  96. * @param {boolean} [flags.unicodeSets] The UnicodeSets flag.
  97. * @returns {string|null} The syntax error.
  98. */
  99. function validateRegExpPattern(pattern, flags) {
  100. try {
  101. validator.validatePattern(pattern, undefined1, undefined1, flags);
  102. return null;
  103. } catch (err) {
  104. return err.message;
  105. }
  106. }
  107. /**
  108. * Check syntax error in a given flags.
  109. * @param {string|null} flags The RegExp flags to validate.
  110. * @param {string|null} flagsToCheck The RegExp invalid flags.
  111. * @param {string} allFlags all valid and allowed flags.
  112. * @returns {string|null} The syntax error.
  113. */
  114. function validateRegExpFlags(flags, flagsToCheck, allFlags) {
  115. const duplicateFlags = [];
  116. if (typeof flagsToCheck === "string") {
  117. for (const flag of flagsToCheck) {
  118. if (allFlags.includes(flag)) {
  119. duplicateFlags.push(flag);
  120. }
  121. }
  122. }
  123. /*
  124. * `regexpp` checks the combination of `u` and `v` flags when parsing `Pattern` according to `ecma262`,
  125. * but this rule may check only the flag when the pattern is unidentifiable, so check it here.
  126. * https://tc39.es/ecma262/multipage/text-processing.html#sec-parsepattern
  127. */
  128. if (flags && flags.includes("u") && flags.includes("v")) {
  129. return "Regex 'u' and 'v' flags cannot be used together";
  130. }
  131. if (duplicateFlags.length > 0) {
  132. return `Duplicate flags ('${duplicateFlags.join("")}') supplied to RegExp constructor`;
  133. }
  134. if (!flagsToCheck) {
  135. return null;
  136. }
  137. return `Invalid flags supplied to RegExp constructor '${flagsToCheck}'`;
  138. }
  139. return {
  140. "CallExpression, NewExpression"(node) {
  141. if (node.callee.type !== "Identifier" || node.callee.name !== "RegExp") {
  142. return;
  143. }
  144. const flags = getFlags(node);
  145. let flagsToCheck = flags;
  146. const allFlags = allowedFlags.length > 0 ? validFlags.split("").concat(allowedFlags) : validFlags.split("");
  147. if (flags) {
  148. allFlags.forEach(flag => {
  149. flagsToCheck = flagsToCheck.replace(flag, "");
  150. });
  151. }
  152. let message = validateRegExpFlags(flags, flagsToCheck, allFlags);
  153. if (message) {
  154. report(node, message);
  155. return;
  156. }
  157. if (!isString(node.arguments[0])) {
  158. return;
  159. }
  160. const pattern = node.arguments[0].value;
  161. message = (
  162. // If flags are unknown, report the regex only if its pattern is invalid both with and without the "u" flag
  163. flags === null
  164. ? (
  165. validateRegExpPattern(pattern, { unicode: true, unicodeSets: false }) &&
  166. validateRegExpPattern(pattern, { unicode: false, unicodeSets: true }) &&
  167. validateRegExpPattern(pattern, { unicode: false, unicodeSets: false })
  168. )
  169. : validateRegExpPattern(pattern, { unicode: flags.includes("u"), unicodeSets: flags.includes("v") })
  170. );
  171. if (message) {
  172. report(node, message);
  173. }
  174. }
  175. };
  176. }
  177. };