Repository: ota-meshi/eslint-plugin-regexp
Pull request #632 (2023-10-01 23:52:52)

Add `regexp/no-empty-string-literal` rule
This PR adds the rule suggested in the comments at the following link:

https://github.com/ota-meshi/eslint-plugin-regexp/issues/545#issuecomment-1742008503

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: ace75c8414ed386ed8dc2167011cb1e8a5d3e323

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/ota-meshi/eslint-plugin-regexp/new/no-empty-string-literal?filename=.changeset/hungry-eels-check.md&value=---%0A%22eslint-plugin-regexp%22%3A%20patch%0A---%0A%0AAdd%20%60regexp%2Fno-empty-string-literal%60%20rule%0A)



Patch:
diff --git a/.changeset/cold-nails-teach.md b/.changeset/cold-nails-teach.md
new file mode 100644
index 0000000..f46eec0
--- /dev/null
+++ b/.changeset/cold-nails-teach.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": minor
+---
+
+Add support for string literal to `regexp/no-empty-alternative`
diff --git a/.changeset/hungry-eels-check.md b/.changeset/hungry-eels-check.md
new file mode 100644
index 0000000..81d237a
--- /dev/null
+++ b/.changeset/hungry-eels-check.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": major
+---
+
+Add `regexp/no-empty-string-literal` rule
diff --git a/README.md b/README.md
index 95afc5c..bbc6e1a 100644
--- a/README.md
+++ b/README.md
@@ -140,6 +140,7 @@ The `plugin:regexp/all` config enables all rules. It's meant for testing, not fo
 | [control-character-escape](https://ota-meshi.github.io/eslint-plugin-regexp/rules/control-character-escape.html)                           | enforce consistent escaping of control characters                                          | ✅  |    | 🔧 |    |
 | [negation](https://ota-meshi.github.io/eslint-plugin-regexp/rules/negation.html)                                                           | enforce use of escapes on negation                                                         | ✅  |    | 🔧 |    |
 | [no-dupe-characters-character-class](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-dupe-characters-character-class.html)       | disallow duplicate characters in the RegExp character class                                | ✅  |    | 🔧 |    |
+| [no-empty-string-literal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-empty-string-literal.html)                             | disallow empty string literals in character classes                                        | ✅  |    |    |    |
 | [no-extra-lookaround-assertions](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-extra-lookaround-assertions.html)               | disallow unnecessary nested lookaround assertions                                          | ✅  |    | 🔧 |    |
 | [no-invisible-character](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-invisible-character.html)                               | disallow invisible raw character                                                           | ✅  |    | 🔧 |    |
 | [no-legacy-features](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-legacy-features.html)                                       | disallow legacy RegExp features                                                            | ✅  |    |    |    |
diff --git a/docs/rules/index.md b/docs/rules/index.md
index 2392df2..293ca80 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -47,6 +47,7 @@ sidebarDepth: 0
 | [control-character-escape](control-character-escape.md)                           | enforce consistent escaping of control characters                                          | ✅  |    | 🔧 |    |
 | [negation](negation.md)                                                           | enforce use of escapes on negation                                                         | ✅  |    | 🔧 |    |
 | [no-dupe-characters-character-class](no-dupe-characters-character-class.md)       | disallow duplicate characters in the RegExp character class                                | ✅  |    | 🔧 |    |
+| [no-empty-string-literal](no-empty-string-literal.md)                             | disallow empty string literals in character classes                                        | ✅  |    |    |    |
 | [no-extra-lookaround-assertions](no-extra-lookaround-assertions.md)               | disallow unnecessary nested lookaround assertions                                          | ✅  |    | 🔧 |    |
 | [no-invisible-character](no-invisible-character.md)                               | disallow invisible raw character                                                           | ✅  |    | 🔧 |    |
 | [no-legacy-features](no-legacy-features.md)                                       | disallow legacy RegExp features                                                            | ✅  |    |    |    |
diff --git a/docs/rules/no-empty-alternative.md b/docs/rules/no-empty-alternative.md
index eb4b10c..ab77a2c 100644
--- a/docs/rules/no-empty-alternative.md
+++ b/docs/rules/no-empty-alternative.md
@@ -37,6 +37,7 @@ var foo = /a+|b*/
 var foo = /a+|b+|/
 var foo = /\|\||\|||\|\|\|/
 var foo = /a(?:a|bc|def|h||ij|k)/
+var foo = /[abc\q{def|}]/v
 ```
 
 </eslint-code-block>
diff --git a/docs/rules/no-empty-string-literal.md b/docs/rules/no-empty-string-literal.md
new file mode 100644
index 0000000..3a472eb
--- /dev/null
+++ b/docs/rules/no-empty-string-literal.md
@@ -0,0 +1,59 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "regexp/no-empty-string-literal"
+description: "disallow empty string literals in character classes"
+---
+# regexp/no-empty-string-literal
+
+💼 This rule is enabled in the ✅ `plugin:regexp/recommended` config.
+
+<!-- end auto-generated rule header -->
+
+> disallow empty string literals in character classes
+
+## :book: Rule Details
+
+This rule reports empty string literals in character classes.
+
+If the empty string literal is supposed to match the empty string, then use a
+quantifier instead. For example, `[ab\q{}]` should be written as `[ab]?`.
+
+This rule does not report empty alternatives in string literals. (e.g. `/[\q{a|}]/v`)\
+If you want to report empty alternatives in string literals, use the [regexp/no-empty-alternative] rule.
+
+<eslint-code-block>
+
+```js
+/* eslint regexp/no-empty-string-literal: "error" */
+
+/* ✓ GOOD */
+var foo = /[\q{a}]/v;
+var foo = /[\q{abc}]/v;
+var foo = /[\q{a|}]/v;
+
+/* ✗ BAD */
+var foo = /[\q{}]/v;
+var foo = /[\q{|}]/v;
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :couple: Related rules
+
+- [regexp/no-empty-alternative]
+
+[regexp/no-empty-alternative]: ./no-empty-alternative.md
+
+## :rocket: Version
+
+:exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-empty-string-literal.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-empty-string-literal.ts)
diff --git a/lib/configs/recommended.ts b/lib/configs/recommended.ts
index 2d1d261..f201495 100644
--- a/lib/configs/recommended.ts
+++ b/lib/configs/recommended.ts
@@ -23,6 +23,7 @@ export const rules = {
     "regexp/no-empty-character-class": "error",
     "regexp/no-empty-group": "error",
     "regexp/no-empty-lookarounds-assertion": "error",
+    "regexp/no-empty-string-literal": "error",
     "regexp/no-escape-backspace": "error",
     "regexp/no-extra-lookaround-assertions": "error",
     "regexp/no-invalid-regexp": "error",
diff --git a/lib/rules/no-empty-alternative.ts b/lib/rules/no-empty-alternative.ts
index 67c794f..f831b53 100644
--- a/lib/rules/no-empty-alternative.ts
+++ b/lib/rules/no-empty-alternative.ts
@@ -2,6 +2,7 @@ import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
 import type {
     Alternative,
     CapturingGroup,
+    ClassStringDisjunction,
     Group,
     Pattern,
 } from "@eslint-community/regexpp/ast"
@@ -73,8 +74,15 @@ export default createRule("no-empty-alternative", {
             getRegexpLocation,
             fixReplaceNode,
         }: RegExpContext): RegExpVisitor.Handlers {
-            function verifyAlternatives(
-                regexpNode: CapturingGroup | Group | Pattern,
+            function verifyAlternatives<
+                N extends
+                    | CapturingGroup
+                    | Group
+                    | Pattern
+                    | ClassStringDisjunction,
+            >(
+                regexpNode: N,
+                suggestFixer: (alt: N["alternatives"][number]) => string | null,
             ) {
                 if (regexpNode.alternatives.length >= 2) {
                     // We want to have at least two alternatives because the zero alternatives isn't possible because of
@@ -96,7 +104,7 @@ export default createRule("no-empty-alternative", {
                                       end: index + 1,
                                   })
 
-                            const fixed = getFixedNode(regexpNode, alt)
+                            const fixed = suggestFixer(alt)
 
                             context.report({
                                 node,
@@ -122,9 +130,20 @@ export default createRule("no-empty-alternative", {
             }
 
             return {
-                onGroupEnter: verifyAlternatives,
-                onCapturingGroupEnter: verifyAlternatives,
-                onPatternEnter: verifyAlternatives,
+                onGroupEnter: (gNode) =>
+                    verifyAlternatives(gNode, (alt) =>
+                        getFixedNode(gNode, alt),
+                    ),
+                onCapturingGroupEnter: (cgNode) =>
+                    verifyAlternatives(cgNode, (alt) =>
+                        getFixedNode(cgNode, alt),
+                    ),
+                onPatternEnter: (pNode) =>
+                    verifyAlternatives(pNode, (alt) =>
+                        getFixedNode(pNode, alt),
+                    ),
+                onClassStringDisjunctionEnter: (csdNode) =>
+                    verifyAlternatives(csdNode, () => null),
             }
         }
 
diff --git a/lib/rules/no-empty-string-literal.ts b/lib/rules/no-empty-string-literal.ts
new file mode 100644
index 0000000..65c8b9c
--- /dev/null
+++ b/lib/rules/no-empty-string-literal.ts
@@ -0,0 +1,45 @@
+import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
+import type { RegExpContext } from "../utils"
+import { createRule, defineRegexpVisitor } from "../utils"
+
+export default createRule("no-empty-string-literal", {
+    meta: {
+        docs: {
+            description: "disallow empty string literals in character classes",
+            category: "Best Practices",
+            recommended: true,
+        },
+        schema: [],
+        messages: {
+            unexpected: "Unexpected empty string literal.",
+        },
+        type: "suggestion", // "problem",
+    },
+    create(context) {
+        function createVisitor(
+            regexpContext: RegExpContext,
+        ): RegExpVisitor.Handlers {
+            const { node, getRegexpLocation } = regexpContext
+
+            return {
+                onClassStringDisjunctionEnter(csdNode) {
+                    if (
+                        csdNode.alternatives.every(
+                            (alt) => alt.elements.length === 0,
+                        )
+                    ) {
+                        context.report({
+                            node,
+                            loc: getRegexpLocation(csdNode),
+                            messageId: "unexpected",
+                        })
+                    }
+                },
+            }
+        }
+
+        return defineRegexpVisitor(context, {
+            createVisitor,
+        })
+    },
+})
diff --git a/lib/utils/rules.ts b/lib/utils/rules.ts
index 05c9f39..ecc1cdc 100644
--- a/lib/utils/rules.ts
+++ b/lib/utils/rules.ts
@@ -15,6 +15,7 @@ import noEmptyCapturingGroup from "../rules/no-empty-capturing-group"
 import noEmptyCharacterClass from "../rules/no-empty-character-class"
 import noEmptyGroup from "../rules/no-empty-group"
 import noEmptyLookaroundsAssertion from "../rules/no-empty-lookarounds-assertion"
+import noEmptyStringLiteral from "../rules/no-empty-string-literal"
 import noEscapeBackspace from "../rules/no-escape-backspace"
 import noExtraLookaroundAssertions from "../rules/no-extra-lookaround-assertions"
 import noInvalidRegexp from "../rules/no-invalid-regexp"
@@ -99,6 +100,7 @@ export const rules = [
     noEmptyCharacterClass,
     noEmptyGroup,
     noEmptyLookaroundsAssertion,
+    noEmptyStringLiteral,
     noEscapeBackspace,
     noExtraLookaroundAssertions,
     noInvalidRegexp,


Test patch:
diff --git a/tests/lib/rules/no-empty-alternative.ts b/tests/lib/rules/no-empty-alternative.ts
index 010d8e7..7cfc9b2 100644
--- a/tests/lib/rules/no-empty-alternative.ts
+++ b/tests/lib/rules/no-empty-alternative.ts
@@ -3,13 +3,13 @@ import rule from "../../../lib/rules/no-empty-alternative"
 
 const tester = new RuleTester({
     parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: "latest",
         sourceType: "module",
     },
 })
 
 tester.run("no-empty-alternative", rule as any, {
-    valid: [`/()|(?:)|(?=)/`, `/(?:)/`, `/a*|b+/`],
+    valid: [`/()|(?:)|(?=)/`, `/(?:)/`, `/a*|b+/`, String.raw`/[\q{a|b}]/v`],
     invalid: [
         {
             code: `/|||||/`,
@@ -68,5 +68,35 @@ tester.run("no-empty-alternative", rule as any, {
                 },
             ],
         },
+        {
+            code: String.raw`/[\q{a|}]/v`,
+            errors: [
+                {
+                    messageId: "empty",
+                    column: 7,
+                    suggestions: [],
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\q{|a}]/v`,
+            errors: [
+                {
+                    messageId: "empty",
+                    column: 6,
+                    suggestions: [],
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\q{a||b}]/v`,
+            errors: [
+                {
+                    messageId: "empty",
+                    column: 8,
+                    suggestions: [],
+                },
+            ],
+        },
     ],
 })
diff --git a/tests/lib/rules/no-empty-string-literal.ts b/tests/lib/rules/no-empty-string-literal.ts
new file mode 100644
index 0000000..85b852c
--- /dev/null
+++ b/tests/lib/rules/no-empty-string-literal.ts
@@ -0,0 +1,42 @@
+import { RuleTester } from "eslint"
+import rule from "../../../lib/rules/no-empty-string-literal"
+
+const tester = new RuleTester({
+    parserOptions: {
+        ecmaVersion: "latest",
+        sourceType: "module",
+    },
+})
+
+tester.run("no-empty-string-literal", rule as any, {
+    valid: [
+        String.raw`/[\q{a}]/v`,
+        String.raw`/[\q{abc}]/v`,
+        String.raw`/[\q{a|}]/v`,
+        String.raw`/[\q{abc|}]/v`,
+        String.raw`/[\q{|a}]/v`,
+        String.raw`/[\q{|abc}]/v`,
+    ],
+    invalid: [
+        {
+            code: String.raw`/[\q{}]/v`,
+            errors: [
+                {
+                    message: "Unexpected empty string literal.",
+                    column: 3,
+                    endColumn: 7,
+                },
+            ],
+        },
+        {
+            code: String.raw`/[\q{|}]/v`,
+            errors: [
+                {
+                    message: "Unexpected empty string literal.",
+                    column: 3,
+                    endColumn: 8,
+                },
+            ],
+        },
+    ],
+})