no-invalid-regexp.js 5.8 KB

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