diff --git a/.changeset/early-islands-press.md b/.changeset/early-islands-press.md
new file mode 100644
index 000000000..16389b689
--- /dev/null
+++ b/.changeset/early-islands-press.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": major
+---
+
+Add `regexp/simplify-set-operations` rule
diff --git a/.changeset/early-islands-press2.md b/.changeset/early-islands-press2.md
new file mode 100644
index 000000000..5810f1b63
--- /dev/null
+++ b/.changeset/early-islands-press2.md
@@ -0,0 +1,5 @@
+---
+"eslint-plugin-regexp": minor
+---
+
+Improve `regexp/negation` rule to report nested negation character classes
diff --git a/README.md b/README.md
index 561ec723e..80dafb974 100644
--- a/README.md
+++ b/README.md
@@ -167,6 +167,7 @@ The `plugin:regexp/all` config enables all rules. It's meant for testing, not fo
| [prefer-regexp-test](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-regexp-test.html) | enforce that `RegExp#test` is used instead of `String#match` and `RegExp#exec` | | | 🔧 | |
| [require-unicode-regexp](https://ota-meshi.github.io/eslint-plugin-regexp/rules/require-unicode-regexp.html) | enforce the use of the `u` flag | | | 🔧 | |
| [require-unicode-sets-regexp](https://ota-meshi.github.io/eslint-plugin-regexp/rules/require-unicode-sets-regexp.html) | enforce the use of the `v` flag | | | 🔧 | |
+| [simplify-set-operations](https://ota-meshi.github.io/eslint-plugin-regexp/rules/simplify-set-operations.html) | require simplify set operations | ✅ | | 🔧 | |
| [sort-alternatives](https://ota-meshi.github.io/eslint-plugin-regexp/rules/sort-alternatives.html) | sort alternatives if order doesn't matter | | | 🔧 | |
| [use-ignore-case](https://ota-meshi.github.io/eslint-plugin-regexp/rules/use-ignore-case.html) | use the `i` flag if it simplifies the pattern | ✅ | | 🔧 | |
diff --git a/docs/rules/index.md b/docs/rules/index.md
index 31e251aac..2af408f20 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -74,6 +74,7 @@ sidebarDepth: 0
| [prefer-regexp-test](prefer-regexp-test.md) | enforce that `RegExp#test` is used instead of `String#match` and `RegExp#exec` | | | 🔧 | |
| [require-unicode-regexp](require-unicode-regexp.md) | enforce the use of the `u` flag | | | 🔧 | |
| [require-unicode-sets-regexp](require-unicode-sets-regexp.md) | enforce the use of the `v` flag | | | 🔧 | |
+| [simplify-set-operations](simplify-set-operations.md) | require simplify set operations | ✅ | | 🔧 | |
| [sort-alternatives](sort-alternatives.md) | sort alternatives if order doesn't matter | | | 🔧 | |
| [use-ignore-case](use-ignore-case.md) | use the `i` flag if it simplifies the pattern | ✅ | | 🔧 | |
diff --git a/docs/rules/negation.md b/docs/rules/negation.md
index 711967485..2d3d75e04 100644
--- a/docs/rules/negation.md
+++ b/docs/rules/negation.md
@@ -53,6 +53,12 @@ var foo = /[^\P{ASCII}]/u
Nothing.
+## :couple: Related rules
+
+- [regexp/simplify-set-operations]
+
+[regexp/simplify-set-operations]: ./simplify-set-operations.md
+
## :rocket: Version
This rule was introduced in eslint-plugin-regexp v0.4.0
diff --git a/docs/rules/simplify-set-operations.md b/docs/rules/simplify-set-operations.md
new file mode 100644
index 000000000..986fb3831
--- /dev/null
+++ b/docs/rules/simplify-set-operations.md
@@ -0,0 +1,92 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "regexp/simplify-set-operations"
+description: "require simplify set operations"
+---
+# regexp/simplify-set-operations
+
+💼 This rule is enabled in the ✅ `plugin:regexp/recommended` config.
+
+🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
+
+
+
+> require simplify set operations
+
+## :book: Rule Details
+
+This rule aims to optimize patterns by simplifying set operations in character classes (with `v` flag).
+
+This rule does not report simple nested negations. (e.g. `/[^[^abc]]/v`)\
+If you want to report simple nested negations, use the [regexp/negation] rule.
+
+
+
+```js
+/* eslint regexp/simplify-set-operations: "error" */
+
+/* ✗ BAD */
+var re = /[a&&[^b]]/v; // -> /[a--b]/v
+var re = /[[^b]&&a]/v; // -> /[a--b]/v
+var re = /[a--[^b]]/v; // -> /[a&&b]/v
+var re = /[[^a]&&[^b]]/v; // -> /[^ab]/v
+var re = /[[^a][^b]]/v; // -> /[^a&&b]/v
+
+/* ✓ GOOD */
+var re = /[a--b]/v;
+var re = /[a&&b]/v;
+var re = /[^ab]/v;
+var re = /[^a&&b]/v;
+```
+
+
+
+### How does this rule work?
+
+This rule attempts to simplify set operations in the ways listed below:
+
+#### De Morgan's laws
+
+This rule uses De Morgan's laws to look for patterns that can convert multiple negations into a single negation, reports on them, and auto-fix them.\
+For example, `/[[^a]&&[^b]]/v` is equivalent to `/[^ab]/v`, and `/[[^a][^b]]/v` is equivalent to `/[^a&&b]/v`.
+
+See .
+
+#### Conversion from the intersection to the subtraction
+
+Intersection sets with complement operands can be converted to difference sets.\
+The rule looks for character class intersection with negation operands, reports on them, auto-fix them.\
+For example, `/[a&&[^b]]/v` is equivalent to `/[a--b]/v`, `/[[^a]&&b]/v` is equivalent to `/[b--a]/v`.
+
+#### Conversion from the subtraction to the intersection
+
+Difference set with a complement operand on the right side can be converted to intersection sets.\
+The rule looks for character class subtraction with negation operand on the right side, reports on them, auto-fix them.\
+For example, `/[a--[^b]]/v` is equivalent to `/[a&&b]/v`.
+
+### Auto Fixes
+
+This rule's auto-fix does not remove unnecessary brackets. For example, `/[[^a]&&[^b]]/v` will be automatically fixed to `/[^[a][b]]/v`.\
+If you want to remove unnecessary brackets (e.g. auto-fixed to `/[^ab]/v`), use [regexp/no-useless-character-class] rule together.
+
+## :wrench: Options
+
+Nothing.
+
+## :couple: Related rules
+
+- [regexp/negation]
+- [regexp/no-useless-character-class]
+
+[regexp/negation]: ./negation.md
+[regexp/no-useless-character-class]: ./no-useless-character-class.md
+
+## :rocket: Version
+
+:exclamation: ***This rule has not been released yet.***
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/simplify-set-operations.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/simplify-set-operations.ts)
diff --git a/lib/configs/recommended.ts b/lib/configs/recommended.ts
index 3d0979ee0..e7d46d2fe 100644
--- a/lib/configs/recommended.ts
+++ b/lib/configs/recommended.ts
@@ -63,6 +63,7 @@ export const rules = {
"regexp/prefer-star-quantifier": "error",
"regexp/prefer-unicode-codepoint-escapes": "error",
"regexp/prefer-w": "error",
+ "regexp/simplify-set-operations": "error",
"regexp/sort-flags": "error",
"regexp/strict": "error",
"regexp/use-ignore-case": "error",
diff --git a/lib/rules/negation.ts b/lib/rules/negation.ts
index bea46c5db..b518ee0dc 100644
--- a/lib/rules/negation.ts
+++ b/lib/rules/negation.ts
@@ -1,11 +1,33 @@
-import { toCharSet, toUnicodeSet } from "regexp-ast-analysis"
+import { toUnicodeSet } from "regexp-ast-analysis"
import type {
+ CharacterClass,
+ CharacterClassElement,
+ CharacterUnicodePropertyCharacterSet,
EscapeCharacterSet,
- UnicodePropertyCharacterSet,
+ ExpressionCharacterClass,
} from "@eslint-community/regexpp/ast"
import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
import type { RegExpContext } from "../utils"
import { createRule, defineRegexpVisitor } from "../utils"
+import { assertNever } from "../utils/util"
+
+type NegatableCharacterClassElement =
+ | CharacterClass
+ | ExpressionCharacterClass
+ | EscapeCharacterSet
+ | CharacterUnicodePropertyCharacterSet
+
+/** Checks whether the given character class is negatable. */
+function isNegatableCharacterClassElement(
+ node: N,
+): node is N & NegatableCharacterClassElement {
+ return (
+ node.type === "CharacterClass" ||
+ node.type === "ExpressionCharacterClass" ||
+ (node.type === "CharacterSet" &&
+ (node.kind !== "property" || !node.strings))
+ )
+}
export default createRule("negation", {
meta: {
@@ -36,19 +58,17 @@ export default createRule("negation", {
}
const element = ccNode.elements[0]
- if (element.type !== "CharacterSet") {
+ if (!isNegatableCharacterClassElement(element)) {
return
}
- if (element.kind === "property" && element.strings) {
- // Unicode property escape with property of strings.
- // Actually the pattern passing through this branch is an invalid pattern,
- // but it has to be checked because of the type guards.
+ if (element.type !== "CharacterSet" && !element.negate) {
return
}
if (
flags.ignoreCase &&
!flags.unicodeSets &&
+ element.type === "CharacterSet" &&
element.kind === "property"
) {
// The ignore case canonicalization affects negated
@@ -61,7 +81,7 @@ export default createRule("negation", {
// (/./, /\s/, /\d/) or inconsistent (/\w/).
const ccSet = toUnicodeSet(ccNode, flags)
- const negatedElementSet = toCharSet(
+ const negatedElementSet = toUnicodeSet(
{
...element,
negate: !element.negate,
@@ -96,17 +116,24 @@ export default createRule("negation", {
/**
* Gets the text that negation the CharacterSet.
*/
-function getNegationText(
- node: EscapeCharacterSet | UnicodePropertyCharacterSet,
-) {
- // they are all of the form: /\\[dswp](?:\{[^{}]+\})?/
- let kind = node.raw[1]
+function getNegationText(node: NegatableCharacterClassElement) {
+ if (node.type === "CharacterSet") {
+ // they are all of the form: /\\[dswp](?:\{[^{}]+\})?/
+ let kind = node.raw[1]
- if (kind.toLowerCase() === kind) {
- kind = kind.toUpperCase()
- } else {
- kind = kind.toLowerCase()
- }
+ if (kind.toLowerCase() === kind) {
+ kind = kind.toUpperCase()
+ } else {
+ kind = kind.toLowerCase()
+ }
- return `\\${kind}${node.raw.slice(2)}`
+ return `\\${kind}${node.raw.slice(2)}`
+ }
+ if (node.type === "CharacterClass") {
+ return `[${node.elements.map((e) => e.raw).join("")}]`
+ }
+ if (node.type === "ExpressionCharacterClass") {
+ return `[${node.raw.slice(2, -1)}]`
+ }
+ return assertNever(node)
}
diff --git a/lib/rules/simplify-set-operations.ts b/lib/rules/simplify-set-operations.ts
new file mode 100644
index 000000000..e4c5a650f
--- /dev/null
+++ b/lib/rules/simplify-set-operations.ts
@@ -0,0 +1,419 @@
+import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
+import type { RegExpContext } from "../utils"
+import { createRule, defineRegexpVisitor } from "../utils"
+import type {
+ CharacterClass,
+ CharacterClassElement,
+ CharacterUnicodePropertyCharacterSet,
+ ClassIntersection,
+ ClassSetOperand,
+ ClassSubtraction,
+ EscapeCharacterSet,
+ ExpressionCharacterClass,
+} from "@eslint-community/regexpp/ast"
+import type { ReadonlyFlags, ToUnicodeSetElement } from "regexp-ast-analysis"
+import { toUnicodeSet } from "regexp-ast-analysis"
+import { RegExpParser } from "@eslint-community/regexpp"
+
+type NegatableCharacterClassElement =
+ | CharacterClass
+ | ExpressionCharacterClass
+ | EscapeCharacterSet
+ | CharacterUnicodePropertyCharacterSet
+type NegateCharacterClassElement = NegatableCharacterClassElement & {
+ negate: true
+}
+
+/** Checks whether the given character class is negatable. */
+function isNegatableCharacterClassElement<
+ N extends CharacterClassElement | CharacterClass | ClassIntersection,
+>(node: N): node is N & NegatableCharacterClassElement {
+ return (
+ node.type === "CharacterClass" ||
+ node.type === "ExpressionCharacterClass" ||
+ (node.type === "CharacterSet" &&
+ (node.kind !== "property" || !node.strings))
+ )
+}
+
+/** Checks whether the given character class is negate. */
+function isNegate<
+ N extends CharacterClassElement | CharacterClass | ClassIntersection,
+>(node: N): node is N & NegateCharacterClassElement {
+ return isNegatableCharacterClassElement(node) && node.negate
+}
+
+/**
+ * Gets the text of a character class that negates the given character class.
+ */
+function getRawTextToNot(negateNode: NegateCharacterClassElement) {
+ const raw = negateNode.raw
+ if (
+ negateNode.type === "CharacterClass" ||
+ negateNode.type === "ExpressionCharacterClass"
+ ) {
+ return `${raw[0]}${raw.slice(2)}`
+ }
+ // else if (node.type === "CharacterSet") {
+ const escapeChar = negateNode.raw[1].toLowerCase()
+ return `${raw[0]}${escapeChar}${raw.slice(2)}`
+}
+
+/** Collect the operands from the given intersection expression */
+function collectIntersectionOperands(
+ expression: ClassIntersection,
+): ClassSetOperand[] {
+ const operands: ClassSetOperand[] = []
+ let operand: ClassIntersection | ClassSetOperand = expression
+ while (operand.type === "ClassIntersection") {
+ operands.unshift(operand.right)
+ operand = operand.left
+ }
+ operands.unshift(operand)
+ return operands
+}
+
+/** Gets the parsed result element. */
+function getParsedElement(
+ pattern: string,
+ flags: ReadonlyFlags,
+): ToUnicodeSetElement | null {
+ try {
+ const ast = new RegExpParser().parsePattern(
+ pattern,
+ undefined,
+ undefined,
+ {
+ unicode: flags.unicode,
+ unicodeSets: flags.unicodeSets,
+ },
+ )
+ if (ast.alternatives.length === 1)
+ if (ast.alternatives[0].elements.length === 1) {
+ const element = ast.alternatives[0].elements[0]
+ if (
+ element.type !== "Assertion" &&
+ element.type !== "Quantifier" &&
+ element.type !== "CapturingGroup" &&
+ element.type !== "Group" &&
+ element.type !== "Backreference"
+ )
+ return element
+ }
+ } catch (_error) {
+ // ignore
+ }
+ return null
+}
+
+export default createRule("simplify-set-operations", {
+ meta: {
+ docs: {
+ description: "require simplify set operations",
+ category: "Best Practices",
+ recommended: true,
+ },
+ schema: [],
+ messages: {
+ toNegationOfDisjunction:
+ "This {{target}} can be converted to the negation of a disjunction using De Morgan's laws.",
+ toNegationOfConjunction:
+ "This character class can be converted to the negation of a conjunction using De Morgan's laws.",
+ toSubtraction:
+ "This expression can be converted to the subtraction.",
+ toIntersection:
+ "This expression can be converted to the intersection.",
+ },
+ fixable: "code",
+ type: "suggestion",
+ },
+ create(context) {
+ /**
+ * Create visitor
+ */
+ function createVisitor(
+ regexpContext: RegExpContext,
+ ): RegExpVisitor.Handlers {
+ const { node, flags, getRegexpLocation, fixReplaceNode } =
+ regexpContext
+
+ if (!flags.unicodeSets) {
+ // set operations are exclusive to the v flag.
+ return {}
+ }
+ return {
+ onCharacterClassEnter(ccNode) {
+ toNegationOfConjunction(ccNode)
+ },
+ onExpressionCharacterClassEnter(eccNode) {
+ if (toNegationOfDisjunction(eccNode)) {
+ return
+ }
+ if (toSubtraction(eccNode)) {
+ return
+ }
+ verifyExpressions(eccNode)
+ },
+ }
+
+ /**
+ * Reports if the fixed pattern is compatible with the original pattern.
+ * Returns true if reported.
+ */
+ function reportWhenFixedIsCompatible({
+ reportNode,
+ targetNode,
+ messageId,
+ data,
+ fix,
+ }: {
+ reportNode:
+ | CharacterClass
+ | ExpressionCharacterClass
+ | ClassIntersection
+ | ClassSubtraction
+ targetNode: CharacterClass | ExpressionCharacterClass
+ messageId:
+ | "toNegationOfDisjunction"
+ | "toNegationOfConjunction"
+ | "toSubtraction"
+ | "toIntersection"
+ data?: Record
+ fix: () => string
+ }) {
+ const us = toUnicodeSet(targetNode, flags)
+ const fixedText = fix()
+ const convertedElement = getParsedElement(fixedText, flags)
+ if (!convertedElement) {
+ return false
+ }
+ const convertedUs = toUnicodeSet(convertedElement, flags)
+ if (!us.equals(convertedUs)) {
+ return false
+ }
+ context.report({
+ node,
+ loc: getRegexpLocation(reportNode),
+ messageId,
+ data: data || {},
+ fix: fixReplaceNode(targetNode, fixedText),
+ })
+ return true
+ }
+
+ /** Verify for intersections and subtractions */
+ function verifyExpressions(eccNode: ExpressionCharacterClass) {
+ let operand:
+ | ClassIntersection
+ | ClassSubtraction
+ | ClassSetOperand = eccNode.expression
+ let right:
+ | ClassIntersection
+ | ClassSubtraction
+ | ClassSetOperand
+ | null = null
+ while (
+ operand.type === "ClassIntersection" ||
+ operand.type === "ClassSubtraction"
+ ) {
+ toIntersection(operand, right, eccNode)
+ right = operand.right
+ operand = operand.left
+ }
+ }
+
+ /**
+ * Checks the given character class and reports if it can be converted to the negation of a disjunction
+ * using De Morgan's laws.
+ * Returns true if reported.
+ *
+ * e.g.
+ * - `[[^a]&&[^b]]` -> `[^ab]`
+ * - `[^[^a]&&[^b]]` -> `[ab]`
+ * - `[[^a]&&[^b]&&c]` -> `[[^ab]&&c]`
+ */
+ function toNegationOfDisjunction(
+ eccNode: ExpressionCharacterClass,
+ ) {
+ const expression = eccNode.expression
+ if (expression.type !== "ClassIntersection") {
+ return false
+ }
+ const operands = collectIntersectionOperands(expression)
+ const negateOperands: (NegateCharacterClassElement &
+ ClassSetOperand)[] = []
+ const others: ClassSetOperand[] = []
+ for (const e of operands) {
+ if (isNegate(e)) {
+ negateOperands.push(e)
+ } else {
+ others.push(e)
+ }
+ }
+ const fixedOperands = negateOperands
+ .map((negateOperand) => getRawTextToNot(negateOperand))
+ .join("")
+ if (negateOperands.length === operands.length) {
+ return reportWhenFixedIsCompatible({
+ reportNode: eccNode,
+ targetNode: eccNode,
+ messageId: "toNegationOfDisjunction",
+ data: {
+ target: "character class",
+ },
+ fix: () =>
+ `[${eccNode.negate ? "" : "^"}${fixedOperands}]`,
+ })
+ }
+ if (negateOperands.length < 2) {
+ return null
+ }
+ return reportWhenFixedIsCompatible({
+ reportNode: negateOperands[negateOperands.length - 1]
+ .parent as ClassIntersection,
+ targetNode: eccNode,
+ messageId: "toNegationOfDisjunction",
+ data: {
+ target: "expression",
+ },
+ fix: () => {
+ const operandTestList = [
+ `[^${fixedOperands}]`,
+ ...others.map((e) => e.raw),
+ ]
+ return `[${
+ eccNode.negate ? "^" : ""
+ }${operandTestList.join("&&")}]`
+ },
+ })
+ }
+
+ /**
+ * Checks the given character class and reports if it can be converted to the negation of a conjunction
+ * using De Morgan's laws.
+ * Returns true if reported.
+ *
+ * e.g.
+ * - `[[^a][^b]]` -> `[^a&&b]`
+ */
+ function toNegationOfConjunction(ccNode: CharacterClass) {
+ if (ccNode.elements.length <= 1) {
+ return false
+ }
+ const elements: CharacterClassElement[] = ccNode.elements
+ const negateElements = elements.filter(isNegate)
+ if (negateElements.length !== elements.length) {
+ return false
+ }
+ return reportWhenFixedIsCompatible({
+ reportNode: ccNode,
+ targetNode: ccNode,
+ messageId: "toNegationOfConjunction",
+ fix: () => {
+ const fixedElements = negateElements.map(
+ (negateElement) => getRawTextToNot(negateElement),
+ )
+ return `[${
+ ccNode.negate ? "" : "^"
+ }${fixedElements.join("&&")}]`
+ },
+ })
+ }
+
+ /**
+ * Checks the given expression and reports whether it can be converted to subtraction by reducing its complement.
+ * Returns true if reported.
+ *
+ * e.g.
+ * - `[a&&[^b]]` -> `[a--b]`
+ * - `[[^a]&&b]` -> `[b--a]`
+ * - `[a&&[^b]&&c]` -> `[[a&&c]--b]`
+ */
+ function toSubtraction(eccNode: ExpressionCharacterClass) {
+ const expression = eccNode.expression
+ if (expression.type !== "ClassIntersection") {
+ return false
+ }
+ const operands = collectIntersectionOperands(expression)
+ const negateOperand = operands.find(isNegate)
+ if (!negateOperand) {
+ return false
+ }
+ return reportWhenFixedIsCompatible({
+ reportNode: expression,
+ targetNode: eccNode,
+ messageId: "toSubtraction",
+ fix() {
+ const others = operands.filter(
+ (e) => e !== negateOperand,
+ )
+ let fixedLeftText = others.map((e) => e.raw).join("&&")
+ if (others.length >= 2) {
+ // Wrap with brackets
+ fixedLeftText = `[${fixedLeftText}]`
+ }
+ const fixedRightText = getRawTextToNot(negateOperand)
+ return `[${
+ eccNode.negate ? "^" : ""
+ }${`${fixedLeftText}--${fixedRightText}`}]`
+ },
+ })
+ }
+
+ /**
+ * Checks the given expression and reports whether it can be converted to intersection by reducing its complement.
+ * Returns true if reported.
+ *
+ * e.g.
+ * - `[a--[^b]]` -> `[a&&b]`
+ */
+ function toIntersection(
+ expression: ClassIntersection | ClassSubtraction,
+ expressionRight:
+ | ClassIntersection
+ | ClassSubtraction
+ | ClassSetOperand
+ | null,
+ eccNode: ExpressionCharacterClass,
+ ) {
+ if (expression.type !== "ClassSubtraction") {
+ return false
+ }
+ const { left, right } = expression
+ if (!isNegate(right)) {
+ return false
+ }
+ return reportWhenFixedIsCompatible({
+ reportNode: expression,
+ targetNode: eccNode,
+ messageId: "toIntersection",
+ fix() {
+ let fixedLeftText = left.raw
+ if (left.type === "ClassSubtraction") {
+ // Wrap with brackets
+ fixedLeftText = `[${fixedLeftText}]`
+ }
+ const fixedRightText = getRawTextToNot(right)
+ let fixedText = `${fixedLeftText}&&${fixedRightText}`
+
+ if (expressionRight) {
+ // Wrap with brackets
+ fixedText = `[${fixedText}]`
+ }
+ const targetRaw = eccNode.raw
+ return `${targetRaw.slice(
+ 0,
+ expression.start - eccNode.start,
+ )}${fixedText}${targetRaw.slice(
+ expression.end - eccNode.start,
+ )}`
+ },
+ })
+ }
+ }
+
+ return defineRegexpVisitor(context, {
+ createVisitor,
+ })
+ },
+})
diff --git a/lib/utils/rules.ts b/lib/utils/rules.ts
index 0d44409c9..b440c9e4c 100644
--- a/lib/utils/rules.ts
+++ b/lib/utils/rules.ts
@@ -73,6 +73,7 @@ import preferUnicodeCodepointEscapes from "../rules/prefer-unicode-codepoint-esc
import preferW from "../rules/prefer-w"
import requireUnicodeRegexp from "../rules/require-unicode-regexp"
import requireUnicodeSetsRegexp from "../rules/require-unicode-sets-regexp"
+import simplifySetOperations from "../rules/simplify-set-operations"
import sortAlternatives from "../rules/sort-alternatives"
import sortCharacterClassElements from "../rules/sort-character-class-elements"
import sortFlags from "../rules/sort-flags"
@@ -155,6 +156,7 @@ export const rules = [
preferW,
requireUnicodeRegexp,
requireUnicodeSetsRegexp,
+ simplifySetOperations,
sortAlternatives,
sortCharacterClassElements,
sortFlags,
diff --git a/package-lock.json b/package-lock.json
index ad90c8f80..0d65cc8ed 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
- "@eslint-community/regexpp": "^4.6.2",
+ "@eslint-community/regexpp": "^4.8.1",
"comment-parser": "^1.4.0",
"grapheme-splitter": "^1.0.4",
"jsdoctypeparser": "^9.0.0",
@@ -2202,9 +2202,9 @@
}
},
"node_modules/@eslint-community/regexpp": {
- "version": "4.8.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz",
- "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==",
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz",
+ "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==",
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
@@ -12990,9 +12990,9 @@
}
},
"@eslint-community/regexpp": {
- "version": "4.8.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz",
- "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg=="
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz",
+ "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ=="
},
"@eslint/eslintrc": {
"version": "2.1.2",
diff --git a/package.json b/package.json
index e00c9eb38..db08b0b50 100644
--- a/package.json
+++ b/package.json
@@ -103,7 +103,7 @@
},
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
- "@eslint-community/regexpp": "^4.6.2",
+ "@eslint-community/regexpp": "^4.8.1",
"comment-parser": "^1.4.0",
"grapheme-splitter": "^1.0.4",
"jsdoctypeparser": "^9.0.0",
diff --git a/tests/lib/rules/negation.ts b/tests/lib/rules/negation.ts
index de4cb82be..4a83e97fe 100644
--- a/tests/lib/rules/negation.ts
+++ b/tests/lib/rules/negation.ts
@@ -16,6 +16,7 @@ tester.run("negation", rule as any, {
String.raw`/[^\P{Ll}]/iu`,
String.raw`/[\p{Basic_Emoji}]/v`,
String.raw`/[^\P{Lowercase_Letter}]/iu`,
+ String.raw`/[^[^a][^b]]/v`,
],
invalid: [
{
@@ -149,5 +150,46 @@ tester.run("negation", rule as any, {
"Unexpected negated character class. Use '\\p{Lowercase_Letter}' instead.",
],
},
+ {
+ code: String.raw`/[^[^abc]]/v`,
+ output: String.raw`/[abc]/v`,
+ errors: [
+ "Unexpected negated character class. Use '[abc]' instead.",
+ ],
+ },
+ {
+ code: String.raw`/[^[^\q{a|1|A}&&\w]]/v`,
+ output: String.raw`/[\q{a|1|A}&&\w]/v`,
+ errors: [
+ "Unexpected negated character class. Use '[\\q{a|1|A}&&\\w]' instead.",
+ ],
+ },
+ {
+ code: String.raw`/[^[^a]]/iv`,
+ output: String.raw`/[a]/iv`,
+ errors: ["Unexpected negated character class. Use '[a]' instead."],
+ },
+ {
+ code: String.raw`/[^[^\P{Lowercase_Letter}]]/iv`,
+ output: String.raw`/[\P{Lowercase_Letter}]/iv`,
+ errors: [
+ "Unexpected negated character class. Use '[\\P{Lowercase_Letter}]' instead.",
+ "Unexpected negated character class. Use '\\p{Lowercase_Letter}' instead.",
+ ],
+ },
+ {
+ code: String.raw`/[^[^[\p{Lowercase_Letter}&&[ABC]]]]/iv`,
+ output: String.raw`/[[\p{Lowercase_Letter}&&[ABC]]]/iv`,
+ errors: [
+ "Unexpected negated character class. Use '[[\\p{Lowercase_Letter}&&[ABC]]]' instead.",
+ ],
+ },
+ {
+ code: String.raw`/[^[^[\p{Lowercase_Letter}&&A]--B]]/iv`,
+ output: String.raw`/[[\p{Lowercase_Letter}&&A]--B]/iv`,
+ errors: [
+ "Unexpected negated character class. Use '[[\\p{Lowercase_Letter}&&A]--B]' instead.",
+ ],
+ },
],
})
diff --git a/tests/lib/rules/simplify-set-operations.ts b/tests/lib/rules/simplify-set-operations.ts
new file mode 100644
index 000000000..9acc4ce73
--- /dev/null
+++ b/tests/lib/rules/simplify-set-operations.ts
@@ -0,0 +1,172 @@
+import { RuleTester } from "eslint"
+import rule from "../../../lib/rules/simplify-set-operations"
+
+const tester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: "latest",
+ sourceType: "module",
+ },
+})
+
+tester.run("simplify-set-operations", rule as any, {
+ valid: [
+ String.raw`/[[abc]]/v`,
+ String.raw`/[\d]/u`,
+ String.raw`/[^\d]/v`,
+ String.raw`/[a--b]/v`,
+ String.raw`/[a&&b]/v`,
+ String.raw`/[^ab]/v`,
+ String.raw`/[^a&&b]/v;`,
+ String.raw`/[\s\p{ASCII}]/u`,
+ String.raw`/[^\S\P{ASCII}]/u`,
+ String.raw`/[^[]]/v`,
+ String.raw`/[a&&b&&[c]]/v`,
+ String.raw`/[a--b--[c]]/v`,
+ ],
+ invalid: [
+ {
+ code: String.raw`/[a&&[^b]]/v`,
+ output: String.raw`/[a--[b]]/v`,
+ errors: ["This expression can be converted to the subtraction."],
+ },
+ {
+ code: String.raw`/[a&&b&&[^c]]/v`,
+ output: String.raw`/[[a&&b]--[c]]/v`,
+ errors: ["This expression can be converted to the subtraction."],
+ },
+ {
+ code: String.raw`/[a&&[^b]&&c]/v`,
+ output: String.raw`/[[a&&c]--[b]]/v`,
+ errors: ["This expression can be converted to the subtraction."],
+ },
+ {
+ code: String.raw`/[a&&b&&[^c]&&d]/v`,
+ output: String.raw`/[[a&&b&&d]--[c]]/v`,
+ errors: ["This expression can be converted to the subtraction."],
+ },
+ {
+ code: String.raw`/[[^a]&&b&&c]/v`,
+ output: String.raw`/[[b&&c]--[a]]/v`,
+ errors: ["This expression can be converted to the subtraction."],
+ },
+ {
+ code: String.raw`/[[^b]&&a]/v`,
+ output: String.raw`/[a--[b]]/v`,
+ errors: ["This expression can be converted to the subtraction."],
+ },
+ {
+ code: String.raw`/[[abc]&&[^def]]/v`,
+ output: String.raw`/[[abc]--[def]]/v`,
+ errors: ["This expression can be converted to the subtraction."],
+ },
+ {
+ code: String.raw`/[a--[^b]]/v`,
+ output: String.raw`/[a&&[b]]/v`,
+ errors: ["This expression can be converted to the intersection."],
+ },
+ {
+ code: String.raw`/[a--[^b]--c]/v`,
+ output: String.raw`/[[a&&[b]]--c]/v`,
+ errors: ["This expression can be converted to the intersection."],
+ },
+ {
+ code: String.raw`/[a--b--[^c]]/v`,
+ output: String.raw`/[[a--b]&&[c]]/v`,
+ errors: ["This expression can be converted to the intersection."],
+ },
+ {
+ code: String.raw`/[[abc]--[^def]]/v`,
+ output: String.raw`/[[abc]&&[def]]/v`,
+ errors: ["This expression can be converted to the intersection."],
+ },
+ {
+ code: String.raw`/[[^a]&&[^b]]/v`,
+ output: String.raw`/[^[a][b]]/v`,
+ errors: [
+ "This character class can be converted to the negation of a disjunction using De Morgan's laws.",
+ ],
+ },
+ {
+ code: String.raw`/[^[^a]&&[^b]]/v`,
+ output: String.raw`/[[a][b]]/v`,
+ errors: [
+ "This character class can be converted to the negation of a disjunction using De Morgan's laws.",
+ ],
+ },
+ {
+ code: String.raw`/[[^a]&&[^b]&&\D]/v`,
+ output: String.raw`/[^[a][b]\d]/v`,
+ errors: [
+ "This character class can be converted to the negation of a disjunction using De Morgan's laws.",
+ ],
+ },
+ {
+ code: String.raw`/[^[^a]&&[^b]&&\D]/v`,
+ output: String.raw`/[[a][b]\d]/v`,
+ errors: [
+ "This character class can be converted to the negation of a disjunction using De Morgan's laws.",
+ ],
+ },
+ {
+ code: String.raw`/[[^a]&&\D&&b]/v`,
+ output: String.raw`/[[^[a]\d]&&b]/v`,
+ errors: [
+ "This expression can be converted to the negation of a disjunction using De Morgan's laws.",
+ ],
+ },
+ {
+ code: String.raw`/[[^abc]&&[^def]&&\D]/v`,
+ output: String.raw`/[^[abc][def]\d]/v`,
+ errors: [
+ "This character class can be converted to the negation of a disjunction using De Morgan's laws.",
+ ],
+ },
+ {
+ code: String.raw`/[[^a]&&[b]&&[^c]]/v`,
+ output: String.raw`/[[^[a][c]]&&[b]]/v`,
+ errors: [
+ "This expression can be converted to the negation of a disjunction using De Morgan's laws.",
+ ],
+ },
+ {
+ code: String.raw`/[[^a][^b]]/v`,
+ output: String.raw`/[^[a]&&[b]]/v`,
+ errors: [
+ "This character class can be converted to the negation of a conjunction using De Morgan's laws.",
+ ],
+ },
+ {
+ code: String.raw`/[[^abc][^def]]/v`,
+ output: String.raw`/[^[abc]&&[def]]/v`,
+ errors: [
+ "This character class can be converted to the negation of a conjunction using De Morgan's laws.",
+ ],
+ },
+ {
+ code: String.raw`/[^[^a][^b]]/v`,
+ output: String.raw`/[[a]&&[b]]/v`,
+ errors: [
+ "This character class can be converted to the negation of a conjunction using De Morgan's laws.",
+ ],
+ },
+ {
+ code: String.raw`/[^\S\P{ASCII}]/v`,
+ output: String.raw`/[\s&&\p{ASCII}]/v`,
+ errors: [
+ "This character class can be converted to the negation of a conjunction using De Morgan's laws.",
+ ],
+ },
+ {
+ code: String.raw`/[a&&[^b]&&[^c]&&d]/v`,
+ output: String.raw`/[[^[b][c]]&&a&&d]/v`,
+ errors: [
+ "This expression can be converted to the negation of a disjunction using De Morgan's laws.",
+ ],
+ },
+ {
+ code: String.raw`/[[^bc]&&a&&d]/v`,
+ output: String.raw`/[[a&&d]--[bc]]/v`,
+ errors: ["This expression can be converted to the subtraction."],
+ },
+ ],
+})