diff --git a/README.md b/README.md
index 68de45aeb..051e65a9b 100644
--- a/README.md
+++ b/README.md
@@ -111,6 +111,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
| [regexp/no-empty-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-empty-group.html) | disallow empty group | :star: |
| [regexp/no-empty-lookarounds-assertion](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-empty-lookarounds-assertion.html) | disallow empty lookahead assertion or empty lookbehind assertion | :star: |
| [regexp/no-escape-backspace](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-escape-backspace.html) | disallow escape backspace (`[\b]`) | :star: |
+| [regexp/no-invalid-regexp](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-invalid-regexp.html) | disallow invalid regular expression strings in `RegExp` constructors | |
| [regexp/no-lazy-ends](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-lazy-ends.html) | disallow lazy quantifiers at the end of an expression | |
| [regexp/no-optional-assertion](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-optional-assertion.html) | disallow optional assertions | |
| [regexp/no-potentially-useless-backreference](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-potentially-useless-backreference.html) | disallow backreferences that reference a group that might not be matched | |
diff --git a/docs/rules/README.md b/docs/rules/README.md
index e5e840224..fe2790c40 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -20,6 +20,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
| [regexp/no-empty-group](./no-empty-group.md) | disallow empty group | :star: |
| [regexp/no-empty-lookarounds-assertion](./no-empty-lookarounds-assertion.md) | disallow empty lookahead assertion or empty lookbehind assertion | :star: |
| [regexp/no-escape-backspace](./no-escape-backspace.md) | disallow escape backspace (`[\b]`) | :star: |
+| [regexp/no-invalid-regexp](./no-invalid-regexp.md) | disallow invalid regular expression strings in `RegExp` constructors | |
| [regexp/no-lazy-ends](./no-lazy-ends.md) | disallow lazy quantifiers at the end of an expression | |
| [regexp/no-optional-assertion](./no-optional-assertion.md) | disallow optional assertions | |
| [regexp/no-potentially-useless-backreference](./no-potentially-useless-backreference.md) | disallow backreferences that reference a group that might not be matched | |
diff --git a/docs/rules/no-invalid-regexp.md b/docs/rules/no-invalid-regexp.md
new file mode 100644
index 000000000..a5b588bfb
--- /dev/null
+++ b/docs/rules/no-invalid-regexp.md
@@ -0,0 +1,64 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "regexp/no-invalid-regexp"
+description: "disallow invalid regular expression strings in `RegExp` constructors"
+---
+# regexp/no-invalid-regexp
+
+> disallow invalid regular expression strings in `RegExp` constructors
+
+- :exclamation: ***This rule has not been released yet.***
+
+## :book: Rule Details
+
+This rule reports invalid regular expression patterns given to `RegExp` constructors.
+
+
+
+```js
+/* eslint regexp/no-invalid-regexp: "error" */
+
+/* ✓ GOOD */
+RegExp('foo')
+RegExp('[a' + ']')
+
+/* ✗ BAD */
+RegExp('\\')
+RegExp('[a-Z]*')
+RegExp('\\p{Foo}', 'u')
+
+const space = '\\s*'
+RegExp('=' + space + '+(\\w+)', 'u')
+```
+
+
+
+### Differences to ESLint's `no-invalid-regexp` rule
+
+This rule is almost functionally equivalent to ESLint's [no-invalid-regexp] rule. The only difference is that this rule doesn't valid flags (see [no-non-standard-flag](./no-non-standard-flag.html)).
+
+There are two reasons we provide this rule:
+
+1. Better error reporting.
+
+ Instead of reporting the whole invalid string, this rule will try to report the exact position of the syntax error.
+
+2. Better support for complex constructor calls.
+
+ ESLint's rule only validates `RegExp` constructors called with simple string literals. This rule also supports operations (e.g. string concatenation) and variables to some degree.
+
+## :wrench: Options
+
+Nothing.
+
+## :books: Further reading
+
+- [no-invalid-regexp]
+
+[no-invalid-regexp]: https://eslint.org/docs/rules/no-invalid-regexp
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-invalid-regexp.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-invalid-regexp.ts)
diff --git a/lib/rules/no-invalid-regexp.ts b/lib/rules/no-invalid-regexp.ts
new file mode 100644
index 000000000..2820b0c8a
--- /dev/null
+++ b/lib/rules/no-invalid-regexp.ts
@@ -0,0 +1,64 @@
+import type { RegExpContextForInvalid } from "../utils"
+import { createRule, defineRegexpVisitor } from "../utils"
+
+/** Returns the position of the error */
+function getErrorIndex(error: SyntaxError): number | null {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- x
+ const index = (error as any).index
+ if (typeof index === "number") {
+ return index
+ }
+ return null
+}
+
+export default createRule("no-invalid-regexp", {
+ meta: {
+ docs: {
+ description:
+ "disallow invalid regular expression strings in `RegExp` constructors",
+ category: "Possible Errors",
+ // TODO Switch to recommended in the major version.
+ // TODO When setting `recommended: true`, do not forget to disable ESLint's no-invalid-regexp in recommended.ts
+ // recommended: true,
+ recommended: false,
+ },
+ schema: [],
+ messages: {
+ error: "{{message}}",
+ },
+ type: "problem",
+ },
+ create(context) {
+ /** Visit invalid regexes */
+ function visitInvalid(regexpContext: RegExpContextForInvalid): void {
+ const { node, error, patternSource } = regexpContext
+
+ let loc = undefined
+
+ const index = getErrorIndex(error)
+ if (
+ index !== null &&
+ index >= 0 &&
+ index <= patternSource.value.length
+ ) {
+ // The error index regexpp reports is a little weird.
+ // It's either spot on or one character to the right.
+ // Since we can't know which index is correct, we will report
+ // both positions.
+ loc = patternSource.getAstLocation({
+ start: Math.max(index - 1, 0),
+ end: Math.min(index + 1, patternSource.value.length),
+ })
+ }
+
+ context.report({
+ node,
+ loc: loc ?? undefined,
+ messageId: "error",
+ data: { message: error.message },
+ })
+ }
+
+ return defineRegexpVisitor(context, { visitInvalid })
+ },
+})
diff --git a/lib/utils/rules.ts b/lib/utils/rules.ts
index 3e5ed9b15..f391fcdff 100644
--- a/lib/utils/rules.ts
+++ b/lib/utils/rules.ts
@@ -13,6 +13,7 @@ import noEmptyCapturingGroup from "../rules/no-empty-capturing-group"
import noEmptyGroup from "../rules/no-empty-group"
import noEmptyLookaroundsAssertion from "../rules/no-empty-lookarounds-assertion"
import noEscapeBackspace from "../rules/no-escape-backspace"
+import noInvalidRegexp from "../rules/no-invalid-regexp"
import noInvisibleCharacter from "../rules/no-invisible-character"
import noLazyEnds from "../rules/no-lazy-ends"
import noLegacyFeatures from "../rules/no-legacy-features"
@@ -80,6 +81,7 @@ export const rules = [
noEmptyGroup,
noEmptyLookaroundsAssertion,
noEscapeBackspace,
+ noInvalidRegexp,
noInvisibleCharacter,
noLazyEnds,
noLegacyFeatures,
diff --git a/tests/lib/rules/no-invalid-regexp.ts b/tests/lib/rules/no-invalid-regexp.ts
new file mode 100644
index 000000000..7b545b368
--- /dev/null
+++ b/tests/lib/rules/no-invalid-regexp.ts
@@ -0,0 +1,48 @@
+import { RuleTester } from "eslint"
+import rule from "../../../lib/rules/no-invalid-regexp"
+
+const tester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: "module",
+ },
+})
+
+tester.run("no-invalid-regexp", rule as any, {
+ valid: [`/regexp/`, `RegExp("(" + ")")`],
+ invalid: [
+ {
+ code: `RegExp("(")`,
+ errors: [
+ {
+ message:
+ "Invalid regular expression: /(/: Unterminated group",
+ column: 9,
+ endColumn: 10,
+ },
+ ],
+ },
+ {
+ code: `RegExp("(" + "(")`,
+ errors: [
+ {
+ message:
+ "Invalid regular expression: /((/: Unterminated group",
+ column: 15,
+ endColumn: 16,
+ },
+ ],
+ },
+ {
+ code: `RegExp("[a-Z] some valid stuff")`,
+ errors: [
+ {
+ message:
+ "Invalid regular expression: /[a-Z] some valid stuff/: Range out of order in character class",
+ column: 12,
+ endColumn: 14,
+ },
+ ],
+ },
+ ],
+})