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."], + }, + ], +})