Repository: ota-meshi/eslint-plugin-regexp
Pull request #287 (2021-08-10 18:53:38)

Support unknown and invalid patterns
This is the first step for #283.

This PR adds the API for unknown and invalid patterns. `defineRegexpVisitor` now accepts two now functions in addition to the existing `create*Visitor` functions:

```ts
    visitInvalid?: (context: RegExpContextForInvalid) => void
    visitUnknown?: (context: RegExpContextForUnknown) => void
```


2 existing rules benefit from this: 

- `no-non-standard-flag` can now report flags even if we can't parse the pattern of a regex.
- Same for `sort-flags`. The rule is even fixable in some of those cases.

I also slightly changed both rules:

- `no-non-standard-flag` will now report on the `regexpNode` instead of the `patternNode` if the location of the flags is unavailable. I think doesn't change anything right now.
- Fixed a slight grammatical error in the error message of `sort-flags`.


Patch:
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/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,


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-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,
+                },
+            ],
+        },
     ],
 })