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

Issue:
`regexp/no-dupe-disjunctions` should "de-sugar" character classes
**Description**
While writing some regexes, I noticed the `regexp/no-dupe-disjunctions` didn't say anything about a regex of this from:

```js
/a+|[abc]/
```

The `a` in `[abc]` is clearly useless but didn't get reported. This is because `regexp/no-dupe-disjunctions` looks at the `[abc]` as a whole and only detects the overlap between `a+` and `[abc]`. It doesn't understand that the overlap is really subset (with `a` in `[abc]`) and therefor you get this report with `report: "all"` even though the problem is trivial.

---

Some test cases:

```js
// bad
var foo = /a+|[abc]/; // remove `a` in `[abc]`
var foo = /a+|a|b|c/; // remove `a` (this currently works)
var foo = /a+|[a-f]/; // change `a-f` to `b-f`

var foo = /a|[ab]a/; // remove `a` in `[ab]`
var foo = /a|aa|bb/; // remove `aa` (this currently works)

// ok
var foo = /c+|[a-f]/;
```

Pull request description:
Make `regexp/no-dupe-disjunctions` account for nested alternatives
This fixes #402.

---

To implement this, I needed to introduce 2 new concepts to the code base:

1. Nested alternatives: These are alternatives or things that behave like alternatives (e.g. character class elements). They are called nested because they are the (transitive) child nodes of some root alternative. 
    E.g. if `a(c|b)[de]` is the root alternative, then `b`, `c`, `d`, and `e` are nested alternatives.
3. Partial NFAs: The partial NFA of a given root alternative and nested alternative is the NFA that contains all paths of the root alternative that go through the nested alternative.
    E.g. for the root alternative `(a|b)(c|d|e)f` and nested alternative `d`, the language of the partial NFA will be `(a|b)df`.

These 2 concepts are useful because they allow us to ask: Is there some part of an alternative that is a subset? 

This is actually quite a bit more general than simple de-sugaring of character classes. This PR allows use to detect partial subsets and duplications within alternatives, and that includes character classes.

---

Side note: I expected this to massively impact performance. We are potentially creating a bunch more NFAs and DFAs than before, but (surprisingly) I couldn't measure any difference when I ran this PR against Prism's 2k regexes. (It did find 5 bugs though.)

Patch:
diff --git a/lib/rules/no-dupe-disjunctions.ts b/lib/rules/no-dupe-disjunctions.ts
index fbd1da1..02dc312 100644
--- a/lib/rules/no-dupe-disjunctions.ts
+++ b/lib/rules/no-dupe-disjunctions.ts
@@ -20,9 +20,11 @@ import {
     JS,
     visitAst,
     transform,
+    isDisjointWith,
 } from "refa"
 import type { ReadonlyFlags } from "regexp-ast-analysis"
 import {
+    hasSomeAncestor,
     hasSomeDescendant,
     getMatchingDirection,
     getEffectiveMaximumRepetition,
@@ -30,7 +32,9 @@ import {
 import { RegExpParser } from "regexpp"
 import { UsageOfPattern } from "../utils/get-usage-of-pattern"
 import { canReorder } from "../utils/reorder-alternatives"
-import { mention } from "../utils/mention"
+import { mention, mentionChar } from "../utils/mention"
+import type { NestedAlternative } from "../utils/partial-parser"
+import { PartialParser } from "../utils/partial-parser"
 
 type ParentNode = Group | CapturingGroup | Pattern | LookaroundAssertion
 
@@ -121,6 +125,18 @@ function containsAssertionsOrUnknowns(
     }
 }
 
+/**
+ * Returns whether the given nodes contains any features that cannot be
+ * expressed by pure regular expression. This mainly includes assertions and
+ * backreferences.
+ */
+function isNonRegular(node: Node): boolean {
+    return hasSomeDescendant(
+        node,
+        (d) => d.type === "Assertion" || d.type === "Backreference",
+    )
+}
+
 const creationOption: Transformers.CreationOptions = {
     ignoreAmbiguity: true,
     ignoreOrder: true,
@@ -175,6 +191,95 @@ function toNFA(
     }
 }
 
+/**
+ * Yields all nested alternatives in the given root alternative.
+ *
+ * This will yield actual alternative nodes as well as character class
+ * elements. The elements of a non-negated character class (e.g. `[abc]`) can
+ * be thought of as an alternative. That's why they are returned.
+ *
+ * Note: If a group contains only a single alternative, then this group won't
+ * be yielded by this function. This is because the partial NFA of that single
+ * alternative is that same as the partial NFA of the parent alternative of the
+ * group. A similar condition applies for the elements of character classes.
+ */
+function* iterateNestedAlternatives(
+    alternative: Alternative,
+): Iterable<NestedAlternative> {
+    for (const e of alternative.elements) {
+        if (e.type === "Group" || e.type === "CapturingGroup") {
+            for (const a of e.alternatives) {
+                if (e.alternatives.length > 1) {
+                    yield a
+                }
+
+                yield* iterateNestedAlternatives(a)
+            }
+        }
+
+        if (e.type === "CharacterClass" && !e.negate) {
+            const nested: NestedAlternative[] = []
+            for (const charElement of e.elements) {
+                if (charElement.type === "CharacterClassRange") {
+                    const min = charElement.min
+                    const max = charElement.max
+                    if (min.value === max.value) {
+                        nested.push(charElement)
+                    } else if (min.value + 1 === max.value) {
+                        nested.push(min, max)
+                    } else {
+                        nested.push(charElement, min, max)
+                    }
+                } else {
+                    nested.push(charElement)
+                }
+            }
+            if (nested.length > 1) yield* nested
+        }
+    }
+}
+
+/**
+ * A nested alternative + the partial NFA of that nested alternatives.
+ *
+ * The partial NFA is generated by {@link PartialParser}.
+ */
+interface PartialAlternative {
+    nested: NestedAlternative
+    nfa: NFA
+}
+
+/**
+ * This will return all partial alternatives.
+ *
+ * A partial alternative is the NFA of the given alternative but with one
+ * nested alternative missing.
+ */
+function* iteratePartialAlternatives(
+    alternative: Alternative,
+    parser: JS.Parser,
+): Iterable<PartialAlternative> {
+    if (isNonRegular(alternative)) {
+        return
+    }
+
+    const maxCharacter = parser.ast.flags.unicode ? 0x10ffff : 0xffff
+    const partialParser = new PartialParser(parser, {
+        assertions: "throw",
+        backreferences: "throw",
+    })
+
+    for (const nested of iterateNestedAlternatives(alternative)) {
+        try {
+            const expression = partialParser.parse(alternative, nested)
+            const nfa = NFA.fromRegex(expression, { maxCharacter })
+            yield { nested, nfa }
+        } catch (error) {
+            // ignore error and skip this
+        }
+    }
+}
+
 /**
  * Unions all given NFAs
  */
@@ -332,9 +437,17 @@ interface DuplicateResult extends ResultBase {
 interface SubsetResult extends ResultBase {
     type: "Subset"
 }
+interface NestedSubsetResult extends ResultBase {
+    type: "NestedSubset"
+    nested: NestedAlternative
+}
 interface PrefixSubsetResult extends ResultBase {
     type: "PrefixSubset"
 }
+interface PrefixNestedSubsetResult extends ResultBase {
+    type: "PrefixNestedSubset"
+    nested: NestedAlternative
+}
 interface SupersetResult extends ResultBase {
     type: "Superset"
 }
@@ -345,7 +458,9 @@ interface OverlapResult extends ResultBase {
 type Result =
     | DuplicateResult
     | SubsetResult
+    | NestedSubsetResult
     | PrefixSubsetResult
+    | PrefixNestedSubsetResult
     | SupersetResult
     | OverlapResult
 
@@ -440,6 +555,7 @@ function* findDuplicationAst(
  */
 function* findPrefixDuplicationNfa(
     alternatives: [NFA, boolean, Alternative][],
+    parser: JS.Parser,
 ): Iterable<Result> {
     if (alternatives.length === 0) {
         return
@@ -455,7 +571,7 @@ function* findPrefixDuplicationNfa(
         if (!partial) {
             const overlapping = alternatives
                 .slice(0, i)
-                .filter(([otherNfa]) => !nfa.isDisjointWith(otherNfa))
+                .filter(([otherNfa]) => !isDisjointWith(nfa, otherNfa))
 
             if (overlapping.length >= 1) {
                 const othersNfa = unionAll(overlapping.map(([n]) => n))
@@ -467,6 +583,17 @@ function* findPrefixDuplicationNfa(
 
                 if (isSubsetOf(othersNfa, nfa)) {
                     yield { type: "PrefixSubset", alternative, others }
+                } else {
+                    const nested = tryFindNestedSubsetResult(
+                        overlapping.map((o) => [o[0], o[2]]),
+                        othersNfa,
+                        alternative,
+                        parser,
+                    )
+
+                    if (nested) {
+                        yield { ...nested, type: "PrefixNestedSubset" }
+                    }
                 }
             }
         }
@@ -489,7 +616,7 @@ function* findDuplicationNfa(
         const { nfa, partial } = toNFA(parser, alternative)
 
         const overlapping = previous.filter(
-            ([otherNfa]) => !nfa.isDisjointWith(otherNfa),
+            ([otherNfa]) => !isDisjointWith(nfa, otherNfa),
         )
 
         if (overlapping.length >= 1) {
@@ -544,7 +671,18 @@ function* findDuplicationNfa(
                 }
 
                 case SubsetRelation.none:
-                case SubsetRelation.unknown:
+                case SubsetRelation.unknown: {
+                    const nested = tryFindNestedSubsetResult(
+                        overlapping.map((o) => [o[0], o[2]]),
+                        othersNfa,
+                        alternative,
+                        parser,
+                    )
+                    if (nested) {
+                        yield nested
+                        break
+                    }
+
                     if (!ignoreOverlap) {
                         yield {
                             type: "Overlap",
@@ -554,6 +692,7 @@ function* findDuplicationNfa(
                         }
                     }
                     break
+                }
 
                 default:
                     throw new Error(relation)
@@ -564,8 +703,52 @@ function* findDuplicationNfa(
     }
 
     if (hasNothingAfter) {
-        yield* findPrefixDuplicationNfa(previous)
+        yield* findPrefixDuplicationNfa(previous, parser)
+    }
+}
+
+/**
+ * Given an alternative and list of overlapping other alternatives, this will
+ * try to find a nested alternative within the given alternative such that the
+ * nested alternative is a subset of the other alternatives.
+ */
+function tryFindNestedSubsetResult(
+    others: [ReadonlyNFA, Alternative][],
+    othersNfa: ReadonlyNFA,
+    alternative: Alternative,
+    parser: JS.Parser,
+): NestedSubsetResult | undefined {
+    const disjointElements = new Set<Node>()
+
+    for (const { nested, nfa: nestedNfa } of iteratePartialAlternatives(
+        alternative,
+        parser,
+    )) {
+        if (hasSomeAncestor(nested, (a) => disjointElements.has(a))) {
+            // there's no point in trying because the partial NFA of an
+            // ancestor of this nested alternative was disjoint with the
+            // target (others) NFA
+            continue
+        }
+
+        if (isDisjointWith(othersNfa, nestedNfa)) {
+            disjointElements.add(nested)
+            continue
+        }
+
+        if (isSubsetOf(othersNfa, nestedNfa)) {
+            return {
+                type: "NestedSubset",
+                alternative,
+                nested,
+                others: others
+                    .filter((o) => !isDisjointWith(o[0], nestedNfa))
+                    .map((o) => o[1]),
+            }
+        }
     }
+
+    return undefined
 }
 
 /**
@@ -592,7 +775,9 @@ function* findDuplication(
 const RESULT_TYPE_ORDER: Result["type"][] = [
     "Duplicate",
     "Subset",
+    "NestedSubset",
     "PrefixSubset",
+    "PrefixNestedSubset",
     "Superset",
     "Overlap",
 ]
@@ -644,6 +829,14 @@ function assertNever(value: never): never {
     throw new Error(`Invalid value: ${value}`)
 }
 
+/** Mentions the given nested alternative. */
+function mentionNested(nested: NestedAlternative): string {
+    if (nested.type === "Alternative") {
+        return mention(nested)
+    }
+    return mentionChar(nested)
+}
+
 const enum ReportOption {
     all = "all",
     trivial = "trivial",
@@ -702,8 +895,12 @@ export default createRule("no-dupe-disjunctions", {
             duplicate:
                 "Unexpected duplicate alternative. This alternative can be removed.{{cap}}{{exp}}",
             subset: "Unexpected useless alternative. This alternative is a strict subset of {{others}} and can be removed.{{cap}}{{exp}}",
+            nestedSubset:
+                "Unexpected useless element. All paths of {{root}} that go through {{nested}} are a strict subset of {{others}}. This element can be removed.{{cap}}{{exp}}",
             prefixSubset:
                 "Unexpected useless alternative. This alternative is already covered by {{others}} and can be removed.{{cap}}",
+            prefixNestedSubset:
+                "Unexpected useless element. All paths of {{root}} that go through {{nested}} are already covered by {{others}}. This element can be removed.{{cap}}",
             superset:
                 "Unexpected superset. This alternative is a superset of {{others}}. It might be possible to remove the other alternative(s).{{cap}}{{exp}}",
             overlap:
@@ -854,6 +1051,7 @@ export default createRule("no-dupe-disjunctions", {
                             switch (type) {
                                 case "Duplicate":
                                 case "Subset":
+                                case "NestedSubset":
                                     return true
 
                                 case "Overlap":
@@ -861,6 +1059,7 @@ export default createRule("no-dupe-disjunctions", {
                                     return reportExp
 
                                 case "PrefixSubset":
+                                case "PrefixNestedSubset":
                                     return reportPrefix
 
                                 default:
@@ -879,6 +1078,7 @@ export default createRule("no-dupe-disjunctions", {
                             switch (type) {
                                 case "Duplicate":
                                 case "Subset":
+                                case "NestedSubset":
                                     return true
 
                                 case "Overlap":
@@ -891,6 +1091,7 @@ export default createRule("no-dupe-disjunctions", {
                                     )
 
                                 case "PrefixSubset":
+                                case "PrefixNestedSubset":
                                     return reportPrefix
 
                                 default:
@@ -916,8 +1117,16 @@ export default createRule("no-dupe-disjunctions", {
                     exp = ""
                 }
 
+                const reportAlternative =
+                    result.type === "NestedSubset" ||
+                    result.type === "PrefixNestedSubset"
+                        ? result.nested
+                        : result.alternative
+
+                const loc = getRegexpLocation(reportAlternative)
+
                 const cap = hasSomeDescendant(
-                    result.alternative,
+                    reportAlternative,
                     (d) => d.type === "CapturingGroup",
                 )
                     ? " Careful! This alternative contains capturing groups which might be difficult to remove."
@@ -931,7 +1140,7 @@ export default createRule("no-dupe-disjunctions", {
                     case "Duplicate":
                         context.report({
                             node,
-                            loc: getRegexpLocation(result.alternative),
+                            loc,
                             messageId: "duplicate",
                             data: { exp, cap, others },
                         })
@@ -940,25 +1149,55 @@ export default createRule("no-dupe-disjunctions", {
                     case "Subset":
                         context.report({
                             node,
-                            loc: getRegexpLocation(result.alternative),
+                            loc,
                             messageId: "subset",
                             data: { exp, cap, others },
                         })
                         break
 
+                    case "NestedSubset":
+                        context.report({
+                            node,
+                            loc,
+                            messageId: "nestedSubset",
+                            data: {
+                                exp,
+                                cap,
+                                others,
+                                root: mention(result.alternative),
+                                nested: mentionNested(result.nested),
+                            },
+                        })
+                        break
+
                     case "PrefixSubset":
                         context.report({
                             node,
-                            loc: getRegexpLocation(result.alternative),
+                            loc,
                             messageId: "prefixSubset",
                             data: { exp, cap, others },
                         })
                         break
 
+                    case "PrefixNestedSubset":
+                        context.report({
+                            node,
+                            loc,
+                            messageId: "prefixNestedSubset",
+                            data: {
+                                exp,
+                                cap,
+                                others,
+                                root: mention(result.alternative),
+                                nested: mentionNested(result.nested),
+                            },
+                        })
+                        break
+
                     case "Superset":
                         context.report({
                             node,
-                            loc: getRegexpLocation(result.alternative),
+                            loc,
                             messageId: "superset",
                             data: { exp, cap, others },
                         })
@@ -967,7 +1206,7 @@ export default createRule("no-dupe-disjunctions", {
                     case "Overlap":
                         context.report({
                             node,
-                            loc: getRegexpLocation(result.alternative),
+                            loc,
                             messageId: "overlap",
                             data: {
                                 exp,
diff --git a/lib/utils/partial-parser.ts b/lib/utils/partial-parser.ts
new file mode 100644
index 0000000..ca9b05d
--- /dev/null
+++ b/lib/utils/partial-parser.ts
@@ -0,0 +1,239 @@
+import type {
+    Concatenation,
+    Element,
+    Expression,
+    NoParent,
+    CharRange,
+} from "refa"
+import { JS } from "refa"
+import type { AST } from "regexpp"
+
+export type NestedAlternative = AST.Alternative | AST.CharacterClassElement
+
+/**
+ * Throws if called.
+ */
+function assertNever(value: never): never {
+    throw new Error(`Invalid value: ${value}`)
+}
+
+class Context {
+    public readonly ancestors: ReadonlySet<AST.Node>
+
+    public readonly alternative: NestedAlternative
+
+    public constructor(alternative: NestedAlternative) {
+        this.alternative = alternative
+
+        const ancestors = new Set<AST.Node>()
+        for (let n: AST.Node | null = alternative; n; n = n.parent) {
+            ancestors.add(n)
+        }
+        this.ancestors = ancestors
+    }
+}
+
+export class PartialParser {
+    private readonly parser: JS.Parser
+
+    private readonly options: JS.ParseOptions
+
+    private readonly nativeCache = new WeakMap<
+        JS.ParsableElement,
+        NoParent<Element>
+    >()
+
+    public constructor(parser: JS.Parser, options: JS.ParseOptions = {}) {
+        this.parser = parser
+        this.options = options
+    }
+
+    /**
+     * Parses the given element while guaranteeing that all paths of the returned
+     * expression go through the given alternative.
+     */
+    public parse(
+        node: JS.ParsableElement,
+        alternative: NestedAlternative,
+    ): NoParent<Expression> {
+        switch (node.type) {
+            case "Pattern":
+                return {
+                    type: "Expression",
+                    alternatives: this.parseAlternatives(
+                        node.alternatives,
+                        new Context(alternative),
+                    ),
+                }
+
+            case "Alternative":
+                return {
+                    type: "Expression",
+                    alternatives: [
+                        this.parseAlternative(node, new Context(alternative)),
+                    ],
+                }
+
+            default:
+                return {
+                    type: "Expression",
+                    alternatives: [
+                        {
+                            type: "Concatenation",
+                            elements: [
+                                this.parseElement(
+                                    node,
+                                    new Context(alternative),
+                                ),
+                            ],
+                        },
+                    ],
+                }
+        }
+    }
+
+    private parseAlternatives(
+        alternatives: readonly AST.Alternative[],
+        context: Context,
+    ): NoParent<Concatenation>[] {
+        const ancestor = alternatives.find((a) => context.ancestors.has(a))
+        if (ancestor) {
+            return [this.parseAlternative(ancestor, context)]
+        }
+
+        const result: NoParent<Concatenation>[] = []
+        for (const a of alternatives) {
+            result.push(
+                ...this.parser.parseElement(a, this.options).expression
+                    .alternatives,
+            )
+        }
+        return result
+    }
+
+    private parseAlternative(
+        alternative: AST.Alternative,
+        context: Context,
+    ): NoParent<Concatenation> {
+        return {
+            type: "Concatenation",
+            elements: alternative.elements.map((e) =>
+                this.parseElement(e, context),
+            ),
+        }
+    }
+
+    private parseElement(
+        element: AST.Element,
+        context: Context,
+    ): NoParent<Element> {
+        if (!context.ancestors.has(element)) {
+            return this.nativeParseElement(element)
+        }
+
+        switch (element.type) {
+            case "Assertion":
+            case "Backreference":
+            case "Character":
+            case "CharacterSet":
+                return this.nativeParseElement(element)
+
+            case "CharacterClass":
+                return this.parseCharacterClass(element, context)
+
+            case "Group":
+            case "CapturingGroup":
+                return {
+                    type: "Alternation",
+                    alternatives: this.parseAlternatives(
+                        element.alternatives,
+                        context,
+                    ),
+                }
+
+            case "Quantifier": {
+                const alternatives: NoParent<Concatenation>[] =
+                    element.element.type === "Group" ||
+                    element.element.type === "CapturingGroup"
+                        ? this.parseAlternatives(
+                              element.element.alternatives,
+                              context,
+                          )
+                        : [
+                              {
+                                  type: "Concatenation",
+                                  elements: [
+                                      this.parseElement(
+                                          element.element,
+                                          context,
+                                      ),
+                                  ],
+                              },
+                          ]
+
+                return {
+                    type: "Quantifier",
+                    min: element.min,
+                    max: element.max,
+                    lazy: !element.greedy,
+                    alternatives,
+                }
+            }
+
+            default:
+                throw assertNever(element)
+        }
+    }
+
+    private parseCharacterClass(
+        cc: AST.CharacterClass,
+        context: Context,
+    ): NoParent<Element> {
+        if (
+            cc.negate ||
+            !context.ancestors.has(cc) ||
+            context.alternative.type === "Alternative"
+        ) {
+            return this.nativeParseElement(cc)
+        }
+
+        if (context.alternative.type === "CharacterClassRange") {
+            const range: CharRange = {
+                min: context.alternative.min.value,
+                max: context.alternative.max.value,
+            }
+            return {
+                type: "CharacterClass",
+                characters: JS.createCharSet([range], this.parser.ast.flags),
+            }
+        }
+        return this.nativeParseElement(context.alternative)
+    }
+
+    private nativeParseElement(element: JS.ParsableElement): NoParent<Element> {
+        let cached = this.nativeCache.get(element)
+        if (!cached) {
+            cached = this.nativeParseElementUncached(element)
+            this.nativeCache.set(element, cached)
+        }
+        return cached
+    }
+
+    private nativeParseElementUncached(
+        element: JS.ParsableElement,
+    ): NoParent<Element> {
+        const { expression } = this.parser.parseElement(element, this.options)
+
+        if (expression.alternatives.length === 1) {
+            const elements = expression.alternatives[0].elements
+            if (elements.length === 1) {
+                return elements[0]
+            }
+        }
+
+        return {
+            type: "Alternation",
+            alternatives: expression.alternatives,
+        }
+    }
+}


Test patch:
diff --git a/tests/lib/rules/no-dupe-disjunctions.ts b/tests/lib/rules/no-dupe-disjunctions.ts
index 628bf89..6d25f34 100644
--- a/tests/lib/rules/no-dupe-disjunctions.ts
+++ b/tests/lib/rules/no-dupe-disjunctions.ts
@@ -14,7 +14,6 @@ tester.run("no-dupe-disjunctions", rule as any, {
         `/a|b/`,
         `/(a|b)/`,
         `/(?:a|b)/`,
-        `/((?:ab|ba)|(?:ba|ac))/`,
         `/(?:js|json)$/`,
         `/(?:js|jso?n?)$/`,
         `/(?:js|json)abc/`,
@@ -22,6 +21,7 @@ tester.run("no-dupe-disjunctions", rule as any, {
         `/(?:yml|ya?ml)$/`,
         `/(?:yml|ya?ml)/`,
         `/<("[^"]*"|'[^']*'|[^'">])*>/g`,
+        `/c+|[a-f]/`,
         String.raw`/b+(?:\w+|[+-]?\d+)/`,
         String.raw`/A+_|A*_/`,
         String.raw`/(?:A+|A*)_/`,
@@ -391,6 +391,96 @@ tester.run("no-dupe-disjunctions", rule as any, {
                 "Unexpected useless alternative. This alternative is a strict subset of '\\d|[a-z]|_' and can be removed.",
             ],
         },
+        {
+            code: String(/((?:ab|ba)|(?:ba|ac))/),
+            errors: [
+                {
+                    message:
+                        "Unexpected useless element. All paths of '(?:ba|ac)' that go through 'ba' are a strict subset of '(?:ab|ba)'. This element can be removed.",
+                    column: 16,
+                },
+            ],
+        },
+        {
+            code: String(/a+|a|b|c/),
+            errors: [
+                {
+                    message:
+                        "Unexpected useless alternative. This alternative is a strict subset of 'a+' and can be removed.",
+                    column: 5,
+                },
+            ],
+        },
+        {
+            code: String(/a+|(?:a|b|c)/),
+            errors: [
+                {
+                    message:
+                        "Unexpected useless element. All paths of '(?:a|b|c)' that go through 'a' are a strict subset of 'a+'. This element can be removed.",
+                    column: 8,
+                },
+            ],
+        },
+        {
+            code: String(/a+|[abc]/),
+            errors: [
+                {
+                    message:
+                        "Unexpected useless element. All paths of '[abc]' that go through 'a' (U+0061) are a strict subset of 'a+'. This element can be removed.",
+                    column: 6,
+                },
+            ],
+        },
+        {
+            code: String(/a+|[a-c]/),
+            errors: [
+                {
+                    message:
+                        "Unexpected useless element. All paths of '[a-c]' that go through 'a' (U+0061) are a strict subset of 'a+'. This element can be removed.",
+                    column: 6,
+                },
+            ],
+        },
+        {
+            code: String(/a|aa|ba/),
+            errors: [
+                {
+                    message:
+                        "Unexpected useless alternative. This alternative is already covered by 'a' and can be removed.",
+                    column: 4,
+                },
+            ],
+        },
+        {
+            code: String(/a|(a|b)a/),
+            errors: [
+                {
+                    message:
+                        "Unexpected useless element. All paths of '(a|b)a' that go through 'a' are already covered by 'a'. This element can be removed.",
+                    column: 5,
+                },
+            ],
+        },
+        {
+            code: String(/a|(?:(a)|b)a/),
+            errors: [
+                {
+                    message:
+                        "Unexpected useless element. All paths of '(?:(a)|b)a' that go through '(a)' are already covered by 'a'. This element can be removed. Careful! This alternative contains capturing groups which might be difficult to remove.",
+                    column: 7,
+                },
+            ],
+        },
+        {
+            code: String(/a|[ab]a/),
+            errors: [
+                {
+                    message:
+                        "Unexpected useless element. All paths of '[ab]a' that go through 'a' (U+0061) are already covered by 'a'. This element can be removed.",
+                    column: 5,
+                },
+            ],
+        },
         {
             // reportExponentialBacktracking: 'potential'
             code: String.raw`