Repository: ota-meshi/eslint-plugin-regexp
Pull request #290 (2021-08-10 21:07:19)

Shorter link labels
The link labels were a little too long for my taste. Who wants to read the entire link anyways?


Patch:
diff --git a/README.md b/README.md
index 0c51ec9..68de45a 100644
--- a/README.md
+++ b/README.md
@@ -51,7 +51,7 @@ and either use one of the two configurations available (`recommended` or `all`)
 ### The recommended configuration
 
 The `plugin:regexp/recommended` config enables a subset of [the rules](#white_check_mark-rules) that should be most useful to most users.
-*See [https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/configs/recommended.ts](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/configs/recommended.ts) for more details.*
+*See [lib/configs/recommended.ts](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/configs/recommended.ts) for more details.*
 
 ```js
 // .eslintrc.js
@@ -87,7 +87,7 @@ module.exports = {
 ### Using `"plugin:regexp/all"`
 
 The `plugin:regexp/all` config enables all rules. It's meant for testing, not for production use because it changes with every minor and major version of the plugin. Use it at your own risk.
-*See [https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/configs/all.ts](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/configs/all.ts) for more details.*
+*See [lib/configs/all.ts](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/configs/all.ts) for more details.*
 
 <!--USAGE_SECTION_END-->
 
diff --git a/docs/user-guide/README.md b/docs/user-guide/README.md
index 50f8516..e780d0f 100644
--- a/docs/user-guide/README.md
+++ b/docs/user-guide/README.md
@@ -23,7 +23,7 @@ and either use one of the two configurations available (`recommended` or `all`)
 ### The recommended configuration
 
 The `plugin:regexp/recommended` config enables a subset of [the rules](../rules/README.md) that should be most useful to most users.
-*See [https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/configs/recommended.ts](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/configs/recommended.ts) for more details.*
+*See [lib/configs/recommended.ts](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/configs/recommended.ts) for more details.*
 
 ```js
 // .eslintrc.js
@@ -59,7 +59,7 @@ module.exports = {
 ### Using `"plugin:regexp/all"`
 
 The `plugin:regexp/all` config enables all rules. It's meant for testing, not for production use because it changes with every minor and major version of the plugin. Use it at your own risk.
-*See [https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/configs/all.ts](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/configs/all.ts) for more details.*
+*See [lib/configs/all.ts](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/configs/all.ts) for more details.*
 
 <!--USAGE_SECTION_END-->
 
diff --git a/lib/rules/control-character-escape.ts b/lib/rules/control-character-escape.ts
index 257c4c8..ebaf5b2 100644
--- a/lib/rules/control-character-escape.ts
+++ b/lib/rules/control-character-escape.ts
@@ -10,6 +10,7 @@ import {
     defineRegexpVisitor,
 } from "../utils"
 import { isRegexpLiteral } from "../utils/ast-utils/utils"
+import { mentionChar } from "../utils/mention"
 
 const CONTROL_CHARS = new Map<number, string>([
     [0, "\\0"],
@@ -33,7 +34,7 @@ export default createRule("control-character-escape", {
         schema: [],
         messages: {
             unexpected:
-                "Unexpected control character escape '{{actual}}' ({{cp}}). Use '{{expected}}' instead.",
+                "Unexpected control character escape {{actual}}. Use '{{expected}}' instead.",
         },
         type: "suggestion", // "problem",
     },
@@ -77,10 +78,7 @@ export default createRule("control-character-escape", {
                         loc: getRegexpLocation(cNode),
                         messageId: "unexpected",
                         data: {
-                            actual: cNode.raw,
-                            cp: `U+${cNode.value
-                                .toString(16)
-                                .padStart(4, "0")}`,
+                            actual: mentionChar(cNode),
                             expected: expectedRaw,
                         },
                         fix: fixReplaceNode(cNode, expectedRaw),
diff --git a/lib/rules/no-dupe-characters-character-class.ts b/lib/rules/no-dupe-characters-character-class.ts
index dcfb8c1..7ceffad 100644
--- a/lib/rules/no-dupe-characters-character-class.ts
+++ b/lib/rules/no-dupe-characters-character-class.ts
@@ -21,6 +21,7 @@ import { JS } from "refa"
 // eslint-disable-next-line no-restricted-imports -- there's no way around it
 import { toCharSet as uncachedToCharSet } from "regexp-ast-analysis"
 import type { Rule } from "eslint"
+import { mentionChar } from "../utils/mention"
 
 interface Grouping {
     duplicates: {
@@ -160,25 +161,6 @@ function fixRemove(
     })
 }
 
-/**
- * Creates a string that mentions the given element.
- */
-function mention(element: CharacterClassElement): string {
-    /** Creates a "U+FFFF" string */
-    function unicode(value: number): string {
-        return `U+${value.toString(16).padStart(4, "0")}`
-    }
-
-    if (element.type === "Character") {
-        return `'${element.raw}' (${unicode(element.value)})`
-    } else if (element.type === "CharacterClassRange") {
-        return `'${element.raw}' (${unicode(element.min.value)} - ${unicode(
-            element.max.value,
-        )})`
-    }
-    return `'${element.raw}'`
-}
-
 export default createRule("no-dupe-characters-character-class", {
     meta: {
         type: "suggestion",
@@ -218,7 +200,7 @@ export default createRule("no-dupe-characters-character-class", {
                     loc: getRegexpLocation(duplicate),
                     messageId: "duplicate",
                     data: {
-                        duplicate: mention(duplicate),
+                        duplicate: mentionChar(duplicate),
                     },
                     fix: fixRemove(regexpContext, duplicate),
                 })
@@ -228,8 +210,8 @@ export default createRule("no-dupe-characters-character-class", {
                     loc: getRegexpLocation(duplicate),
                     messageId: "duplicateNonObvious",
                     data: {
-                        duplicate: mention(duplicate),
-                        element: mention(element),
+                        duplicate: mentionChar(duplicate),
+                        element: mentionChar(element),
                     },
                     fix: fixRemove(regexpContext, duplicate),
                 })
@@ -250,8 +232,8 @@ export default createRule("no-dupe-characters-character-class", {
                 loc: getRegexpLocation(element),
                 messageId: "overlap",
                 data: {
-                    elementA: mention(element),
-                    elementB: mention(intersectElement),
+                    elementA: mentionChar(element),
+                    elementB: mentionChar(intersectElement),
                     overlap,
                 },
             })
@@ -274,8 +256,8 @@ export default createRule("no-dupe-characters-character-class", {
                 loc: getRegexpLocation(subsetElement),
                 messageId: "subset",
                 data: {
-                    subsetElement: mention(subsetElement),
-                    element: mention(element),
+                    subsetElement: mentionChar(subsetElement),
+                    element: mentionChar(element),
                 },
                 fix: fixRemove(regexpContext, subsetElement),
             })
@@ -296,10 +278,10 @@ export default createRule("no-dupe-characters-character-class", {
                 loc: getRegexpLocation(subsetElement),
                 messageId: "subsetOfMany",
                 data: {
-                    subsetElement: mention(subsetElement),
+                    subsetElement: mentionChar(subsetElement),
                     elements: `'${elements
                         .map((e) => e.raw)
-                        .join("")}' (${elements.map(mention).join(", ")})`,
+                        .join("")}' (${elements.map(mentionChar).join(", ")})`,
                 },
                 fix: fixRemove(regexpContext, subsetElement),
             })
diff --git a/lib/rules/no-non-standard-flag.ts b/lib/rules/no-non-standard-flag.ts
index b3d662f..4b87958 100644
--- a/lib/rules/no-non-standard-flag.ts
+++ b/lib/rules/no-non-standard-flag.ts
@@ -1,5 +1,4 @@
-import type { RegExpVisitor } from "regexpp/visitor"
-import type { RegExpContext } from "../utils"
+import type { RegExpContext, UnparsableRegExpContext } from "../utils"
 import { createRule, defineRegexpVisitor } from "../utils"
 
 const STANDARD_FLAGS = "dgimsuy"
@@ -20,14 +19,12 @@ export default createRule("no-non-standard-flag", {
         type: "suggestion", // "problem",
     },
     create(context) {
-        /**
-         * Create visitor
-         */
-        function createVisitor({
-            node,
+        /** The logic of this rule */
+        function visit({
+            regexpNode,
             getFlagsLocation,
             flagsString,
-        }: RegExpContext): RegExpVisitor.Handlers {
+        }: RegExpContext | UnparsableRegExpContext) {
             if (flagsString) {
                 const nonStandard = [...flagsString].filter(
                     (f) => !STANDARD_FLAGS.includes(f),
@@ -35,18 +32,22 @@ export default createRule("no-non-standard-flag", {
 
                 if (nonStandard.length > 0) {
                     context.report({
-                        node,
+                        node: regexpNode,
                         loc: getFlagsLocation(),
                         messageId: "unexpected",
                         data: { flag: nonStandard[0] },
                     })
                 }
             }
-            return {}
         }
 
         return defineRegexpVisitor(context, {
-            createVisitor,
+            createVisitor(regexpContext) {
+                visit(regexpContext)
+                return {}
+            },
+            visitInvalid: visit,
+            visitUnknown: visit,
         })
     },
 })
diff --git a/lib/rules/no-obscure-range.ts b/lib/rules/no-obscure-range.ts
index 3eacd17..5385b03 100644
--- a/lib/rules/no-obscure-range.ts
+++ b/lib/rules/no-obscure-range.ts
@@ -13,6 +13,7 @@ import {
     isUseHexEscape,
     isOctalEscape,
 } from "../utils"
+import { mentionChar } from "../utils/mention"
 
 export default createRule("no-obscure-range", {
     meta: {
@@ -34,7 +35,7 @@ export default createRule("no-obscure-range", {
         ],
         messages: {
             unexpected:
-                "Unexpected obscure character range. The characters of '{{range}}' ({{unicode}}) are not obvious.",
+                "Unexpected obscure character range. The characters of {{range}} are not obvious.",
         },
         type: "suggestion", // "problem",
     },
@@ -84,16 +85,12 @@ export default createRule("no-obscure-range", {
                         return
                     }
 
-                    const uMin = `U+${min.value.toString(16).padStart(4, "0")}`
-                    const uMax = `U+${max.value.toString(16).padStart(4, "0")}`
-
                     context.report({
                         node,
                         loc: getRegexpLocation(rNode),
                         messageId: "unexpected",
                         data: {
-                            range: rNode.raw,
-                            unicode: `${uMin} - ${uMax}`,
+                            range: mentionChar(rNode),
                         },
                     })
                 },
diff --git a/lib/rules/sort-character-class-elements.ts b/lib/rules/sort-character-class-elements.ts
index f03a642..565e3be 100644
--- a/lib/rules/sort-character-class-elements.ts
+++ b/lib/rules/sort-character-class-elements.ts
@@ -11,6 +11,7 @@ import {
     createRule,
     defineRegexpVisitor,
 } from "../utils"
+import { mention } from "../utils/mention"
 
 type CharacterClassElementKind = "\\w" | "\\d" | "\\s" | "\\p" | "*"
 const DEFAULT_ORDER: CharacterClassElementKind[] = [
@@ -61,7 +62,7 @@ export default createRule("sort-character-class-elements", {
         ],
         messages: {
             sortElements:
-                "Expected character class elements to be in ascending order. '{{next}}' should be before '{{prev}}'.",
+                "Expected character class elements to be in ascending order. {{next}} should be before {{prev}}.",
         },
         type: "layout",
     },
@@ -107,8 +108,8 @@ export default createRule("sort-character-class-elements", {
                                     loc: getRegexpLocation(next),
                                     messageId: "sortElements",
                                     data: {
-                                        next: next.raw,
-                                        prev: moveTarget.raw,
+                                        next: mention(next),
+                                        prev: mention(moveTarget),
                                     },
                                     *fix(fixer) {
                                         const nextRange = patternSource.getReplaceRange(
diff --git a/lib/rules/sort-flags.ts b/lib/rules/sort-flags.ts
index 645c8ad..20f5969 100644
--- a/lib/rules/sort-flags.ts
+++ b/lib/rules/sort-flags.ts
@@ -1,5 +1,4 @@
-import type { RegExpVisitor } from "regexpp/visitor"
-import type { RegExpContext } from "../utils"
+import type { RegExpContext, UnparsableRegExpContext } from "../utils"
 import { createRule, defineRegexpVisitor } from "../utils"
 
 export default createRule("sort-flags", {
@@ -15,7 +14,7 @@ export default createRule("sort-flags", {
         schema: [],
         messages: {
             sortFlags:
-                "The flags '{{flags}}' should in the order '{{sortedFlags}}'.",
+                "The flags '{{flags}}' should be in the order '{{sortedFlags}}'.",
         },
         type: "suggestion", // "problem",
     },
@@ -29,16 +28,14 @@ export default createRule("sort-flags", {
                 .join("")
         }
 
-        /**
-         * Create visitor
-         */
-        function createVisitor({
+        /** The logic of this rule */
+        function visit({
             regexpNode,
             flagsString,
             ownsFlags,
             getFlagsLocation,
             fixReplaceFlags,
-        }: RegExpContext): RegExpVisitor.Handlers {
+        }: RegExpContext | UnparsableRegExpContext) {
             if (flagsString && ownsFlags) {
                 const sortedFlags = sortFlags(flagsString)
                 if (flagsString !== sortedFlags) {
@@ -51,12 +48,15 @@ export default createRule("sort-flags", {
                     })
                 }
             }
-
-            return {} // not visit RegExpNodes
         }
 
         return defineRegexpVisitor(context, {
-            createVisitor,
+            createVisitor(regexpContext) {
+                visit(regexpContext)
+                return {}
+            },
+            visitInvalid: visit,
+            visitUnknown: visit,
         })
     },
 })
diff --git a/lib/utils/ast-utils/pattern-source.ts b/lib/utils/ast-utils/pattern-source.ts
index a0d7820..bb6aeb6 100644
--- a/lib/utils/ast-utils/pattern-source.ts
+++ b/lib/utils/ast-utils/pattern-source.ts
@@ -24,7 +24,7 @@ export interface PatternRange {
 export class PatternReplaceRange {
     public range: AST.Range
 
-    public type: "RegExp" | "String"
+    public type: "RegExp" | "SingleQuotedString" | "DoubleQuotedString"
 
     public constructor(range: AST.Range, type: PatternReplaceRange["type"]) {
         if (!range || range[0] < 0 || range[0] > range[1]) {
@@ -60,7 +60,11 @@ export class PatternReplaceRange {
             const astRange = getStringValueRange(sourceCode, node, start, end)
 
             if (astRange) {
-                return new PatternReplaceRange(astRange, "String")
+                const quote = sourceCode.text[node.range[0]]
+                return new PatternReplaceRange(
+                    astRange,
+                    quote === "'" ? "SingleQuotedString" : "DoubleQuotedString",
+                )
             }
         }
         return null
@@ -71,15 +75,23 @@ export class PatternReplaceRange {
     }
 
     public escape(text: string): string {
-        if (this.type === "String") {
-            return text
+        if (
+            this.type === "DoubleQuotedString" ||
+            this.type === "SingleQuotedString"
+        ) {
+            const base = text
                 .replace(/\\/g, "\\\\")
                 .replace(/\n/g, "\\n")
                 .replace(/\r/g, "\\r")
                 .replace(/\t/g, "\\t")
+
+            if (this.type === "DoubleQuotedString") {
+                return base.replace(/"/g, '\\"')
+            }
+            return base.replace(/'/g, "\\'")
         }
 
-        return text
+        return text.replace(/\n/g, "\\n").replace(/\r/g, "\\r")
     }
 
     public replace(fixer: Rule.RuleFixer, text: string): Rule.Fix {
diff --git a/lib/utils/index.ts b/lib/utils/index.ts
index 7f7369a..852c870 100644
--- a/lib/utils/index.ts
+++ b/lib/utils/index.ts
@@ -22,7 +22,8 @@ export type ToCharSet = (
     node: ToCharSetElement,
     flags?: ReadonlyFlags,
 ) => CharSet
-type RegExpHelpersBase = {
+
+type RegExpContextBase = {
     toCharSet: ToCharSet
 
     /**
@@ -83,22 +84,53 @@ type RegExpHelpersBase = {
 
     flags: ReadonlyFlags
 }
-
 export type RegExpContextForLiteral = {
     node: ESTree.RegExpLiteral
     flagsString: string
     ownsFlags: true
     regexpNode: ESTree.RegExpLiteral
-} & RegExpHelpersBase
+} & RegExpContextBase
 export type RegExpContextForSource = {
     node: ESTree.Expression
     flagsString: string | null
     ownsFlags: boolean
-    regexpNode: ESTree.NewExpression | ESTree.CallExpression
-} & RegExpHelpersBase
+    regexpNode: ESTree.CallExpression
+} & RegExpContextBase
 export type RegExpContext = RegExpContextForLiteral | RegExpContextForSource
 
-type RegexpRule = {
+type UnparsableRegExpContextBase = {
+    node: ESTree.Expression
+    regexpNode: ESTree.RegExpLiteral | ESTree.CallExpression
+
+    flags: ReadonlyFlags
+    flagsString: string | null
+    ownsFlags: boolean
+
+    /**
+     * Creates SourceLocation from the regexp flags
+     *
+     * @returns The SourceLocation
+     */
+    getFlagsLocation: () => AST.SourceLocation
+
+    fixReplaceFlags: (
+        newFlags: string | (() => string | null),
+    ) => (fixer: Rule.RuleFixer) => Rule.Fix[] | Rule.Fix | null
+}
+export type RegExpContextForInvalid = {
+    pattern: string
+    patternSource: PatternSource
+    error: SyntaxError
+} & UnparsableRegExpContextBase
+export type RegExpContextForUnknown = {
+    pattern: null
+    patternSource: null
+} & UnparsableRegExpContextBase
+export type UnparsableRegExpContext =
+    | RegExpContextForInvalid
+    | RegExpContextForUnknown
+
+type ParsableRegexpRule = {
     createLiteralVisitor?: (
         context: RegExpContextForLiteral,
     ) => RegExpVisitor.Handlers
@@ -106,6 +138,12 @@ type RegexpRule = {
         context: RegExpContextForSource,
     ) => RegExpVisitor.Handlers
 }
+type UnparsableRegexpRule = {
+    visitInvalid?: (context: RegExpContextForInvalid) => void
+    visitUnknown?: (context: RegExpContextForUnknown) => void
+}
+type RegexpRule = ParsableRegexpRule & UnparsableRegexpRule
+
 const regexpRules = new WeakMap<ESTree.Program, RegexpRule[]>()
 
 export const FLAG_GLOBAL = "g"
@@ -161,16 +199,20 @@ export function createRule(
     }
 }
 
+type DefineRegexpVisitorRule = UnparsableRegexpRule &
+    (
+        | ParsableRegexpRule
+        | {
+              createVisitor: (context: RegExpContext) => RegExpVisitor.Handlers
+          }
+    )
+
 /**
  * Define the RegExp visitor rule.
  */
 export function defineRegexpVisitor(
     context: Rule.RuleContext,
-    rule:
-        | RegexpRule
-        | {
-              createVisitor?: (context: RegExpContext) => RegExpVisitor.Handlers
-          },
+    rule: DefineRegexpVisitorRule,
 ): RuleListener {
     const programNode = context.getSourceCode().ast
 
@@ -186,20 +228,22 @@ export function defineRegexpVisitor(
         visitor = {}
     }
 
-    const createLiteralVisitor =
-        "createVisitor" in rule
-            ? rule.createVisitor
-            : "createLiteralVisitor" in rule
-            ? rule.createLiteralVisitor
-            : undefined
-    const createSourceVisitor =
-        "createVisitor" in rule
-            ? rule.createVisitor
-            : "createSourceVisitor" in rule
-            ? rule.createSourceVisitor
-            : undefined
+    let createLiteralVisitor = undefined
+    let createSourceVisitor = undefined
+    if ("createVisitor" in rule) {
+        createLiteralVisitor = rule.createVisitor
+        createSourceVisitor = rule.createVisitor
+    } else {
+        createLiteralVisitor = rule.createLiteralVisitor
+        createSourceVisitor = rule.createSourceVisitor
+    }
 
-    rules.push({ createLiteralVisitor, createSourceVisitor })
+    rules.push({
+        createLiteralVisitor,
+        createSourceVisitor,
+        visitInvalid: rule.visitInvalid,
+        visitUnknown: rule.visitUnknown,
+    })
 
     return visitor
 }
@@ -214,20 +258,38 @@ function buildRegexpVisitor(
 
     /**
      * Verify a given regular expression.
-     * @param pattern The regular expression pattern to verify.
+     * @param patternNode The regular expression pattern to verify.
      * @param flags The flags of the regular expression.
      */
     function verify(
-        exprNode: ESTree.Expression,
+        patternNode: ESTree.Expression,
         regexpNode: ESTree.RegExpLiteral | ESTree.CallExpression,
-        patternSource: PatternSource,
-        flags: ReadonlyFlags,
-        createVisitors: (
-            helpers: RegExpHelpersBase,
-        ) => IterableIterator<RegExpVisitor.Handlers>,
+        patternSource: PatternSource | null,
+        flagsString: string | null,
+        ownsFlags: boolean,
+        createVisitor: (helpers: RegExpContextBase) => RegExpVisitor.Handlers,
     ) {
-        let parsedPattern
+        const flags = parseFlags(flagsString || "")
 
+        if (!patternSource) {
+            visitUnknownForRules(rules, {
+                pattern: null,
+                patternSource: null,
+
+                ...buildUnparsableRegExpContextBase({
+                    patternSource,
+                    patternNode,
+                    regexpNode,
+                    context,
+                    flags,
+                    flagsString,
+                    ownsFlags,
+                }),
+            })
+            return
+        }
+
+        let parsedPattern
         try {
             parsedPattern = parser.parsePattern(
                 patternSource.value,
@@ -235,46 +297,37 @@ function buildRegexpVisitor(
                 patternSource.value.length,
                 flags.unicode,
             )
-        } catch {
-            // Ignore regular expressions with syntax errors
+        } catch (error: unknown) {
+            if (error instanceof SyntaxError) {
+                // regex with syntax error
+                visitInvalidForRules(rules, {
+                    pattern: patternSource.value,
+                    patternSource,
+                    error,
+
+                    ...buildUnparsableRegExpContextBase({
+                        patternSource,
+                        patternNode,
+                        regexpNode,
+                        context,
+                        flags,
+                        flagsString,
+                        ownsFlags,
+                    }),
+                })
+            }
             return
         }
 
-        const helpers = buildRegExpHelperBase({
+        const helpers = buildRegExpContextBase({
             patternSource,
-            exprNode,
             regexpNode,
             context,
             flags,
             parsedPattern,
         })
 
-        const handler: RegExpVisitor.Handlers = {}
-        for (const visitor of createVisitors(helpers)) {
-            for (const [key, fn] of Object.entries(visitor) as [
-                keyof RegExpVisitor.Handlers,
-                RegExpVisitor.Handlers[keyof RegExpVisitor.Handlers],
-            ][]) {
-                const orig = handler[key]
-                if (orig) {
-                    handler[key] = (
-                        // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore
-                        node: any,
-                    ) => {
-                        orig(node)
-                        fn!(node)
-                    }
-                } else {
-                    // @ts-expect-error -- ignore
-                    handler[key] = fn
-                }
-            }
-        }
-        if (Object.keys(handler).length === 0) {
-            return
-        }
-
-        visitRegExpAST(parsedPattern, handler)
+        visitRegExpAST(parsedPattern, createVisitor(helpers))
     }
 
     return {
@@ -284,21 +337,15 @@ function buildRegexpVisitor(
                 return
             }
             const flagsString = node.regex.flags
-            const flags = parseFlags(flagsString)
             const patternSource = PatternSource.fromRegExpLiteral(context, node)
-            verify(node, node, patternSource, flags, function* (helpers) {
-                const regexpContext: RegExpContextForLiteral = {
+            verify(node, node, patternSource, flagsString, true, (base) => {
+                return createLiteralVisitorFromRules(rules, {
                     node,
                     flagsString,
                     ownsFlags: true,
                     regexpNode: node,
-                    ...helpers,
-                }
-                for (const rule of rules) {
-                    if (rule.createLiteralVisitor) {
-                        yield rule.createLiteralVisitor(regexpContext)
-                    }
-                }
+                    ...base,
+                })
             })
         },
         Program() {
@@ -308,18 +355,16 @@ function buildRegexpVisitor(
             // E.g., `new RegExp()`, `RegExp()`, `new window.RegExp()`,
             //       `const {RegExp: a} = window; new a()`, etc...
             const regexpDataList: {
-                newOrCall: ESTree.NewExpression | ESTree.CallExpression
+                call: ESTree.CallExpression
                 patternNode: ESTree.Expression
-                patternSource: PatternSource
+                patternSource: PatternSource | null
                 flagsString: string | null
                 ownsFlags: boolean
             }[] = []
             for (const { node } of tracker.iterateGlobalReferences({
                 RegExp: { [CALL]: true, [CONSTRUCT]: true },
             })) {
-                const newOrCall = node as
-                    | ESTree.NewExpression
-                    | ESTree.CallExpression
+                const newOrCall = node as ESTree.CallExpression
                 const [patternNode, flagsNode] = newOrCall.arguments
                 if (!patternNode || patternNode.type === "SpreadElement") {
                     continue
@@ -329,54 +374,47 @@ function buildRegexpVisitor(
                     context,
                     patternNode,
                 )
-                if (!patternSource) {
-                    continue
-                }
 
                 let flagsString = null
                 let ownsFlags = false
                 if (flagsNode && flagsNode.type !== "SpreadElement") {
                     flagsString = getStringIfConstant(context, flagsNode)
                     ownsFlags = isStringLiteral(flagsNode)
-                } else if (patternSource.regexpValue) {
+                } else if (patternSource && patternSource.regexpValue) {
                     flagsString = patternSource.regexpValue.flags
                     ownsFlags = Boolean(patternSource.regexpValue.ownedNode)
                 }
 
                 regexpDataList.push({
-                    newOrCall,
+                    call: newOrCall,
                     patternNode,
                     patternSource,
                     flagsString,
                     ownsFlags,
                 })
             }
+
             for (const {
-                newOrCall,
+                call,
                 patternNode,
                 patternSource,
                 flagsString,
                 ownsFlags,
             } of regexpDataList) {
-                const flags = parseFlags(flagsString || "")
                 verify(
                     patternNode,
-                    newOrCall,
+                    call,
                     patternSource,
-                    flags,
-                    function* (helpers) {
-                        const regexpContext: RegExpContextForSource = {
+                    flagsString,
+                    ownsFlags,
+                    (base) => {
+                        return createSourceVisitorFromRules(rules, {
                             node: patternNode,
                             flagsString,
                             ownsFlags,
-                            regexpNode: newOrCall,
-                            ...helpers,
-                        }
-                        for (const rule of rules) {
-                            if (rule.createSourceVisitor) {
-                                yield rule.createSourceVisitor(regexpContext)
-                            }
-                        }
+                            regexpNode: call,
+                            ...base,
+                        })
                     },
                 )
             }
@@ -384,6 +422,86 @@ function buildRegexpVisitor(
     }
 }
 
+/** Create a visitor handler from the given rules */
+function createLiteralVisitorFromRules(
+    rules: Iterable<RegexpRule>,
+    context: RegExpContextForLiteral,
+): RegExpVisitor.Handlers {
+    const handlers: RegExpVisitor.Handlers[] = []
+    for (const rule of rules) {
+        if (rule.createLiteralVisitor) {
+            handlers.push(rule.createLiteralVisitor(context))
+        }
+    }
+    return composeRegExpVisitors(handlers)
+}
+
+/** Create a visitor handler from the given rules */
+function createSourceVisitorFromRules(
+    rules: Iterable<RegexpRule>,
+    context: RegExpContextForSource,
+): RegExpVisitor.Handlers {
+    const handlers: RegExpVisitor.Handlers[] = []
+    for (const rule of rules) {
+        if (rule.createSourceVisitor) {
+            handlers.push(rule.createSourceVisitor(context))
+        }
+    }
+    return composeRegExpVisitors(handlers)
+}
+
+/** Calls a visit function for all the given rules */
+function visitInvalidForRules(
+    rules: Iterable<RegexpRule>,
+    context: RegExpContextForInvalid,
+): void {
+    for (const rule of rules) {
+        rule.visitInvalid?.(context)
+    }
+}
+
+/** Calls a visit function for all the given rules */
+function visitUnknownForRules(
+    rules: Iterable<RegexpRule>,
+    context: RegExpContextForUnknown,
+): void {
+    for (const rule of rules) {
+        rule.visitUnknown?.(context)
+    }
+}
+
+/** Returns a single visitor handler that executes all the given handlers. */
+function composeRegExpVisitors(
+    handlers: Iterable<RegExpVisitor.Handlers>,
+): RegExpVisitor.Handlers {
+    const handler: RegExpVisitor.Handlers = {}
+
+    for (const visitor of handlers) {
+        const entries = Object.entries(visitor) as [
+            keyof RegExpVisitor.Handlers,
+            RegExpVisitor.Handlers[keyof RegExpVisitor.Handlers],
+        ][]
+
+        for (const [key, fn] of entries) {
+            const orig = handler[key]
+            if (orig) {
+                handler[key] = (
+                    // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore
+                    node: any,
+                ) => {
+                    orig(node)
+                    fn!(node)
+                }
+            } else {
+                // @ts-expect-error -- ignore
+                handler[key] = fn
+            }
+        }
+    }
+
+    return handler
+}
+
 /**
  * Composite all given visitors.
  */
@@ -410,23 +528,21 @@ export function compositingVisitors(
 }
 
 /**
- * Build RegExpHelperBase
+ * Build RegExpContextBase
  */
-function buildRegExpHelperBase({
+function buildRegExpContextBase({
     patternSource,
-    exprNode,
     regexpNode,
     context,
     flags,
     parsedPattern,
 }: {
     patternSource: PatternSource
-    exprNode: ESTree.Expression
     regexpNode: ESTree.CallExpression | ESTree.RegExpLiteral
     context: Rule.RuleContext
     flags: ReadonlyFlags
     parsedPattern: Pattern
-}): RegExpHelpersBase {
+}): RegExpContextBase {
     const sourceCode = context.getSourceCode()
 
     const cacheCharSet = new WeakMap<ToCharSetElement, CharSet>()
@@ -447,9 +563,6 @@ function buildRegExpHelperBase({
             return charSet
         },
         getRegexpLocation: (range, offsets) => {
-            if (!patternSource) {
-                return exprNode.loc!
-            }
             if (offsets) {
                 return patternSource.getAstLocation({
                     start: range.start + offsets[0],
@@ -460,21 +573,12 @@ function buildRegExpHelperBase({
         },
         getFlagsLocation: () => getFlagsLocation(sourceCode, regexpNode),
         fixReplaceNode: (node, replacement) => {
-            if (!patternSource) {
-                return () => null
-            }
             return fixReplaceNode(patternSource, node, replacement)
         },
         fixReplaceQuant: (qNode, replacement) => {
-            if (!patternSource) {
-                return () => null
-            }
             return fixReplaceQuant(patternSource, qNode, replacement)
         },
         fixReplaceFlags: (newFlags) => {
-            if (!patternSource) {
-                return () => null
-            }
             return fixReplaceFlags(patternSource, regexpNode, newFlags)
         },
         getUsageOfPattern: () =>
@@ -487,6 +591,47 @@ function buildRegExpHelperBase({
     }
 }
 
+/**
+ * Build UnparsableRegExpContextBase
+ */
+function buildUnparsableRegExpContextBase({
+    patternSource,
+    patternNode,
+    regexpNode,
+    context,
+    flags,
+    flagsString,
+    ownsFlags,
+}: {
+    patternSource: PatternSource | null
+    patternNode: ESTree.Expression
+    regexpNode: ESTree.CallExpression | ESTree.RegExpLiteral
+    context: Rule.RuleContext
+    flags: ReadonlyFlags
+    flagsString: string | null
+    ownsFlags: boolean
+}): UnparsableRegExpContextBase {
+    const sourceCode = context.getSourceCode()
+
+    return {
+        regexpNode,
+        node: patternNode,
+
+        flags,
+        flagsString,
+        ownsFlags,
+
+        getFlagsLocation: () => getFlagsLocation(sourceCode, regexpNode),
+
+        fixReplaceFlags: (newFlags) => {
+            if (!patternSource) {
+                return () => null
+            }
+            return fixReplaceFlags(patternSource, regexpNode, newFlags)
+        },
+    }
+}
+
 function getFlagsRange(regexpNode: ESTree.RegExpLiteral): AST.Range
 function getFlagsRange(
     regexpNode: ESTree.CallExpression | ESTree.RegExpLiteral,
diff --git a/lib/utils/mention.ts b/lib/utils/mention.ts
new file mode 100644
index 0000000..efa8da0
--- /dev/null
+++ b/lib/utils/mention.ts
@@ -0,0 +1,64 @@
+import type { CharacterClassElement, Node } from "regexpp/ast"
+
+/** Formats the given Unicode code point as `U+0000`. */
+function formatCodePoint(value: number): string {
+    return `U+${value.toString(16).padStart(4, "0")}`
+}
+
+/**
+ * Creates a string that mentions the given character class element.
+ *
+ * This is a specialized version of {@link mention} that will add
+ * character-related information if possible.
+ */
+export function mentionChar(element: CharacterClassElement): string {
+    if (element.type === "Character") {
+        const value = formatCodePoint(element.value)
+        return `'${escape(element.raw)}' (${value})`
+    }
+    if (element.type === "CharacterClassRange") {
+        const min = formatCodePoint(element.min.value)
+        const max = formatCodePoint(element.max.value)
+        return `'${escape(element.raw)}' (${min} - ${max})`
+    }
+
+    return mention(element)
+}
+
+/**
+ * Creates a string that mentions the given character class element.
+ */
+export function mention(element: Node): string {
+    return `'${escape(element.raw)}'`
+}
+
+/** Escape control characters in the given string */
+function escape(value: string): string {
+    // Escaping has to be done in 2 phases:
+    //  1. Escape all backslash-escaped control characters
+    //  2. Escape all non-escape control character
+    return (
+        value
+            .replace(/\\([\s\S])/g, (m, char) => {
+                if (char.charCodeAt(0) < 0x20) {
+                    return escapeControl(char)
+                }
+                return m
+            })
+            // eslint-disable-next-line no-control-regex -- x
+            .replace(/[\0-\x1f]/g, escapeControl)
+    )
+}
+
+/**
+ * Assuming that the given character is a control character, this function will
+ * return an escape sequence for that character.
+ */
+function escapeControl(control: string): string {
+    // we will allow tabs
+    if (control === "\t") return control
+
+    if (control === "\n") return "\\n"
+    if (control === "\r") return "\\r"
+    return `\\x${control.charCodeAt(0).toString(16).padStart(2, "0")}`
+}


Test patch:
diff --git a/tests/lib/rules/no-non-standard-flag.ts b/tests/lib/rules/no-non-standard-flag.ts
index e84347c..5488168 100644
--- a/tests/lib/rules/no-non-standard-flag.ts
+++ b/tests/lib/rules/no-non-standard-flag.ts
@@ -24,5 +24,34 @@ tester.run("no-non-standard-flag", rule as any, {
                 },
             ],
         },
+        {
+            code: `RegExp("foo", "l")`,
+            errors: [
+                {
+                    message: "Unexpected non-standard flag 'l'.",
+                    column: 16,
+                },
+            ],
+        },
+        {
+            // unknown pattern
+            code: `RegExp(someVariable, "l")`,
+            errors: [
+                {
+                    message: "Unexpected non-standard flag 'l'.",
+                    column: 23,
+                },
+            ],
+        },
+        {
+            // invalid pattern
+            code: `RegExp("(", "l")`,
+            errors: [
+                {
+                    message: "Unexpected non-standard flag 'l'.",
+                    column: 14,
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-useless-character-class.ts b/tests/lib/rules/no-useless-character-class.ts
index 0f17d47..5bd0b00 100644
--- a/tests/lib/rules/no-useless-character-class.ts
+++ b/tests/lib/rules/no-useless-character-class.ts
@@ -145,5 +145,11 @@ tester.run("no-useless-character-class", rule as any, {
             options: [{ ignores: [] }],
             errors: 18,
         },
+        {
+            code: String.raw`RegExp("[\"]" + '[\']')`,
+            output: String.raw`RegExp("\"" + '\'')`,
+            options: [{ ignores: [] }],
+            errors: 2,
+        },
     ],
 })
diff --git a/tests/lib/rules/sort-character-class-elements.ts b/tests/lib/rules/sort-character-class-elements.ts
index e97b1ac..1ef4681 100644
--- a/tests/lib/rules/sort-character-class-elements.ts
+++ b/tests/lib/rules/sort-character-class-elements.ts
@@ -217,13 +217,17 @@ tester.run("sort-character-class-elements", rule as any, {
             ],
         },
         {
-            code: String.raw`const s = "[\\d"+"\\w]"
-            new RegExp(s, 'u')`,
-            output: String.raw`const s = "[\\w\\d"+"]"
-            new RegExp(s, 'u')`,
-            options: [{ order: [] }],
-            errors: [
-                "Expected character class elements to be in ascending order. '\\w' should be before '\\d'.",
+            code: String.raw`
+            const jsxWhitespaceChars = " \n\r\t";
+            const matchJsxWhitespaceRegex = new RegExp("([" + jsxWhitespaceChars + "]+)");
+            `,
+            output: String.raw`
+            const jsxWhitespaceChars = "\n \r\t";
+            const matchJsxWhitespaceRegex = new RegExp("([" + jsxWhitespaceChars + "]+)");
+            `,
+            errors: [
+                "Expected character class elements to be in ascending order. '\\n' should be before ' '.",
+                "Expected character class elements to be in ascending order. '\t' should be before ' '.",
             ],
         },
     ],
diff --git a/tests/lib/rules/sort-flags.ts b/tests/lib/rules/sort-flags.ts
index 0f331cf..cabb6fe 100644
--- a/tests/lib/rules/sort-flags.ts
+++ b/tests/lib/rules/sort-flags.ts
@@ -31,7 +31,8 @@ tester.run("sort-flags", rule as any, {
             output: String.raw`/\w/gimsuy`,
             errors: [
                 {
-                    message: "The flags 'yusimg' should in the order 'gimsuy'.",
+                    message:
+                        "The flags 'yusimg' should be in the order 'gimsuy'.",
                     column: 5,
                 },
             ],
@@ -41,7 +42,8 @@ tester.run("sort-flags", rule as any, {
             output: String.raw`new RegExp("\\w", "gimsuy")`,
             errors: [
                 {
-                    message: "The flags 'yusimg' should in the order 'gimsuy'.",
+                    message:
+                        "The flags 'yusimg' should be in the order 'gimsuy'.",
                     column: 20,
                 },
             ],
@@ -52,10 +54,21 @@ tester.run("sort-flags", rule as any, {
             errors: [
                 {
                     message:
-                        "The flags 'yusimgd' should in the order 'dgimsuy'.",
+                        "The flags 'yusimgd' should be in the order 'dgimsuy'.",
                     column: 20,
                 },
             ],
         },
+        {
+            // sort flags even on invalid patterns
+            code: String.raw`new RegExp("\\w)", "ui")`,
+            output: String.raw`new RegExp("\\w)", "iu")`,
+            errors: [
+                {
+                    message: "The flags 'ui' should be in the order 'iu'.",
+                    column: 21,
+                },
+            ],
+        },
     ],
 })