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

Issue:
More detailed control over the `prefer-quantifier' rule
Good day,

I have a suggestion regarding the behavior of rule [`prefer-quantifier`](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-quantifier.html).

I definitely agree that `/\d{4}-\d{2}-\d{2}/` is much better than `/\d\d\d\d-\d\d-\d\d`. 
However, at the same time, I find the `www` version is clearer and easier to understand than `w{3}`:

```ts
// Removes protocol, www, and trailing slash
url.replaceAll(/^(https?:)?\/\/(www\.)?|\/$/g, '')
url.replaceAll(/^(https?:)?\/\/(w{3}\.)?|\/$/g, '')
```

So I'd like to suggest a few possible options:

1) Add an option to validate only certain character types, such as digits and symbols - for example an open property like `validate: ['digit', 'letter', 'symbol']`. \
If I understand the [implementation](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/prefer-quantifier.ts) correctly, this would be the easiest option to add and wouldn't introduce any breaking changes.
2) Introduce an explicit whitelist option. \
  I imagine it would be fairly limited in scope, but still useful in specific cases.
3) Increase the validation threshold for letters from 2 to 3. \
  This feels like a weaker solution, but could still help in edge cases like `www`

What do you think?

I can do it myself if this makes sense to you - let me know if this is needed

Pull request description:
feat: Add `allows` option to `prefer-quantifier` rule
Closes #834 

Patch:
diff --git a/.changeset/chilled-doors-wave.md b/.changeset/chilled-doors-wave.md
new file mode 100644
index 0000000..b37690a
--- /dev/null
+++ b/.changeset/chilled-doors-wave.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": minor
+---
+
+Add `allows` option to `prefer-quantifier` rule
diff --git a/docs/rules/prefer-quantifier.md b/docs/rules/prefer-quantifier.md
index d2f2c2b..d889ec7 100644
--- a/docs/rules/prefer-quantifier.md
+++ b/docs/rules/prefer-quantifier.md
@@ -33,7 +33,33 @@ var foo = /\d\d\d\d-\d\d-\d\d/;
 
 ## :wrench: Options
 
-Nothing.
+```json
+{
+  "regexp/prefer-quantifier": ["error", {
+    "allows": ["www", "\\d\\d"]
+  }]
+}
+```
+
+- `"allows"` ... Array of allowed patterns.
+
+### `{ "allows": ["www", "\\d\\d"] }`
+
+<eslint-code-block fix>
+
+```js
+/* eslint regexp/prefer-quantifier: ["error", { "allows": ["www", "\\d\\d"] }] */
+
+/* ✓ GOOD */
+var foo = /(?:www\.)?(.*)/;
+var foo = /\d\d:\d\d/;
+
+/* ✗ BAD */
+var foo = /wwww/;
+var foo = /\d\d\d\d/;
+```
+
+</eslint-code-block>
 
 ## :rocket: Version
 
diff --git a/lib/rules/prefer-quantifier.ts b/lib/rules/prefer-quantifier.ts
index 2074f8d..134bbd5 100644
--- a/lib/rules/prefer-quantifier.ts
+++ b/lib/rules/prefer-quantifier.ts
@@ -1,5 +1,6 @@
 import type { Character, CharacterSet } from "@eslint-community/regexpp/ast"
 import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
+import type { ObjectOption } from "../types"
 import type { RegExpContext } from "../utils"
 import {
     createRule,
@@ -22,9 +23,12 @@ class CharBuffer {
 
     public equalChar: (element: CharTarget) => boolean
 
-    public constructor(target: CharTarget) {
+    private readonly allows: string[]
+
+    public constructor(target: CharTarget, allows: string[]) {
         this.target = target
         this.elements = [target]
+        this.allows = allows
 
         this.times = 1
 
@@ -57,10 +61,18 @@ class CharBuffer {
         this.times += 1
     }
 
+    private get bufferRawContents() {
+        return this.elements.reduce((acc, element) => acc + element.raw, "")
+    }
+
     public isValid(): boolean {
         if (this.elements.length < 2) {
             return true
         }
+        if (this.allows.includes(this.bufferRawContents)) {
+            return true
+        }
+
         let charKind: "digit" | "letter" | "symbol" | null = null
         for (const element of this.elements) {
             if (element.type === "Character") {
@@ -105,7 +117,20 @@ export default createRule("prefer-quantifier", {
             recommended: false,
         },
         fixable: "code",
-        schema: [],
+        schema: [
+            {
+                type: "object",
+                properties: {
+                    allows: {
+                        type: "array",
+                        items: {
+                            type: "string",
+                        },
+                    },
+                },
+                additionalProperties: false,
+            },
+        ],
         messages: {
             unexpected:
                 "Unexpected consecutive same {{type}}. Use '{{quantifier}}' instead.",
@@ -113,6 +138,9 @@ export default createRule("prefer-quantifier", {
         type: "suggestion", // "problem",
     },
     create(context) {
+        const allows: string[] =
+            (context.options[0] as ObjectOption)?.allows ?? []
+
         function createVisitor({
             node,
             patternSource,
@@ -129,7 +157,7 @@ export default createRule("prefer-quantifier", {
                                 charBuffer.addElement(element)
                             } else {
                                 validateBuffer(charBuffer)
-                                charBuffer = new CharBuffer(element)
+                                charBuffer = new CharBuffer(element, allows)
                             }
                         } else {
                             validateBuffer(charBuffer)


Test patch:
diff --git a/tests/lib/rules/__snapshots__/prefer-quantifier.ts.eslintsnap b/tests/lib/rules/__snapshots__/prefer-quantifier.ts.eslintsnap
index 6ddb64f..c1f3046 100644
--- a/tests/lib/rules/__snapshots__/prefer-quantifier.ts.eslintsnap
+++ b/tests/lib/rules/__snapshots__/prefer-quantifier.ts.eslintsnap
@@ -162,3 +162,35 @@ Output: unchanged
 
 [1] Unexpected consecutive same character class escapes. Use '{2}' instead.
 ---
+
+
+Test: prefer-quantifier >> invalid
+Options:
+  - allows:
+      - www
+
+Code:
+  1 | /wwww/
+    |  ^~~~ [1]
+
+Output:
+  1 | /w{4}/
+
+[1] Unexpected consecutive same characters. Use '{4}' instead.
+---
+
+
+Test: prefer-quantifier >> invalid
+Options:
+  - allows:
+      - \d\d
+
+Code:
+  1 | /\d\d-\d\d\d\d/
+    |       ^~~~~~~~ [1]
+
+Output:
+  1 | /\d\d-\d{4}/
+
+[1] Unexpected consecutive same character class escapes. Use '{4}' instead.
+---
diff --git a/tests/lib/rules/prefer-quantifier.ts b/tests/lib/rules/prefer-quantifier.ts
index 441f557..17d1038 100644
--- a/tests/lib/rules/prefer-quantifier.ts
+++ b/tests/lib/rules/prefer-quantifier.ts
@@ -19,6 +19,14 @@ tester.run("prefer-quantifier", rule as any, {
         `/---/`,
         `/a.{1,3}?.{2,4}c/`,
         `/a.{1,3}.{2,4}?c/`,
+        {
+            code: `/www/`,
+            options: [{ allows: ["www"] }],
+        },
+        {
+            code: `/\\d\\d/`,
+            options: [{ allows: [`\\d\\d`] }],
+        },
     ],
     invalid: [
         `/\\d\\d/`,
@@ -39,5 +47,13 @@ tester.run("prefer-quantifier", rule as any, {
             const s = "\\\\d"+"\\\\d"
             new RegExp(s)
             `,
+        {
+            code: `/wwww/`,
+            options: [{ allows: ["www"] }],
+        },
+        {
+            code: String.raw`/\d\d-\d\d\d\d/`,
+            options: [{ allows: [`\\d\\d`] }],
+        },
     ],
 })