Repository: ota-meshi/eslint-plugin-regexp (ts)

Issue:
Avoid double reporting for owned RegExp literals
![image](https://user-images.githubusercontent.com/20878432/129445512-50be0ae3-209b-464d-a4f2-07c33678a6bf.png)

The image speaks for itself. 

The problem gets reported once for the RegExp literal and once for the RegExp constructor. 

---

Solution: 
Since #282, we know exactly which RegExp literals are owned by a `RegExp` constructor. This information is necessary for `PatternSource` because it can only apply fixes to owned RegExp literals.

Pull request:
Avoid double reporting + flags API improvements
Fixes #304.

---

This turned out to be more difficult than I thought. Avoiding double reporting was easy enough (you can see that in the first commit). But changing `no-useless-flag` wasn't.

### Changes

Double reporting is now fixed:
![image](https://user-images.githubusercontent.com/20878432/129450838-11578552-3734-4c2d-83a9-3611bd64e09a.png)


As for `no-useless-flag`:

- Added a new category of useless flags: The flags of regexp literals that are only used by source. E.g. `/foo/i.source`, `RegExp(/foo/i, '')`

     ![image](https://user-images.githubusercontent.com/20878432/129450806-6eff2e71-727a-4228-9bb7-320405ac75aa.png)


- Improved `getFlagsLocation`. The function now has access to the flags argument directly which makes the whole thing easier to implement.
- Added `getFlagLocation`. This replaces the logic `no-useless-flag` formerly implemented in a private function. It's error locations are more precise because it has access to more information.
 
    Old:
    ![image](https://user-images.githubusercontent.com/20878432/129450785-45cd56b3-2834-45a9-a52e-997ef8efe5a2.png)

    New:
    ![image](https://user-images.githubusercontent.com/20878432/129450788-17a498e8-e5be-4d09-ab7e-20eccf0e6051.png)


Patch:
diff --git a/lib/rules/no-useless-flag.ts b/lib/rules/no-useless-flag.ts
index 16a6e9e..ec9ff9a 100644
--- a/lib/rules/no-useless-flag.ts
+++ b/lib/rules/no-useless-flag.ts
@@ -1,5 +1,11 @@
-import type { RegExpContext } from "../utils"
-import { compositingVisitors, createRule, defineRegexpVisitor } from "../utils"
+import type { RegExpContext, RegExpContextForSource } from "../utils"
+import {
+    getFlagsRange,
+    compositingVisitors,
+    createRule,
+    defineRegexpVisitor,
+    getFlagsLocation,
+} from "../utils"
 import type {
     CallExpression,
     Expression,
@@ -45,9 +51,11 @@ type RegExpMethodKind =
     | "replaceAll"
 
 class RegExpReference {
-    public regExpContext: RegExpContext
+    public readonly regExpContext: RegExpContext
 
-    public readonly defineNode: RegExpExpression
+    public get defineNode(): RegExpExpression {
+        return this.regExpContext.regexpNode
+    }
 
     public defineId?: CodePathId
 
@@ -69,12 +77,8 @@ class RegExpReference {
         track: true,
     }
 
-    public constructor(
-        regExpContext: RegExpContext,
-        defineNode: RegExpExpression,
-    ) {
+    public constructor(regExpContext: RegExpContext) {
         this.regExpContext = regExpContext
-        this.defineNode = defineNode
     }
 
     public addReadNode(node: Expression) {
@@ -165,31 +169,6 @@ class RegExpReference {
     }
 }
 
-/**
- * Gets the location for reporting the flag.
- */
-function getFlagLocation(
-    context: Rule.RuleContext,
-    node: RegExpExpression,
-    flag: "i" | "m" | "s" | "g" | "y",
-) {
-    const sourceCode = context.getSourceCode()
-    if (node.type === "Literal") {
-        const flagIndex =
-            node.range![1] -
-            node.regex.flags.length +
-            node.regex.flags.indexOf(flag)
-        return {
-            start: sourceCode.getLocFromIndex(flagIndex),
-            end: sourceCode.getLocFromIndex(flagIndex + 1),
-        }
-    }
-    if (node.arguments.length >= 2) {
-        return node.arguments[1].loc!
-    }
-    return context.getSourceCode().getTokenAfter(node.arguments[0])!.loc!
-}
-
 /**
  * Returns a fixer that removes the given flag.
  */
@@ -209,7 +188,13 @@ function fixRemoveFlag(
 function createUselessIgnoreCaseFlagVisitor(context: Rule.RuleContext) {
     return defineRegexpVisitor(context, {
         createVisitor(regExpContext: RegExpContext) {
-            const { flags, regexpNode, toCharSet, ownsFlags } = regExpContext
+            const {
+                flags,
+                regexpNode,
+                toCharSet,
+                ownsFlags,
+                getFlagLocation,
+            } = regExpContext
 
             if (!flags.ignoreCase || !ownsFlags) {
                 return {}
@@ -261,7 +246,7 @@ function createUselessIgnoreCaseFlagVisitor(context: Rule.RuleContext) {
                     if (unnecessary) {
                         context.report({
                             node: regexpNode,
-                            loc: getFlagLocation(context, regexpNode, "i"),
+                            loc: getFlagLocation("i"),
                             messageId: "uselessIgnoreCaseFlag",
                             fix: fixRemoveFlag(regExpContext, "i"),
                         })
@@ -278,7 +263,12 @@ function createUselessIgnoreCaseFlagVisitor(context: Rule.RuleContext) {
 function createUselessMultilineFlagVisitor(context: Rule.RuleContext) {
     return defineRegexpVisitor(context, {
         createVisitor(regExpContext: RegExpContext) {
-            const { flags, regexpNode, ownsFlags } = regExpContext
+            const {
+                flags,
+                regexpNode,
+                ownsFlags,
+                getFlagLocation,
+            } = regExpContext
 
             if (!flags.multiline || !ownsFlags) {
                 return {}
@@ -294,7 +284,7 @@ function createUselessMultilineFlagVisitor(context: Rule.RuleContext) {
                     if (unnecessary) {
                         context.report({
                             node: regexpNode,
-                            loc: getFlagLocation(context, regexpNode, "m"),
+                            loc: getFlagLocation("m"),
                             messageId: "uselessMultilineFlag",
                             fix: fixRemoveFlag(regExpContext, "m"),
                         })
@@ -311,7 +301,12 @@ function createUselessMultilineFlagVisitor(context: Rule.RuleContext) {
 function createUselessDotAllFlagVisitor(context: Rule.RuleContext) {
     return defineRegexpVisitor(context, {
         createVisitor(regExpContext: RegExpContext) {
-            const { flags, regexpNode, ownsFlags } = regExpContext
+            const {
+                flags,
+                regexpNode,
+                ownsFlags,
+                getFlagLocation,
+            } = regExpContext
 
             if (!flags.dotAll || !ownsFlags) {
                 return {}
@@ -327,7 +322,7 @@ function createUselessDotAllFlagVisitor(context: Rule.RuleContext) {
                     if (unnecessary) {
                         context.report({
                             node: regexpNode,
-                            loc: getFlagLocation(context, regexpNode, "s"),
+                            loc: getFlagLocation("s"),
                             messageId: "uselessDotAllFlag",
                             fix: fixRemoveFlag(regExpContext, "s"),
                         })
@@ -367,10 +362,12 @@ function createUselessGlobalFlagVisitor(
         regExpReference: RegExpReference,
         data: ReportData,
     ) {
+        const { getFlagLocation } = regExpReference.regExpContext
         const node = regExpReference.defineNode
+
         context.report({
             node,
-            loc: getFlagLocation(context, node, "g"),
+            loc: getFlagLocation("g"),
             messageId:
                 data.kind === ReportKind.usedOnlyInSplit
                     ? "uselessGlobalFlagForSplit"
@@ -492,10 +489,12 @@ function createUselessStickyFlagVisitor(
         regExpReference: RegExpReference,
         data: ReportData,
     ) {
+        const { getFlagLocation } = regExpReference.regExpContext
         const node = regExpReference.defineNode
+
         context.report({
             node,
-            loc: getFlagLocation(context, node, "y"),
+            loc: getFlagLocation("y"),
             messageId: "uselessStickyFlag",
             fix: data.fixable
                 ? fixRemoveFlag(regExpReference.regExpContext, "y")
@@ -640,10 +639,7 @@ function createRegExpReferenceExtractVisitor(
             createVisitor(regExpContext: RegExpContext) {
                 const { flags, regexpNode } = regExpContext
                 if (flags[flag]) {
-                    const regExpReference = new RegExpReference(
-                        regExpContext,
-                        regexpNode,
-                    )
+                    const regExpReference = new RegExpReference(regExpContext)
                     regExpReferenceList.push(regExpReference)
                     regExpReferenceMap.set(regexpNode, regExpReference)
                     for (const ref of extractExpressionReferences(
@@ -772,6 +768,56 @@ function createRegExpReferenceExtractVisitor(
     )
 }
 
+/**
+ * Create visitor for verify unnecessary flags of owned RegExp literals
+ */
+function createOwnedRegExpFlagsVisitor(context: Rule.RuleContext) {
+    const sourceCode = context.getSourceCode()
+
+    /** Remove the flags of the given literal */
+    function removeFlags(node: RegExpLiteral): void {
+        // The u flag is relevant for parsing the literal, so
+        // we can't just remove it and potentially create
+        // invalid source code.
+        const newFlags = node.regex.flags.replace(/[^u]+/g, "")
+        if (newFlags === node.regex.flags) {
+            return
+        }
+
+        context.report({
+            node,
+            loc: getFlagsLocation(sourceCode, node, node),
+            messageId: "uselessFlagsOwned",
+            fix(fixer) {
+                const range = getFlagsRange(node)
+                return fixer.replaceTextRange(range, newFlags)
+            },
+        })
+    }
+
+    return defineRegexpVisitor(context, {
+        createSourceVisitor(regExpContext: RegExpContextForSource) {
+            const { patternSource, regexpNode } = regExpContext
+
+            if (patternSource.isStringValue()) {
+                // all regexp literals are used via `.source`
+                patternSource.getOwnedRegExpLiterals().forEach(removeFlags)
+            } else {
+                // The source is copied from some other regex
+                if (regexpNode.arguments.length >= 2) {
+                    // and the flags are given using the second parameter
+                    const ownedNode = patternSource.regexpValue?.ownedNode
+                    if (ownedNode) {
+                        removeFlags(ownedNode)
+                    }
+                }
+            }
+
+            return {}
+        },
+    })
+}
+
 /**
  * Parse option
  */
@@ -844,6 +890,8 @@ export default createRule("no-useless-flag", {
                 "The 'g' flag is unnecessary because 'String.prototype.search' ignores the 'g' flag.",
             uselessStickyFlag:
                 "The 'y' flag is unnecessary because 'String.prototype.split' ignores the 'y' flag.",
+            uselessFlagsOwned:
+                "The flags of this RegExp literal are useless because only the source of the regex is used.",
         },
         type: "suggestion", // "problem",
     },
@@ -881,6 +929,10 @@ export default createRule("no-useless-flag", {
                 createUselessStickyFlagVisitor(context, strictTypes),
             )
         }
+        visitor = compositingVisitors(
+            visitor,
+            createOwnedRegExpFlagsVisitor(context),
+        )
         return visitor
     },
 })
diff --git a/lib/utils/ast-utils/pattern-source.ts b/lib/utils/ast-utils/pattern-source.ts
index bb6aeb6..d435149 100644
--- a/lib/utils/ast-utils/pattern-source.ts
+++ b/lib/utils/ast-utils/pattern-source.ts
@@ -139,30 +139,43 @@ class PatternSegment implements PatternRange {
         return this.start <= range.start && range.end <= this.end
     }
 
+    public getOwnedRegExpLiteral(): RegExpLiteral | null {
+        if (isRegexpLiteral(this.node)) {
+            return this.node
+        }
+
+        // e.g. /foo/.source
+        if (
+            this.node.type === "MemberExpression" &&
+            this.node.object.type !== "Super" &&
+            isRegexpLiteral(this.node.object) &&
+            getPropertyName(this.node) === "source"
+        ) {
+            return this.node.object
+        }
+
+        return null
+    }
+
     public getReplaceRange(range: PatternRange): PatternReplaceRange | null {
         if (!this.contains(range)) {
             return null
         }
 
-        if (this.node.type === "Literal") {
-            // This will cover string literals and RegExp literals
+        const regexp = this.getOwnedRegExpLiteral()
+        if (regexp) {
             return PatternReplaceRange.fromLiteral(
-                this.node,
+                regexp,
                 this.sourceCode,
                 this,
                 range,
             )
         }
 
-        // e.g. /foo/.source
-        if (
-            this.node.type === "MemberExpression" &&
-            this.node.object.type !== "Super" &&
-            isRegexpLiteral(this.node.object) &&
-            getPropertyName(this.node) === "source"
-        ) {
+        if (this.node.type === "Literal") {
+            // This will cover string literals
             return PatternReplaceRange.fromLiteral(
-                this.node.object,
+                this.node,
                 this.sourceCode,
                 this,
                 range,
@@ -394,6 +407,25 @@ export class PatternSource {
     public getAstLocation(range: PatternRange): AST.SourceLocation {
         return astRangeToLocation(this.sourceCode, this.getAstRange(range))
     }
+
+    /**
+     * Returns all RegExp literals nodes that are owned by this pattern.
+     *
+     * This means that the returned RegExp literals are only used to create
+     * this pattern and for nothing else.
+     */
+    public getOwnedRegExpLiterals(): readonly RegExpLiteral[] {
+        const literals: RegExpLiteral[] = []
+
+        for (const segment of this.segments) {
+            const regexp = segment.getOwnedRegExpLiteral()
+            if (regexp) {
+                literals.push(regexp)
+            }
+        }
+
+        return literals
+    }
 }
 
 /**
diff --git a/lib/utils/index.ts b/lib/utils/index.ts
index 852c870..18deafd 100644
--- a/lib/utils/index.ts
+++ b/lib/utils/index.ts
@@ -13,7 +13,11 @@ import type { CharSet } from "refa"
 import { JS } from "refa"
 import type { UsageOfPattern } from "./get-usage-of-pattern"
 import { getUsageOfPattern } from "./get-usage-of-pattern"
-import { isRegexpLiteral, isStringLiteral } from "./ast-utils/utils"
+import {
+    getStringValueRange,
+    isRegexpLiteral,
+    isStringLiteral,
+} from "./ast-utils/utils"
 import type { PatternRange } from "./ast-utils/pattern-source"
 import { PatternSource } from "./ast-utils/pattern-source"
 export * from "./unicode"
@@ -40,12 +44,15 @@ type RegExpContextBase = {
     ) => AST.SourceLocation
 
     /**
-     * Creates SourceLocation from the regexp flags
-     *
-     * @returns The SourceLocation
+     * Creates SourceLocation of the regexp flags
      */
     getFlagsLocation: () => AST.SourceLocation
 
+    /**
+     * Creates SourceLocation of the regexp flag
+     */
+    getFlagLocation: (flag: string) => AST.SourceLocation
+
     /**
      * Creates a new fix that replaces the given node with a given string.
      *
@@ -107,12 +114,15 @@ type UnparsableRegExpContextBase = {
     ownsFlags: boolean
 
     /**
-     * Creates SourceLocation from the regexp flags
-     *
-     * @returns The SourceLocation
+     * Creates SourceLocation of the regexp flags
      */
     getFlagsLocation: () => AST.SourceLocation
 
+    /**
+     * Creates SourceLocation of the regexp flag
+     */
+    getFlagLocation: (flag: string) => AST.SourceLocation
+
     fixReplaceFlags: (
         newFlags: string | (() => string | null),
     ) => (fixer: Rule.RuleFixer) => Rule.Fix[] | Rule.Fix | null
@@ -263,6 +273,7 @@ function buildRegexpVisitor(
      */
     function verify(
         patternNode: ESTree.Expression,
+        flagsNode: ESTree.Expression | null,
         regexpNode: ESTree.RegExpLiteral | ESTree.CallExpression,
         patternSource: PatternSource | null,
         flagsString: string | null,
@@ -283,6 +294,7 @@ function buildRegexpVisitor(
                     context,
                     flags,
                     flagsString,
+                    flagsNode,
                     ownsFlags,
                 }),
             })
@@ -312,6 +324,7 @@ function buildRegexpVisitor(
                         context,
                         flags,
                         flagsString,
+                        flagsNode,
                         ownsFlags,
                     }),
                 })
@@ -322,6 +335,7 @@ function buildRegexpVisitor(
         const helpers = buildRegExpContextBase({
             patternSource,
             regexpNode,
+            flagsNode,
             context,
             flags,
             parsedPattern,
@@ -330,23 +344,33 @@ function buildRegexpVisitor(
         visitRegExpAST(parsedPattern, createVisitor(helpers))
     }
 
+    const ownedRegExpLiterals = new Set<ESTree.RegExpLiteral>()
+
     return {
         "Program:exit": programExit,
         Literal(node: ESTree.Literal) {
-            if (!isRegexpLiteral(node)) {
+            if (!isRegexpLiteral(node) || ownedRegExpLiterals.has(node)) {
                 return
             }
             const flagsString = node.regex.flags
             const patternSource = PatternSource.fromRegExpLiteral(context, node)
-            verify(node, node, patternSource, flagsString, true, (base) => {
-                return createLiteralVisitorFromRules(rules, {
-                    node,
-                    flagsString,
-                    ownsFlags: true,
-                    regexpNode: node,
-                    ...base,
-                })
-            })
+            verify(
+                node,
+                node,
+                node,
+                patternSource,
+                flagsString,
+                true,
+                (base) => {
+                    return createLiteralVisitorFromRules(rules, {
+                        node,
+                        flagsString,
+                        ownsFlags: true,
+                        regexpNode: node,
+                        ...base,
+                    })
+                },
+            )
         },
         Program() {
             const tracker = new ReferenceTracker(context.getScope())
@@ -358,6 +382,7 @@ function buildRegexpVisitor(
                 call: ESTree.CallExpression
                 patternNode: ESTree.Expression
                 patternSource: PatternSource | null
+                flagsNode: ESTree.Expression | null
                 flagsString: string | null
                 ownsFlags: boolean
             }[] = []
@@ -365,30 +390,46 @@ function buildRegexpVisitor(
                 RegExp: { [CALL]: true, [CONSTRUCT]: true },
             })) {
                 const newOrCall = node as ESTree.CallExpression
-                const [patternNode, flagsNode] = newOrCall.arguments
-                if (!patternNode || patternNode.type === "SpreadElement") {
+                const args = newOrCall.arguments as (
+                    | ESTree.Expression
+                    | ESTree.SpreadElement
+                    | undefined
+                )[]
+                const [patternArg, flagsArg] = args
+                if (!patternArg || patternArg.type === "SpreadElement") {
                     continue
                 }
 
                 const patternSource = PatternSource.fromExpression(
                     context,
-                    patternNode,
+                    patternArg,
                 )
 
+                // avoid double reporting
+                patternSource
+                    ?.getOwnedRegExpLiterals()
+                    .forEach((n) => ownedRegExpLiterals.add(n))
+
+                let flagsNode = null
                 let flagsString = null
                 let ownsFlags = false
-                if (flagsNode && flagsNode.type !== "SpreadElement") {
-                    flagsString = getStringIfConstant(context, flagsNode)
-                    ownsFlags = isStringLiteral(flagsNode)
+                if (flagsArg) {
+                    if (flagsArg.type !== "SpreadElement") {
+                        flagsNode = flagsArg
+                        flagsString = getStringIfConstant(context, flagsArg)
+                        ownsFlags = isStringLiteral(flagsArg)
+                    }
                 } else if (patternSource && patternSource.regexpValue) {
                     flagsString = patternSource.regexpValue.flags
                     ownsFlags = Boolean(patternSource.regexpValue.ownedNode)
+                    flagsNode = patternSource.regexpValue.ownedNode
                 }
 
                 regexpDataList.push({
                     call: newOrCall,
-                    patternNode,
+                    patternNode: patternArg,
                     patternSource,
+                    flagsNode,
                     flagsString,
                     ownsFlags,
                 })
@@ -398,11 +439,13 @@ function buildRegexpVisitor(
                 call,
                 patternNode,
                 patternSource,
+                flagsNode,
                 flagsString,
                 ownsFlags,
             } of regexpDataList) {
                 verify(
                     patternNode,
+                    flagsNode,
                     call,
                     patternSource,
                     flagsString,
@@ -533,12 +576,14 @@ export function compositingVisitors(
 function buildRegExpContextBase({
     patternSource,
     regexpNode,
+    flagsNode,
     context,
     flags,
     parsedPattern,
 }: {
     patternSource: PatternSource
     regexpNode: ESTree.CallExpression | ESTree.RegExpLiteral
+    flagsNode: ESTree.Expression | null
     context: Rule.RuleContext
     flags: ReadonlyFlags
     parsedPattern: Pattern
@@ -571,7 +616,10 @@ function buildRegExpContextBase({
             }
             return patternSource.getAstLocation(range)
         },
-        getFlagsLocation: () => getFlagsLocation(sourceCode, regexpNode),
+        getFlagsLocation: () =>
+            getFlagsLocation(sourceCode, regexpNode, flagsNode),
+        getFlagLocation: (flag) =>
+            getFlagLocation(sourceCode, regexpNode, flagsNode, flag),
         fixReplaceNode: (node, replacement) => {
             return fixReplaceNode(patternSource, node, replacement)
         },
@@ -579,7 +627,12 @@ function buildRegExpContextBase({
             return fixReplaceQuant(patternSource, qNode, replacement)
         },
         fixReplaceFlags: (newFlags) => {
-            return fixReplaceFlags(patternSource, regexpNode, newFlags)
+            return fixReplaceFlags(
+                patternSource,
+                regexpNode,
+                flagsNode,
+                newFlags,
+            )
         },
         getUsageOfPattern: () =>
             (cacheUsageOfPattern ??= getUsageOfPattern(regexpNode, context)),
@@ -601,6 +654,7 @@ function buildUnparsableRegExpContextBase({
     context,
     flags,
     flagsString,
+    flagsNode,
     ownsFlags,
 }: {
     patternSource: PatternSource | null
@@ -609,6 +663,7 @@ function buildUnparsableRegExpContextBase({
     context: Rule.RuleContext
     flags: ReadonlyFlags
     flagsString: string | null
+    flagsNode: ESTree.Expression | null
     ownsFlags: boolean
 }): UnparsableRegExpContextBase {
     const sourceCode = context.getSourceCode()
@@ -621,42 +676,48 @@ function buildUnparsableRegExpContextBase({
         flagsString,
         ownsFlags,
 
-        getFlagsLocation: () => getFlagsLocation(sourceCode, regexpNode),
+        getFlagsLocation: () =>
+            getFlagsLocation(sourceCode, regexpNode, flagsNode),
+        getFlagLocation: (flag) =>
+            getFlagLocation(sourceCode, regexpNode, flagsNode, flag),
 
         fixReplaceFlags: (newFlags) => {
             if (!patternSource) {
                 return () => null
             }
-            return fixReplaceFlags(patternSource, regexpNode, newFlags)
+            return fixReplaceFlags(
+                patternSource,
+                regexpNode,
+                flagsNode,
+                newFlags,
+            )
         },
     }
 }
 
-function getFlagsRange(regexpNode: ESTree.RegExpLiteral): AST.Range
-function getFlagsRange(
-    regexpNode: ESTree.CallExpression | ESTree.RegExpLiteral,
+export function getFlagsRange(flagsNode: ESTree.RegExpLiteral): AST.Range
+export function getFlagsRange(
+    flagsNode: ESTree.Expression | null,
 ): AST.Range | null
 /**
  * Creates source range of the flags of the given regexp node
- * @param regexpNode The regexp node to report.
- * @returns The SourceLocation
+ * @param flagsNode The expression that contributes the flags.
  */
-function getFlagsRange(
-    regexpNode: ESTree.CallExpression | ESTree.RegExpLiteral,
+export function getFlagsRange(
+    flagsNode: ESTree.Expression | null,
 ): AST.Range | null {
-    if (isRegexpLiteral(regexpNode)) {
+    if (!flagsNode) {
+        return null
+    }
+
+    if (isRegexpLiteral(flagsNode)) {
         return [
-            regexpNode.range![1] - regexpNode.regex.flags.length,
-            regexpNode.range![1],
+            flagsNode.range![1] - flagsNode.regex.flags.length,
+            flagsNode.range![1],
         ]
     }
-    const flagsArg = regexpNode.arguments[1]
-    if (
-        flagsArg &&
-        flagsArg.type === "Literal" &&
-        typeof flagsArg.value === "string"
-    ) {
-        return [flagsArg.range![0] + 1, flagsArg.range![1] - 1]
+    if (isStringLiteral(flagsNode)) {
+        return [flagsNode.range![0] + 1, flagsNode.range![1] - 1]
     }
 
     return null
@@ -666,15 +727,67 @@ function getFlagsRange(
  * Creates SourceLocation of the flags of the given regexp node
  * @param sourceCode The ESLint source code instance.
  * @param regexpNode The node to report.
- * @returns The SourceLocation
  */
-function getFlagsLocation(
+export function getFlagsLocation(
+    sourceCode: SourceCode,
+    regexpNode: ESTree.CallExpression | ESTree.RegExpLiteral,
+    flagsNode: ESTree.Expression | null,
+): AST.SourceLocation {
+    const range = getFlagsRange(flagsNode)
+    if (range == null) {
+        return flagsNode?.loc ?? regexpNode.loc!
+    }
+    return {
+        start: sourceCode.getLocFromIndex(range[0]),
+        end: sourceCode.getLocFromIndex(range[1]),
+    }
+}
+
+/**
+ * Creates source range of the given flag in the given flags node
+ * @param flagsNode The expression that contributes the flags.
+ */
+function getFlagRange(
+    sourceCode: SourceCode,
+    flagsNode: ESTree.Expression | null,
+    flag: string,
+): AST.Range | null {
+    if (!flagsNode || !flag) {
+        return null
+    }
+
+    if (isRegexpLiteral(flagsNode)) {
+        const index = flagsNode.regex.flags.indexOf(flag)
+        if (index === -1) {
+            return null
+        }
+        const start = flagsNode.range![1] - flagsNode.regex.flags.length + index
+        return [start, start + 1]
+    }
+    if (isStringLiteral(flagsNode)) {
+        const index = flagsNode.value.indexOf(flag)
+        if (index === -1) {
+            return null
+        }
+        return getStringValueRange(sourceCode, flagsNode, index, index + 1)
+    }
+
+    return null
+}
+
+/**
+ * Creates source location of the given flag in the given flags node
+ * @param flagsNode The expression that contributes the flags.
+ */
+function getFlagLocation(
     sourceCode: SourceCode,
     regexpNode: ESTree.CallExpression | ESTree.RegExpLiteral,
+    flagsNode: ESTree.Expression | null,
+    flag: string,
 ): AST.SourceLocation {
-    const range = getFlagsRange(regexpNode)
+    const range = getFlagRange(sourceCode, flagsNode, flag)
     if (range == null) {
-        return regexpNode.loc!
+        return flagsNode?.loc ?? regexpNode.loc!
     }
     return {
         start: sourceCode.getLocFromIndex(range[0]),
@@ -765,6 +878,7 @@ function fixReplaceQuant(
 function fixReplaceFlags(
     patternSource: PatternSource,
     regexpNode: ESTree.CallExpression | ESTree.RegExpLiteral,
+    flagsNode: ESTree.Expression | null,
     replacement: string | (() => string | null),
 ) {
     return (fixer: Rule.RuleFixer): Rule.Fix[] | Rule.Fix | null => {
@@ -793,7 +907,7 @@ function fixReplaceFlags(
             )
         }
 
-        const range = getFlagsRange(regexpNode)
+        const range = getFlagsRange(flagsNode)
         if (range == null) {
             return null
         }


Test patch:
diff --git a/tests/lib/rules/no-dupe-characters-character-class.ts b/tests/lib/rules/no-dupe-characters-character-class.ts
index 7830581..71ef3e2 100644
--- a/tests/lib/rules/no-dupe-characters-character-class.ts
+++ b/tests/lib/rules/no-dupe-characters-character-class.ts
@@ -78,6 +78,16 @@ tester.run("no-dupe-characters-character-class", rule as any, {
                 },
             ],
         },
+        {
+            code: "RegExp(/[bb]/)",
+            output: "RegExp(/[b]/)",
+            errors: [
+                {
+                    message: "Unexpected duplicate 'b' (U+0062).",
+                    column: 11,
+                },
+            ],
+        },
         {
             code:
                 "/[\\s \\f\\n\\r\\t\\v\\u00a0\\u1680\\u180e\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]/",
diff --git a/tests/lib/rules/no-useless-flag.ts b/tests/lib/rules/no-useless-flag.ts
index e5fb339..f76f124 100644
--- a/tests/lib/rules/no-useless-flag.ts
+++ b/tests/lib/rules/no-useless-flag.ts
@@ -109,7 +109,7 @@ tester.run("no-useless-flag", rule as any, {
         `
         const notStr = {}
         notStr.split(/foo/g);
-        
+
         maybeStr.split(/foo/g);
         `,
 
@@ -214,6 +214,10 @@ tester.run("no-useless-flag", rule as any, {
         { code: String.raw`/\w/m`, options: [{ ignore: ["m"] }] },
         { code: String.raw`/\w/s`, options: [{ ignore: ["s"] }] },
         { code: `/foo/g.test(string)`, options: [{ ignore: ["g"] }] },
+        String.raw`
+        const orig = /\w/i; // eslint-disable-line
+        const clone = new RegExp(orig);
+        `,
     ],
     invalid: [
         // i
@@ -401,9 +405,9 @@ tester.run("no-useless-flag", rule as any, {
                     message:
                         "The 'g' flag is unnecessary because the regex is used only once in 'RegExp.prototype.test'.",
                     line: 25,
-                    column: 31,
+                    column: 32,
                     endLine: 25,
-                    endColumn: 34,
+                    endColumn: 33,
                 },
                 {
                     message:
@@ -459,7 +463,7 @@ tester.run("no-useless-flag", rule as any, {
                     message:
                         "The 'g' flag is unnecessary because the regex is used only once in 'RegExp.prototype.test'.",
                     line: 2,
-                    column: 31,
+                    column: 32,
                 },
             ],
         },
@@ -608,17 +612,17 @@ tester.run("no-useless-flag", rule as any, {
             const str = 'table football, foosball';
             regex1.lastIndex = 6
             var array = regex1.exec(str)
-            
+
             const regex2 = /foo/y;
             regex2.test(string);
             regex2.test(string);
-            
+
             str.replace(/foo/y, 'bar');
             str.replaceAll(/foo/gy, 'bar');
 
             const regexp3 = /foo/y
             str.search(regexp3)
-            
+
             /* ✗ BAD */
             str.split(/foo/y);
             `,
@@ -628,17 +632,17 @@ tester.run("no-useless-flag", rule as any, {
             const str = 'table football, foosball';
             regex1.lastIndex = 6
             var array = regex1.exec(str)
-            
+
             const regex2 = /foo/y;
             regex2.test(string);
             regex2.test(string);
-            
+
             str.replace(/foo/y, 'bar');
             str.replaceAll(/foo/gy, 'bar');
 
             const regexp3 = /foo/y
             str.search(regexp3)
-            
+
             /* ✗ BAD */
             str.split(/foo/);
             `,
@@ -679,7 +683,7 @@ tester.run("no-useless-flag", rule as any, {
                     message:
                         "The 'y' flag is unnecessary because 'String.prototype.split' ignores the 'y' flag.",
                     line: 2,
-                    column: 43,
+                    column: 44,
                 },
             ],
         },
@@ -724,44 +728,47 @@ tester.run("no-useless-flag", rule as any, {
         {
             code: String.raw`
             const orig = /\w/i; // eslint-disable-line
-            const clone = new RegExp(orig);
+            const clone = new RegExp(orig, 'i');
+            `,
+            output: String.raw`
+            const orig = /\w/i; // eslint-disable-line
+            const clone = new RegExp(orig, '');
             `,
-            output: null,
             errors: [
                 {
                     message:
                         "The 'i' flag is unnecessary because the pattern only contains case-invariant characters.",
                     line: 3,
-                    column: 42,
+                    column: 45,
                 },
             ],
         },
         {
             code: String.raw`
-            const orig = /\w/i; // eslint-disable-line
-            const clone = new RegExp(orig, 'i');
+            const orig = /\w/iy;
+            const clone = new RegExp(orig, '');
             `,
             output: String.raw`
-            const orig = /\w/i; // eslint-disable-line
+            const orig = /\w/;
             const clone = new RegExp(orig, '');
             `,
             errors: [
                 {
                     message:
-                        "The 'i' flag is unnecessary because the pattern only contains case-invariant characters.",
-                    line: 3,
-                    column: 44,
+                        "The flags of this RegExp literal are useless because only the source of the regex is used.",
+                    line: 2,
+                    column: 30,
                 },
             ],
         },
         {
             code: String.raw`
             const orig = /\w/i;
-            const clone = new RegExp(orig, '');
+            const clone = new RegExp(orig);
             `,
             output: String.raw`
             const orig = /\w/;
-            const clone = new RegExp(orig, '');
+            const clone = new RegExp(orig);
             `,
             errors: [
                 {
@@ -772,6 +779,18 @@ tester.run("no-useless-flag", rule as any, {
                 },
             ],
         },
+        {
+            code: String.raw`RegExp(/\w/i);`,
+            output: String.raw`RegExp(/\w/);`,
+            errors: [
+                {
+                    message:
+                        "The 'i' flag is unnecessary because the pattern only contains case-invariant characters.",
+                    line: 1,
+                    column: 12,
+                },
+            ],
+        },
         {
             code: String.raw`
             const orig = /\w/;
@@ -786,7 +805,19 @@ tester.run("no-useless-flag", rule as any, {
                     message:
                         "The 'i' flag is unnecessary because the pattern only contains case-invariant characters.",
                     line: 3,
-                    column: 44,
+                    column: 45,
+                },
+            ],
+        },
+        {
+            code: String.raw`RegExp(/\w/imu.source);`,
+            output: String.raw`RegExp(/\w/u.source);`,
+            errors: [
+                {
+                    message:
+                        "The flags of this RegExp literal are useless because only the source of the regex is used.",
+                    line: 1,
+                    column: 12,
                 },
             ],
         },