Repository: ota-meshi/eslint-plugin-regexp
Pull request #297 (2021-08-12 19:32:01)

Improve `sort-alternatives`
This improves `sort-alternatives` in 2 ways:

1) I added a few more punctuation characters to the list of allowed characters. This will allow the rule for sort more alternatives.

2) The rule can now sort slices. A slice is a continuous section of alternatives (basically the result of calling `alternatives.slice(start, end)`).

   One of the problems of this rule was that adding even one alternative containing forbidden characters meant that the whole group couldn't be sorted anymore. This has now been addressed using slices. Instead of trying to sort the group, the rule will now try to find slices within the group and sort those.

    This has the nice consequence that you can now add alternatives with forbidden characters to a group and most of the group will still be sortable.

   ![image](https://user-images.githubusercontent.com/20878432/129257475-12c60e36-e5c3-4733-8b3d-f9a5baa2a12c.png)



Patch:
diff --git a/lib/rules/match-any.ts b/lib/rules/match-any.ts
index f362dfe..9a312af 100644
--- a/lib/rules/match-any.ts
+++ b/lib/rules/match-any.ts
@@ -5,6 +5,7 @@ import type { RegExpContext } from "../utils"
 import { createRule, defineRegexpVisitor } from "../utils"
 import { isRegexpLiteral } from "../utils/ast-utils/utils"
 import { matchesAllCharacters } from "regexp-ast-analysis"
+import { mention } from "../utils/mention"
 
 const OPTION_SS1 = "[\\s\\S]" as const
 const OPTION_SS2 = "[\\S\\s]" as const
@@ -47,7 +48,7 @@ export default createRule("match-any", {
             },
         ],
         messages: {
-            unexpected: "Unexpected using '{{expr}}' to match any character.",
+            unexpected: "Unexpected using {{expr}} to match any character.",
         },
         type: "suggestion", // "problem",
     },
@@ -145,7 +146,7 @@ export default createRule("match-any", {
                             loc: getRegexpLocation(csNode),
                             messageId: "unexpected",
                             data: {
-                                expr: ".",
+                                expr: mention(csNode),
                             },
                             fix(fixer) {
                                 return fix(fixer, regexpContext, csNode)
@@ -163,7 +164,7 @@ export default createRule("match-any", {
                             loc: getRegexpLocation(ccNode),
                             messageId: "unexpected",
                             data: {
-                                expr: ccNode.raw,
+                                expr: mention(ccNode),
                             },
                             fix(fixer) {
                                 return fix(fixer, regexpContext, ccNode)
diff --git a/lib/rules/no-dupe-disjunctions.ts b/lib/rules/no-dupe-disjunctions.ts
index 5a35477..b20d3f8 100644
--- a/lib/rules/no-dupe-disjunctions.ts
+++ b/lib/rules/no-dupe-disjunctions.ts
@@ -30,6 +30,7 @@ import {
 import { RegExpParser } from "regexpp"
 import { UsageOfPattern } from "../utils/get-usage-of-pattern"
 import { canReorder } from "../utils/reorder-alternatives"
+import { mention } from "../utils/mention"
 
 type ParentNode = Group | CapturingGroup | Pattern | LookaroundAssertion
 
@@ -714,13 +715,13 @@ 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}}",
+                "Unexpected useless alternative. This alternative is a strict subset of {{others}} and can be removed.{{cap}}{{exp}}",
             prefixSubset:
-                "Unexpected useless alternative. This alternative is already covered by '{{others}}' and can be removed.{{cap}}",
+                "Unexpected useless alternative. This alternative is already covered by {{others}} and 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}}",
+                "Unexpected superset. This alternative is a superset of {{others}}. It might be possible to remove the other alternative(s).{{cap}}{{exp}}",
             overlap:
-                "Unexpected overlap. This alternative overlaps with '{{others}}'. The overlap is '{{expr}}'.{{cap}}{{exp}}",
+                "Unexpected overlap. This alternative overlaps with {{others}}. The overlap is {{expr}}.{{cap}}{{exp}}",
         },
         type: "suggestion", // "problem",
     },
@@ -936,7 +937,9 @@ export default createRule("no-dupe-disjunctions", {
                     ? " Careful! This alternative contains capturing groups which might be difficult to remove."
                     : ""
 
-                const others = result.others.map((a) => a.raw).join("|")
+                const others = mention(
+                    result.others.map((a) => a.raw).join("|"),
+                )
 
                 switch (result.type) {
                     case "Duplicate":
@@ -984,7 +987,9 @@ export default createRule("no-dupe-disjunctions", {
                                 exp,
                                 cap,
                                 others,
-                                expr: faToSource(result.overlap, flags),
+                                expr: mention(
+                                    faToSource(result.overlap, flags),
+                                ),
                             },
                         })
                         break
diff --git a/lib/rules/no-super-linear-backtracking.ts b/lib/rules/no-super-linear-backtracking.ts
index b2d655c..11c8f21 100644
--- a/lib/rules/no-super-linear-backtracking.ts
+++ b/lib/rules/no-super-linear-backtracking.ts
@@ -5,6 +5,7 @@ import { UsageOfPattern } from "../utils/get-usage-of-pattern"
 import type { ParsedLiteral } from "scslre"
 import { analyse } from "scslre"
 import type { Position, SourceLocation } from "estree"
+import { mention } from "../utils/mention"
 
 /**
  * Returns the combined source location of the two given locations.
@@ -74,11 +75,11 @@ export default createRule("no-super-linear-backtracking", {
         ],
         messages: {
             self:
-                "This quantifier can reach itself via the loop '{{parent}}'." +
+                "This quantifier can reach itself via the loop {{parent}}." +
                 " Using any string accepted by {{attack}}, this can be exploited to cause at least polynomial backtracking." +
                 "{{exp}}",
             trade:
-                "The quantifier '{{start}}' can exchange characters with '{{end}}'." +
+                "The quantifier {{start}} can exchange characters with {{end}}." +
                 " Using any string accepted by {{attack}}, this can be exploited to cause at least polynomial backtracking." +
                 "{{exp}}",
         },
@@ -134,7 +135,7 @@ export default createRule("no-super-linear-backtracking", {
                         data: {
                             exp,
                             attack,
-                            parent: report.parentQuant.raw,
+                            parent: mention(report.parentQuant),
                         },
                         fix,
                     })
@@ -149,8 +150,8 @@ export default createRule("no-super-linear-backtracking", {
                         data: {
                             exp,
                             attack,
-                            start: report.startQuant.raw,
-                            end: report.endQuant.raw,
+                            start: mention(report.startQuant),
+                            end: mention(report.endQuant),
                         },
                         fix,
                     })
diff --git a/lib/rules/no-useless-assertions.ts b/lib/rules/no-useless-assertions.ts
index df813d0..452178a 100644
--- a/lib/rules/no-useless-assertions.ts
+++ b/lib/rules/no-useless-assertions.ts
@@ -16,26 +16,27 @@ import {
     hasSomeDescendant,
     isPotentiallyEmpty,
 } from "regexp-ast-analysis"
+import { mention } from "../utils/mention"
 
 const messages = {
     alwaysRejectByChar:
-        "'{{assertion}}' will always reject because it is {{followedOrPreceded}} by a character.",
+        "{{assertion}} will always reject because it is {{followedOrPreceded}} by a character.",
     alwaysRejectByNonLineTerminator:
-        "'{{assertion}}' will always reject because it is {{followedOrPreceded}} by a non-line-terminator character.",
+        "{{assertion}} will always reject because it is {{followedOrPreceded}} by a non-line-terminator character.",
     alwaysAcceptByLineTerminator:
-        "'{{assertion}}' will always accept because it is {{followedOrPreceded}} by a line-terminator character.",
+        "{{assertion}} will always accept because it is {{followedOrPreceded}} by a line-terminator character.",
     alwaysAcceptOrRejectFollowedByWord:
-        "'{{assertion}}' will always {{acceptOrReject}} because it is preceded by a non-word character and followed by a word character.",
+        "{{assertion}} will always {{acceptOrReject}} because it is preceded by a non-word character and followed by a word character.",
     alwaysAcceptOrRejectFollowedByNonWord:
-        "'{{assertion}}' will always {{acceptOrReject}} because it is preceded by a non-word character and followed by a non-word character.",
+        "{{assertion}} will always {{acceptOrReject}} because it is preceded by a non-word character and followed by a non-word character.",
     alwaysAcceptOrRejectPrecededByWordFollowedByNonWord:
-        "'{{assertion}}' will always {{acceptOrReject}} because it is preceded by a word character and followed by a non-word character.",
+        "{{assertion}} will always {{acceptOrReject}} because it is preceded by a word character and followed by a non-word character.",
     alwaysAcceptOrRejectPrecededByWordFollowedByWord:
-        "'{{assertion}}' will always {{acceptOrReject}} because it is preceded by a word character and followed by a word character.",
+        "{{assertion}} will always {{acceptOrReject}} because it is preceded by a word character and followed by a word character.",
     alwaysForLookaround:
-        "The {{kind}} '{{assertion}}' will always {{acceptOrReject}}.",
+        "The {{kind}} {{assertion}} will always {{acceptOrReject}}.",
     alwaysForNegativeLookaround:
-        "The negative {{kind}} '{{assertion}}' will always {{acceptOrReject}}.",
+        "The negative {{kind}} {{assertion}} will always {{acceptOrReject}}.",
 }
 
 export default createRule("no-useless-assertions", {
@@ -72,7 +73,7 @@ export default createRule("no-useless-assertions", {
                     loc: getRegexpLocation(assertion),
                     messageId,
                     data: {
-                        assertion: assertion.raw,
+                        assertion: mention(assertion),
                         ...data,
                     },
                 })
diff --git a/lib/rules/no-useless-backreference.ts b/lib/rules/no-useless-backreference.ts
index 2ca89bc..84f5f3f 100644
--- a/lib/rules/no-useless-backreference.ts
+++ b/lib/rules/no-useless-backreference.ts
@@ -12,6 +12,7 @@ import {
     getMatchingDirection,
     isZeroLength,
 } from "regexp-ast-analysis"
+import { mention } from "../utils/mention"
 
 /**
  * Returns whether the list of ancestors from `from` to `to` contains a negated
@@ -89,17 +90,17 @@ export default createRule("no-useless-backreference", {
         schema: [],
         messages: {
             nested:
-                "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' from within that group.",
+                "Backreference {{ bref }} will be ignored. It references group {{ group }} from within that group.",
             forward:
-                "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which appears later in the pattern.",
+                "Backreference {{ bref }} will be ignored. It references group {{ group }} which appears later in the pattern.",
             backward:
-                "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which appears before in the same lookbehind.",
+                "Backreference {{ bref }} will be ignored. It references group {{ group }} which appears before in the same lookbehind.",
             disjunctive:
-                "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which is in another alternative.",
+                "Backreference {{ bref }} will be ignored. It references group {{ group }} which is in another alternative.",
             intoNegativeLookaround:
-                "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which is in a negative lookaround.",
+                "Backreference {{ bref }} will be ignored. It references group {{ group }} which is in a negative lookaround.",
             empty:
-                "Backreference '{{ bref }}' will be ignored. It references group '{{ group }}' which always captures zero characters.",
+                "Backreference {{ bref }} will be ignored. It references group {{ group }} which always captures zero characters.",
         },
         type: "suggestion", // "problem",
     },
@@ -121,8 +122,8 @@ export default createRule("no-useless-backreference", {
                             loc: getRegexpLocation(backRef),
                             messageId,
                             data: {
-                                bref: backRef.raw,
-                                group: backRef.resolved.raw,
+                                bref: mention(backRef),
+                                group: mention(backRef.resolved),
                             },
                         })
                     }
diff --git a/lib/rules/no-useless-lazy.ts b/lib/rules/no-useless-lazy.ts
index 0459baf..e6ca63b 100644
--- a/lib/rules/no-useless-lazy.ts
+++ b/lib/rules/no-useless-lazy.ts
@@ -94,7 +94,7 @@ export default createRule("no-useless-lazy", {
 
                     const matchingDir = getMatchingDirection(qNode)
                     const firstChar = getFirstConsumedChar(
-                        qNode,
+                        qNode.element,
                         matchingDir,
                         flags,
                     )
diff --git a/lib/rules/no-useless-non-capturing-group.ts b/lib/rules/no-useless-non-capturing-group.ts
index 76daafb..bf432c6 100644
--- a/lib/rules/no-useless-non-capturing-group.ts
+++ b/lib/rules/no-useless-non-capturing-group.ts
@@ -125,7 +125,7 @@ export default createRule("no-useless-non-capturing-group", {
 
                     context.report({
                         node,
-                        loc: getRegexpLocation(gNode),
+                        loc: getRegexpLocation(gNode, [0, 3]),
                         messageId: "unexpected",
                         fix: fixReplaceNode(gNode, () => {
                             if (
diff --git a/lib/rules/optimal-lookaround-quantifier.ts b/lib/rules/optimal-lookaround-quantifier.ts
index 3291357..295e9ff 100644
--- a/lib/rules/optimal-lookaround-quantifier.ts
+++ b/lib/rules/optimal-lookaround-quantifier.ts
@@ -3,6 +3,7 @@ import type { Alternative, LookaroundAssertion, Quantifier } from "regexpp/ast"
 import type { RegExpContext } from "../utils"
 import { createRule, defineRegexpVisitor } from "../utils"
 import { hasSomeDescendant } from "regexp-ast-analysis"
+import { mention } from "../utils/mention"
 
 /**
  * Extract invalid quantifiers for lookarounds
@@ -65,9 +66,9 @@ export default createRule("optimal-lookaround-quantifier", {
         schema: [],
         messages: {
             remove:
-                "The quantified expression '{{expr}}' at the {{endOrStart}} of the expression tree should only be matched a constant number of times. The expression can be removed without affecting the lookaround.",
+                "The quantified expression {{expr}} at the {{endOrStart}} of the expression tree should only be matched a constant number of times. The expression can be removed without affecting the lookaround.",
             replacedWith:
-                "The quantified expression '{{expr}}' at the {{endOrStart}} of the expression tree should only be matched a constant number of times. The expression can be replaced with {{replacer}} without affecting the lookaround.",
+                "The quantified expression {{expr}} at the {{endOrStart}} of the expression tree should only be matched a constant number of times. The expression can be replaced with {{replacer}} without affecting the lookaround.",
         },
         type: "problem",
     },
@@ -105,7 +106,7 @@ export default createRule("optimal-lookaround-quantifier", {
                                 messageId:
                                     q.min === 0 ? "remove" : "replacedWith",
                                 data: {
-                                    expr: q.raw,
+                                    expr: mention(q),
                                     endOrStart,
                                     replacer,
                                 },
diff --git a/lib/rules/optimal-quantifier-concatenation.ts b/lib/rules/optimal-quantifier-concatenation.ts
index e4c30fc..1e8e67a 100644
--- a/lib/rules/optimal-quantifier-concatenation.ts
+++ b/lib/rules/optimal-quantifier-concatenation.ts
@@ -19,6 +19,7 @@ import { createRule, defineRegexpVisitor, quantToString } from "../utils"
 import { Chars, hasSomeDescendant } from "regexp-ast-analysis"
 import { getPossiblyConsumedChar } from "../utils/regexp-ast"
 import type { CharSet } from "refa"
+import { mention } from "../utils/mention"
 
 /**
  * Returns whether the given node is or contains a capturing group.
@@ -511,17 +512,17 @@ export default createRule("optimal-quantifier-concatenation", {
         schema: [],
         messages: {
             combine:
-                "'{{left}}' and '{{right}}' can be combined into one quantifier '{{fix}}'.{{cap}}",
+                "{{left}} and {{right}} can be combined into one quantifier {{fix}}.{{cap}}",
             removeLeft:
-                "'{{left}}' can be removed because it is already included by '{{right}}'.{{cap}}",
+                "{{left}} can be removed because it is already included by {{right}}.{{cap}}",
             removeRight:
-                "'{{right}}' can be removed because it is already included by '{{left}}'.{{cap}}",
+                "{{right}} can be removed because it is already included by {{left}}.{{cap}}",
             replace:
-                "'{{left}}' and '{{right}}' can be replaced with '{{fix}}'.{{cap}}",
+                "{{left}} and {{right}} can be replaced with {{fix}}.{{cap}}",
             nestedRemove:
-                "'{{nested}}' can be removed because of '{{dominate}}'.{{cap}}",
+                "{{nested}} can be removed because of {{dominate}}.{{cap}}",
             nestedReplace:
-                "'{{nested}}' can be replaced with '{{fix}}' because of '{{dominate}}'.{{cap}}",
+                "{{nested}} can be replaced with {{fix}} because of {{dominate}}.{{cap}}",
         },
         type: "suggestion",
     },
@@ -562,9 +563,9 @@ export default createRule("optimal-quantifier-concatenation", {
                                 loc: getLoc(left, right, regexpContext),
                                 messageId: replacement.messageId,
                                 data: {
-                                    left: left.raw,
-                                    right: right.raw,
-                                    fix: replacement.raw,
+                                    left: mention(left),
+                                    right: mention(right),
+                                    fix: mention(replacement.raw),
                                     cap,
                                 },
                                 fix: fixReplaceNode(aNode, () => {
@@ -589,9 +590,9 @@ export default createRule("optimal-quantifier-concatenation", {
                                 loc: getRegexpLocation(replacement.nested),
                                 messageId: replacement.messageId,
                                 data: {
-                                    nested: replacement.nested.raw,
-                                    dominate: replacement.dominate.raw,
-                                    fix: replacement.raw,
+                                    nested: mention(replacement.nested),
+                                    dominate: mention(replacement.dominate),
+                                    fix: mention(replacement.raw),
                                     cap,
                                 },
                                 fix: fixReplaceNode(replacement.nested, () => {
diff --git a/lib/rules/prefer-d.ts b/lib/rules/prefer-d.ts
index 59e83a9..b045c4e 100644
--- a/lib/rules/prefer-d.ts
+++ b/lib/rules/prefer-d.ts
@@ -7,6 +7,7 @@ import {
     CP_DIGIT_NINE,
 } from "../utils"
 import { Chars } from "regexp-ast-analysis"
+import { mention } from "../utils/mention"
 
 export default createRule("prefer-d", {
     meta: {
@@ -19,7 +20,7 @@ export default createRule("prefer-d", {
         schema: [],
         messages: {
             unexpected:
-                "Unexpected {{type}} '{{expr}}'. Use '{{instead}}' instead.",
+                "Unexpected {{type}} {{expr}}. Use '{{instead}}' instead.",
         },
         type: "suggestion", // "problem",
     },
@@ -56,7 +57,7 @@ export default createRule("prefer-d", {
                             messageId: "unexpected",
                             data: {
                                 type: "character class",
-                                expr: ccNode.raw,
+                                expr: mention(ccNode),
                                 instead: predefined,
                             },
                             fix: fixReplaceNode(ccNode, predefined),
@@ -83,7 +84,7 @@ export default createRule("prefer-d", {
                             messageId: "unexpected",
                             data: {
                                 type: "character class range",
-                                expr: ccrNode.raw,
+                                expr: mention(ccrNode),
                                 instead,
                             },
                             fix: fixReplaceNode(ccrNode, instead),
diff --git a/lib/rules/prefer-question-quantifier.ts b/lib/rules/prefer-question-quantifier.ts
index 7c3dbce..0be48d4 100644
--- a/lib/rules/prefer-question-quantifier.ts
+++ b/lib/rules/prefer-question-quantifier.ts
@@ -1,3 +1,4 @@
+import { mention } from "../utils/mention"
 import type { Group, Quantifier } from "regexpp/ast"
 import type { RegExpVisitor } from "regexpp/visitor"
 import type { RegExpContext } from "../utils"
@@ -15,7 +16,7 @@ export default createRule("prefer-question-quantifier", {
         messages: {
             unexpected: "Unexpected quantifier '{{expr}}'. Use '?' instead.",
             unexpectedGroup:
-                "Unexpected group '{{expr}}'. Use '{{instead}}' instead.",
+                "Unexpected group {{expr}}. Use '{{instead}}' instead.",
         },
         type: "suggestion", // "problem",
     },
@@ -96,7 +97,7 @@ export default createRule("prefer-question-quantifier", {
                             loc: getRegexpLocation(reportNode),
                             messageId: "unexpectedGroup",
                             data: {
-                                expr: reportNode.raw,
+                                expr: mention(reportNode),
                                 instead,
                             },
                             fix: fixReplaceNode(reportNode, instead),
diff --git a/lib/rules/prefer-range.ts b/lib/rules/prefer-range.ts
index c9cae54..c3ee7b6 100644
--- a/lib/rules/prefer-range.ts
+++ b/lib/rules/prefer-range.ts
@@ -8,6 +8,7 @@ import {
     inRange,
 } from "../utils/char-ranges"
 import type { PatternReplaceRange } from "../utils/ast-utils/pattern-source"
+import { mention } from "../utils/mention"
 
 export default createRule("prefer-range", {
     meta: {
@@ -30,7 +31,7 @@ export default createRule("prefer-range", {
         ],
         messages: {
             unexpected:
-                "Unexpected multiple adjacent characters. Use '{{range}}' instead.",
+                "Unexpected multiple adjacent characters. Use {{range}} instead.",
         },
         type: "suggestion", // "problem",
     },
@@ -161,7 +162,7 @@ export default createRule("prefer-range", {
                                     node,
                                     loc: node.loc!,
                                     messageId: "unexpected",
-                                    data: { range: newText },
+                                    data: { range: mention(newText) },
                                 })
                                 continue
                             }
@@ -171,7 +172,7 @@ export default createRule("prefer-range", {
                                     node,
                                     loc: range.getAstLocation(sourceCode),
                                     messageId: "unexpected",
-                                    data: { range: newText },
+                                    data: { range: mention(newText) },
                                     fix: (fixer) => {
                                         return ranges.map((r, index) => {
                                             if (index === 0) {
diff --git a/lib/rules/prefer-t.ts b/lib/rules/prefer-t.ts
index a91fce8..171ec5d 100644
--- a/lib/rules/prefer-t.ts
+++ b/lib/rules/prefer-t.ts
@@ -1,3 +1,4 @@
+import { mention } from "../utils/mention"
 import type { RegExpVisitor } from "regexpp/visitor"
 import type { RegExpContext } from "../utils"
 import { createRule, defineRegexpVisitor, CP_TAB } from "../utils"
@@ -13,7 +14,7 @@ export default createRule("prefer-t", {
         fixable: "code",
         schema: [],
         messages: {
-            unexpected: "Unexpected character '{{expr}}'. Use '\\t' instead.",
+            unexpected: "Unexpected character {{expr}}. Use '\\t' instead.",
         },
         type: "suggestion", // "problem",
         deprecated: true,
@@ -39,7 +40,7 @@ export default createRule("prefer-t", {
                             loc: getRegexpLocation(cNode),
                             messageId: "unexpected",
                             data: {
-                                expr: cNode.raw,
+                                expr: mention(cNode),
                             },
                             fix: fixReplaceNode(cNode, "\\t"),
                         })
diff --git a/lib/rules/prefer-w.ts b/lib/rules/prefer-w.ts
index 1c08c0b..db66e15 100644
--- a/lib/rules/prefer-w.ts
+++ b/lib/rules/prefer-w.ts
@@ -14,6 +14,7 @@ import {
     CP_LOW_LINE,
 } from "../utils"
 import { Chars } from "regexp-ast-analysis"
+import { mention } from "../utils/mention"
 
 /**
  * Checks if small letter char class range
@@ -71,7 +72,7 @@ export default createRule("prefer-w", {
         schema: [],
         messages: {
             unexpected:
-                "Unexpected {{type}} '{{expr}}'. Use '{{instead}}' instead.",
+                "Unexpected {{type}} {{expr}}. Use '{{instead}}' instead.",
         },
         type: "suggestion", // "problem",
     },
@@ -105,7 +106,7 @@ export default createRule("prefer-w", {
                             messageId: "unexpected",
                             data: {
                                 type: "character class",
-                                expr: ccNode.raw,
+                                expr: mention(ccNode),
                                 instead: predefined,
                             },
                             fix: fixReplaceNode(ccNode, predefined),
@@ -156,9 +157,9 @@ export default createRule("prefer-w", {
                             messageId: "unexpected",
                             data: {
                                 type: "character class ranges",
-                                expr: `[${unexpectedElements
+                                expr: `'[${unexpectedElements
                                     .map((e) => e.raw)
-                                    .join("")}]`,
+                                    .join("")}]'`,
                                 instead: "\\w",
                             },
                             fix(fixer: Rule.RuleFixer) {
diff --git a/lib/rules/sort-alternatives.ts b/lib/rules/sort-alternatives.ts
index 8e6111a..7989c43 100644
--- a/lib/rules/sort-alternatives.ts
+++ b/lib/rules/sort-alternatives.ts
@@ -3,6 +3,10 @@ import type { Alternative, Element, Pattern } from "regexpp/ast"
 import type { RegExpContext } from "../utils"
 import {
     CP_MINUS,
+    CP_PLUS,
+    CP_STAR,
+    CP_QUESTION,
+    CP_SLASH,
     CP_SPACE,
     CP_APOSTROPHE,
     createRule,
@@ -16,7 +20,6 @@ import {
 } from "regexp-ast-analysis"
 import type { CharSet } from "refa"
 import { JS } from "refa"
-import type { SourceLocation, Position } from "estree"
 import { canReorder } from "../utils/reorder-alternatives"
 import { getPossiblyConsumedChar } from "../utils/regexp-ast"
 
@@ -36,8 +39,15 @@ function getAllowedChars(flags: ReadonlyFlags) {
                 [
                     { kind: "word", negate: false },
                     { min: CP_SPACE, max: CP_SPACE },
+
+                    // common punctuation and operators
+                    { min: CP_PLUS, max: CP_PLUS },
                     { min: CP_MINUS, max: CP_MINUS },
+                    { min: CP_STAR, max: CP_STAR },
+                    { min: CP_SLASH, max: CP_SLASH },
+
                     { min: CP_APOSTROPHE, max: CP_APOSTROPHE },
+                    { min: CP_QUESTION, max: CP_QUESTION },
                 ],
                 flags,
             ),
@@ -133,50 +143,12 @@ function isIntegerString(str: string): boolean {
  * are a number.
  */
 function trySortNumberAlternatives(alternatives: Alternative[]): void {
-    const numberRanges: [number, number][] = []
-    {
-        let start = 0
-        for (let i = 0; i < alternatives.length; i++) {
-            if (!isIntegerString(alternatives[i].raw)) {
-                if (start < i) {
-                    numberRanges.push([start, i])
-                }
-                start = i + 1
-            }
-        }
-        if (start < alternatives.length) {
-            numberRanges.push([start, alternatives.length])
-        }
-    }
-
-    for (const [start, end] of numberRanges) {
-        const slice = alternatives.slice(start, end)
-
-        slice.sort((a, b) => {
+    const runs = getRuns(alternatives, (a) => isIntegerString(a.raw))
+    for (const { startIndex, elements } of runs) {
+        elements.sort((a, b) => {
             return Number(a.raw) - Number(b.raw)
         })
-
-        alternatives.splice(start, end - start, ...slice)
-    }
-}
-
-/**
- * Returns the combined source location of the two given locations.
- */
-function unionLocations(a: SourceLocation, b: SourceLocation): SourceLocation {
-    /** x < y */
-    function less(x: Position, y: Position): boolean {
-        if (x.line < y.line) {
-            return true
-        } else if (x.line > y.line) {
-            return false
-        }
-        return x.column < y.column
-    }
-
-    return {
-        start: { ...(less(a.start, b.start) ? a.start : b.start) },
-        end: { ...(less(a.end, b.end) ? b.end : a.end) },
+        alternatives.splice(startIndex, elements.length, ...elements)
     }
 }
 
@@ -208,7 +180,7 @@ function getReorderingBounds<T>(
 }
 
 interface Run<T> {
-    index: number
+    startIndex: number
     elements: T[]
 }
 
@@ -226,7 +198,7 @@ function getRuns<T>(iter: Iterable<T>, condFn: (item: T) => boolean): Run<T>[] {
             elements.push(item)
         } else {
             if (elements.length > 0) {
-                runs.push({ index, elements })
+                runs.push({ startIndex: index - elements.length, elements })
                 elements = []
             }
         }
@@ -234,6 +206,11 @@ function getRuns<T>(iter: Iterable<T>, condFn: (item: T) => boolean): Run<T>[] {
         index++
     }
 
+    if (elements.length > 0) {
+        runs.push({ startIndex: index - elements.length, elements })
+        elements = []
+    }
+
     return runs
 }
 
@@ -253,6 +230,8 @@ export default createRule("sort-alternatives", {
         type: "suggestion", // "problem",
     },
     create(context) {
+        const sliceMinLength = 3
+
         /**
          * Create visitor
          */
@@ -268,23 +247,76 @@ export default createRule("sort-alternatives", {
 
             const allowedChars = getAllowedChars(flags)
 
+            const possibleCharsCache = new Map<Alternative, CharSet>()
+
+            /** A cached version of getPossiblyConsumedChar */
+            function getPossibleChars(a: Alternative): CharSet {
+                let chars = possibleCharsCache.get(a)
+                if (chars === undefined) {
+                    chars = getPossiblyConsumedChar(a, regexpContext).char
+                    possibleCharsCache.set(a, chars)
+                }
+                return chars
+            }
+
+            /** Tries to sort the given alternatives. */
+            function trySortRun(run: Run<Alternative>): void {
+                const alternatives = run.elements
+
+                if (canReorder(alternatives, regexpContext)) {
+                    // alternatives can be reordered freely
+                    sortAlternatives(alternatives, regexpContext)
+                    trySortNumberAlternatives(alternatives)
+                } else {
+                    const consumedChars = Chars.empty(flags).union(
+                        ...alternatives.map(getPossibleChars),
+                    )
+                    if (!consumedChars.isDisjointWith(Chars.digit(flags))) {
+                        // let's try to at least sort numbers
+                        const runs = getRuns(alternatives, (a) =>
+                            isIntegerString(a.raw),
+                        )
+
+                        for (const { startIndex: index, elements } of runs) {
+                            if (
+                                elements.length > 1 &&
+                                canReorder(elements, regexpContext)
+                            ) {
+                                trySortNumberAlternatives(elements)
+                                alternatives.splice(
+                                    index,
+                                    elements.length,
+                                    ...elements,
+                                )
+                            }
+                        }
+                    }
+                }
+
+                enforceSorted(run)
+            }
+
             /**
              * Creates a report if the sorted alternatives are different from
              * the unsorted ones.
              */
-            function enforceSorted(sorted: Alternative[]): void {
+            function enforceSorted(run: Run<Alternative>): void {
+                const sorted = run.elements
                 const parent = sorted[0].parent
-                const unsorted = parent.alternatives
+                const unsorted = parent.alternatives.slice(
+                    run.startIndex,
+                    run.startIndex + sorted.length,
+                )
 
                 const bounds = getReorderingBounds(unsorted, sorted)
                 if (!bounds) {
                     return
                 }
 
-                const loc = unionLocations(
-                    getRegexpLocation(unsorted[bounds[0]]),
-                    getRegexpLocation(unsorted[bounds[1]]),
-                )
+                const loc = getRegexpLocation({
+                    start: unsorted[bounds[0]].start,
+                    end: unsorted[bounds[1]].end,
+                })
 
                 context.report({
                     node,
@@ -293,11 +325,10 @@ export default createRule("sort-alternatives", {
                     fix: fixReplaceNode(parent, () => {
                         const prefix = parent.raw.slice(
                             0,
-                            parent.alternatives[0].start - parent.start,
+                            unsorted[0].start - parent.start,
                         )
                         const suffix = parent.raw.slice(
-                            parent.alternatives[parent.alternatives.length - 1]
-                                .end - parent.start,
+                            unsorted[unsorted.length - 1].end - parent.start,
                         )
 
                         return (
@@ -313,65 +344,46 @@ export default createRule("sort-alternatives", {
                     return
                 }
 
-                if (!containsOnlyLiterals(parent)) {
-                    return
-                }
-
-                if (
-                    hasSomeDescendant(
-                        parent,
-                        (d) => d !== parent && d.type === "CapturingGroup",
-                    )
-                ) {
-                    // it's not safe to reorder alternatives with capturing groups
-                    return
-                }
+                const runs = getRuns(parent.alternatives, (a) => {
+                    if (!containsOnlyLiterals(a)) {
+                        return false
+                    }
 
-                const consumedChars = getPossiblyConsumedChar(
-                    parent,
-                    regexpContext,
-                ).char
-                if (consumedChars.isEmpty) {
-                    // all alternatives are either empty or only contain
-                    // assertions
-                    return
-                }
-                if (!consumedChars.isSubsetOf(allowedChars.allowed)) {
-                    // contains some chars that are not allowed
-                    return
-                }
-                if (consumedChars.isDisjointWith(allowedChars.required)) {
-                    // doesn't contain required chars
-                    return
-                }
+                    const consumedChars = getPossibleChars(a)
+                    if (consumedChars.isEmpty) {
+                        // the alternative is either empty or only contains
+                        // assertions
+                        return false
+                    }
+                    if (!consumedChars.isSubsetOf(allowedChars.allowed)) {
+                        // contains some chars that are not allowed
+                        return false
+                    }
+                    if (consumedChars.isDisjointWith(allowedChars.required)) {
+                        // doesn't contain required chars
+                        return false
+                    }
 
-                const alternatives = [...parent.alternatives]
+                    return true
+                })
 
-                if (canReorder(alternatives, regexpContext)) {
-                    // alternatives can be reordered freely
-                    sortAlternatives(alternatives, regexpContext)
-                    trySortNumberAlternatives(alternatives)
-                } else if (!consumedChars.isDisjointWith(Chars.digit(flags))) {
-                    // let's try to at least sort numbers
-                    const runs = getRuns(alternatives, (a) =>
-                        isIntegerString(a.raw),
-                    )
-                    for (const { index, elements } of runs) {
+                if (
+                    runs.length === 1 &&
+                    runs[0].elements.length === parent.alternatives.length
+                ) {
+                    // All alternatives are to be sorted
+                    trySortRun(runs[0])
+                } else {
+                    // Some slices are to be sorted
+                    for (const run of runs) {
                         if (
-                            elements.length > 1 &&
-                            canReorder(elements, regexpContext)
+                            run.elements.length >= sliceMinLength &&
+                            run.elements.length >= 2
                         ) {
-                            trySortNumberAlternatives(elements)
-                            alternatives.splice(
-                                index,
-                                elements.length,
-                                ...elements,
-                            )
+                            trySortRun(run)
                         }
                     }
                 }
-
-                enforceSorted(alternatives)
             }
 
             return {
diff --git a/lib/rules/strict.ts b/lib/rules/strict.ts
index 5656ecd..7199942 100644
--- a/lib/rules/strict.ts
+++ b/lib/rules/strict.ts
@@ -1,3 +1,4 @@
+import { mention } from "../utils/mention"
 import { RegExpValidator } from "regexpp"
 import type { CharacterClassElement, Element } from "regexpp/ast"
 import type { RegExpVisitor } from "regexpp/visitor"
@@ -46,14 +47,14 @@ export default createRule("strict", {
             invalidControlEscape:
                 "Invalid or incomplete control escape sequence. Either use a valid control escape sequence or escaping the standalone backslash.",
             incompleteEscapeSequence:
-                "Incomplete escape sequence '{{expr}}'. Either use a valid escape sequence or remove the useless escaping.",
+                "Incomplete escape sequence {{expr}}. Either use a valid escape sequence or remove the useless escaping.",
             invalidPropertyEscape:
-                "Invalid property escape sequence '{{expr}}'. Either use a valid property escape sequence or remove the useless escaping.",
+                "Invalid property escape sequence {{expr}}. Either use a valid property escape sequence or remove the useless escaping.",
             incompleteBackreference:
-                "Incomplete backreference '{{expr}}'. Either use a valid backreference or remove the useless escaping.",
-            unescapedSourceCharacter: "Unescaped source character '{{expr}}'.",
+                "Incomplete backreference {{expr}}. Either use a valid backreference or remove the useless escaping.",
+            unescapedSourceCharacter: "Unescaped source character {{expr}}.",
             octalEscape:
-                "Invalid legacy octal escape sequence '{{expr}}'. Use a hexadecimal escape instead.",
+                "Invalid legacy octal escape sequence {{expr}}. Use a hexadecimal escape instead.",
             uselessEscape:
                 "Useless identity escapes with non-syntax characters are forbidden.",
 
@@ -111,7 +112,7 @@ export default createRule("strict", {
                         loc: getRegexpLocation(element),
                         messageId,
                         data: {
-                            expr: element.raw,
+                            expr: mention(element),
                         },
                         suggest: [
                             {
@@ -126,7 +127,7 @@ export default createRule("strict", {
                         loc: getRegexpLocation(element),
                         messageId,
                         data: {
-                            expr: element.raw,
+                            expr: mention(element),
                         },
                         fix: fix ? fixReplaceNode(element, fix) : null,
                     })
diff --git a/lib/utils/mention.ts b/lib/utils/mention.ts
index efa8da0..7b8f698 100644
--- a/lib/utils/mention.ts
+++ b/lib/utils/mention.ts
@@ -28,8 +28,8 @@ export function mentionChar(element: CharacterClassElement): string {
 /**
  * Creates a string that mentions the given character class element.
  */
-export function mention(element: Node): string {
-    return `'${escape(element.raw)}'`
+export function mention(element: Node | string): string {
+    return `'${escape(typeof element === "string" ? element : element.raw)}'`
 }
 
 /** Escape control characters in the given string */
diff --git a/package-lock.json b/package-lock.json
index a27ee46..8b8fd09 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13226,9 +13226,9 @@
             }
         },
         "regexp-ast-analysis": {
-            "version": "0.2.3",
-            "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.2.3.tgz",
-            "integrity": "sha512-K/6d90dP1DI68HHdHORj9vVxV5oWkLB8F7LeGs70EdA/h5BIqkrK8SdGy+O9Ii+AVVPprF0Ia6f5UwgI+ahIcw==",
+            "version": "0.2.4",
+            "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.2.4.tgz",
+            "integrity": "sha512-8L7kOZQaKPxKKAwGuUZxTQtlO3WZ+tiXy4s6G6PKL6trbOXcZoumwC3AOHHFtI/xoSbNxt7jgLvCnP1UADLWqg==",
             "requires": {
                 "refa": "^0.9.0",
                 "regexpp": "^3.2.0"
diff --git a/package.json b/package.json
index 4ee8279..1c34d55 100644
--- a/package.json
+++ b/package.json
@@ -95,7 +95,7 @@
         "eslint-utils": "^3.0.0",
         "jsdoctypeparser": "^9.0.0",
         "refa": "^0.9.0",
-        "regexp-ast-analysis": "^0.2.3",
+        "regexp-ast-analysis": "^0.2.4",
         "regexpp": "^3.2.0",
         "scslre": "^0.1.6"
     }


Test patch:
diff --git a/tests/lib/rules/no-useless-lazy.ts b/tests/lib/rules/no-useless-lazy.ts
index 9d6029a..58e35c6 100644
--- a/tests/lib/rules/no-useless-lazy.ts
+++ b/tests/lib/rules/no-useless-lazy.ts
@@ -72,6 +72,11 @@ tester.run("no-useless-lazy", rule as any, {
             output: `/a+b+/`,
             errors: [{ messageId: "possessive" }],
         },
+        {
+            code: `/a*?b+/`,
+            output: `/a*b+/`,
+            errors: [{ messageId: "possessive" }],
+        },
         {
             code: `/(?:a|cd)+?(?:b+|zzz)/`,
             output: `/(?:a|cd)+(?:b+|zzz)/`,
diff --git a/tests/lib/rules/no-useless-non-capturing-group.ts b/tests/lib/rules/no-useless-non-capturing-group.ts
index 1f04a44..6ea70ed 100644
--- a/tests/lib/rules/no-useless-non-capturing-group.ts
+++ b/tests/lib/rules/no-useless-non-capturing-group.ts
@@ -87,7 +87,7 @@ tester.run("no-useless-non-capturing-group", rule as any, {
                     line: 1,
                     column: 2,
                     endLine: 1,
-                    endColumn: 10,
+                    endColumn: 5,
                 },
             ],
         },
@@ -100,7 +100,7 @@ tester.run("no-useless-non-capturing-group", rule as any, {
                     line: 1,
                     column: 2,
                     endLine: 1,
-                    endColumn: 12,
+                    endColumn: 5,
                 },
             ],
         },
diff --git a/tests/lib/rules/sort-alternatives.ts b/tests/lib/rules/sort-alternatives.ts
index 19e27c9..9650bcb 100644
--- a/tests/lib/rules/sort-alternatives.ts
+++ b/tests/lib/rules/sort-alternatives.ts
@@ -9,7 +9,7 @@ const tester = new RuleTester({
 })
 
 tester.run("sort-alternatives", rule as any, {
-    valid: [String.raw`/\b(?:a|\d+|c|b)\b/`],
+    valid: [String.raw`/\b(?:a|\d+|c|b)\b/`, String.raw`/\b(?:\^|c|b)\b/`],
     invalid: [
         {
             code: String.raw`/c|b|a/`,
@@ -103,5 +103,15 @@ tester.run("sort-alternatives", rule as any, {
                 "The alternatives of this group can be sorted without affecting the regex.",
             ],
         },
+
+        // sorting slices
+
+        {
+            code: String.raw`/\b(?:\^|c|b|a)\b/`,
+            output: String.raw`/\b(?:\^|a|b|c)\b/`,
+            errors: [
+                "The alternatives of this group can be sorted without affecting the regex.",
+            ],
+        },
     ],
 })