From cf22f9fd79e7e2277da71333f05bb43a2649eed9 Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Mon, 11 Sep 2023 22:34:14 +0900 Subject: [PATCH 01/14] Add `regexp/require-reduce-negation` rule --- README.md | 1 + docs/rules/index.md | 1 + docs/rules/negation.md | 6 + docs/rules/require-reduce-negation.md | 64 +++ lib/configs/recommended.ts | 1 + lib/rules/require-reduce-negation.ts | 515 +++++++++++++++++++++ lib/utils/rules.ts | 2 + tests/lib/rules/require-reduce-negation.ts | 178 +++++++ 8 files changed, 768 insertions(+) create mode 100644 docs/rules/require-reduce-negation.md create mode 100644 lib/rules/require-reduce-negation.ts create mode 100644 tests/lib/rules/require-reduce-negation.ts diff --git a/README.md b/README.md index bc02ae606..650ad2181 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,7 @@ The `plugin:regexp/all` config enables all rules. It's meant for testing, not fo | [prefer-range](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-range.html) | enforce using character class range | ✅ | | 🔧 | | | [prefer-regexp-exec](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-regexp-exec.html) | enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided | | | | | | [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-reduce-negation](https://ota-meshi.github.io/eslint-plugin-regexp/rules/require-reduce-negation.html) | require to reduce negation of character classes | ✅ | | 🔧 | | | [require-unicode-regexp](https://ota-meshi.github.io/eslint-plugin-regexp/rules/require-unicode-regexp.html) | enforce the use of the `u` flag | | | 🔧 | | | [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 0313956f5..cbcab7c35 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -72,6 +72,7 @@ sidebarDepth: 0 | [prefer-range](prefer-range.md) | enforce using character class range | ✅ | | 🔧 | | | [prefer-regexp-exec](prefer-regexp-exec.md) | enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided | | | | | | [prefer-regexp-test](prefer-regexp-test.md) | enforce that `RegExp#test` is used instead of `String#match` and `RegExp#exec` | | | 🔧 | | +| [require-reduce-negation](require-reduce-negation.md) | require to reduce negation of character classes | ✅ | | 🔧 | | | [require-unicode-regexp](require-unicode-regexp.md) | enforce the use of the `u` flag | | | 🔧 | | | [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..5b871f54e 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/require-reduce-negation] + +[regexp/require-reduce-negation]: ./require-reduce-negation.md + ## :rocket: Version This rule was introduced in eslint-plugin-regexp v0.4.0 diff --git a/docs/rules/require-reduce-negation.md b/docs/rules/require-reduce-negation.md new file mode 100644 index 000000000..b13d7e0a1 --- /dev/null +++ b/docs/rules/require-reduce-negation.md @@ -0,0 +1,64 @@ +--- +pageClass: "rule-details" +sidebarDepth: 0 +title: "regexp/require-reduce-negation" +description: "require to reduce negation of character classes" +--- +# regexp/require-reduce-negation + +💼 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 to reduce negation of character classes + +## :book: Rule Details + +This rule is aimed at optimizing patterns by reducing the negation (complement) representation of character classes (with `v` flag). + + + +```js +/* eslint regexp/require-reduce-negation: "error" */ + +/* ✗ BAD */ +var re = /[^[^abc]]/v; // -> /[[abc]]/v +var re = /[^\D]/u; // -> /[\d]/u +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 = /[[abc]]/v; +var re = /[\d]/u; +var re = /[\D]/u; +var re = /[a--b]/v; +var re = /[a&&b]/v; +var re = /[^ab]/v; +var re = /[^a&&b]/v; +``` + + + +## :wrench: Options + +Nothing. + +## :couple: Related rules + +- [regexp/negation] + +[regexp/negation]: ./negation.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/require-reduce-negation.ts) +- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/require-reduce-negation.ts) diff --git a/lib/configs/recommended.ts b/lib/configs/recommended.ts index 3d0979ee0..17666671f 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/require-reduce-negation": "error", "regexp/sort-flags": "error", "regexp/strict": "error", "regexp/use-ignore-case": "error", diff --git a/lib/rules/require-reduce-negation.ts b/lib/rules/require-reduce-negation.ts new file mode 100644 index 000000000..476639668 --- /dev/null +++ b/lib/rules/require-reduce-negation.ts @@ -0,0 +1,515 @@ +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 + +/** 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)) + ) +} + +/** + * Gets the text of a character class that negates the given character class. + */ +function getRawTextForNot(node: NegatableCharacterClassElement) { + const raw = node.raw + if ( + node.type === "CharacterClass" || + node.type === "ExpressionCharacterClass" + ) { + if (node.negate) { + return `${raw[0]}${raw.slice(2)}` + } + return `${raw[0]}^${raw.slice(1)}` + } + // else if (node.type === "CharacterSet") { + const escapeChar = node.raw[1] + return `${raw[0]}${ + node.negate ? escapeChar.toLowerCase() : escapeChar.toUpperCase() + }${raw.slice(2)}` +} + +export default createRule("require-reduce-negation", { + meta: { + docs: { + description: "require to reduce negation of character classes", + category: "Best Practices", + recommended: true, + }, + schema: [], + messages: { + doubleNegationElimination: + "This character class can be double negation elimination.", + 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 + return { + onCharacterClassEnter(ccNode) { + if (doubleNegationElimination(ccNode)) { + return + } + toNegationOfConjunction(ccNode) + }, + onExpressionCharacterClassEnter(eccNode) { + if (toNegationOfDisjunctionForCharacterClass(eccNode)) { + return + } + verifyExpressions(eccNode) + }, + } + + /** 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" + ) { + if (toNegationOfDisjunctionForExpression(operand)) { + return + } + void ( + toSubtraction(operand, right) || + toIntersection(operand, right) + ) + right = operand.right + operand = operand.left + } + } + + /** + * Checks the given character class and reports if double negation elimination + * is possible. + * Returns true if reported. + * + * e.g. + * - `[^[^abc]]` -> `[abc]` + * - `[^\D]` -> `[\d]` + */ + function doubleNegationElimination(ccNode: CharacterClass) { + if (!ccNode.negate && ccNode.elements.length !== 1) { + return false + } + const element = ccNode.elements[0] + if ( + !isNegatableCharacterClassElement(element) || + !element.negate + ) { + return false + } + const complementElement: NegatableCharacterClassElement = { + ...element, + negate: false, + } + + const us = toUnicodeSet(ccNode, flags) + const convertedUs = toUnicodeSet(complementElement, flags) + if (!us.equals(convertedUs)) { + return false + } + context.report({ + node, + loc: getRegexpLocation(ccNode), + messageId: "doubleNegationElimination", + fix: fixReplaceNode(ccNode, () => { + let fixedElementText = getRawTextForNot(element) + if ( + element.type === "CharacterClass" && + element.negate + ) { + // Remove brackets + fixedElementText = fixedElementText.slice(1, -1) + } + + return `[${fixedElementText}]` + }), + }) + return true // reported + } + + /** + * 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]` + */ + function toNegationOfDisjunctionForCharacterClass( + eccNode: ExpressionCharacterClass, + ) { + return toNegationOfDisjunction( + eccNode.expression, + eccNode, + (fixedElements) => { + return `[${eccNode.negate ? "" : "^"}${fixedElements}]` + }, + ) + } + + /** + * Checks the given expression 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]&&c]` -> `[[^ab]&&c]` + */ + function toNegationOfDisjunctionForExpression( + expression: ClassIntersection | ClassSubtraction, + ) { + return toNegationOfDisjunction( + expression, + expression, + (fixedElements) => { + return `[^${fixedElements}]` + }, + ) + } + + /** + * Checks the given expression and reports if it can be converted to the negation of a disjunction + * using De Morgan's laws. + * Returns true if reported. + */ + function toNegationOfDisjunction( + expression: ClassIntersection | ClassSubtraction, + targetNode: + | ExpressionCharacterClass + | ClassIntersection + | ClassSubtraction, + postFix: (fixedElements: string) => string, + ) { + if (expression.type !== "ClassIntersection") { + return false + } + const operands: ClassSetOperand[] = [] + let operand: ClassIntersection | ClassSetOperand = expression + while (operand.type === "ClassIntersection") { + operands.unshift(operand.right) + operand = operand.left + } + operands.unshift(operand) + const elements = operands + .filter(isNegatableCharacterClassElement) + .filter((e) => e.negate) + if (elements.length !== operands.length) { + return false + } + const us = toUnicodeSet(targetNode, flags) + const fixedElements = elements.map((element) => { + let fixedElementText = getRawTextForNot(element) + if (element.type === "CharacterClass" && element.negate) { + // Remove brackets + fixedElementText = fixedElementText.slice(1, -1) + } + return fixedElementText + }) + const fixedText = postFix(fixedElements.join("")) + 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(targetNode), + messageId: "toNegationOfDisjunction", + data: { + target: + targetNode.type === "ExpressionCharacterClass" + ? "character class" + : "expression", + }, + fix: fixReplaceNode(targetNode, fixedText), + }) + return true // reported + } + + /** + * 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 || !flags.unicodeSets) { + return false + } + const operands: CharacterClassElement[] = ccNode.elements + const elements = operands + .filter(isNegatableCharacterClassElement) + .filter((e) => e.negate) + if (elements.length !== operands.length) { + return false + } + const us = toUnicodeSet(ccNode, flags) + const fixedElements = elements.map((element) => { + let fixedElementText = getRawTextForNot(element) + if ( + element.type === "CharacterClass" && + element.negate && + element.elements.length === 1 + ) { + // Remove brackets + fixedElementText = fixedElementText.slice(1, -1) + } + return fixedElementText + }) + const fixedText = `[${ + ccNode.negate ? "" : "^" + }${fixedElements.join("&&")}]` + 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(ccNode), + messageId: "toNegationOfConjunction", + fix: fixReplaceNode(ccNode, fixedText), + }) + return true // reported + } + + /** + * 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]` + */ + function toSubtraction( + expression: ClassIntersection | ClassSubtraction, + expressionRight: + | ClassIntersection + | ClassSubtraction + | ClassSetOperand + | null, + ) { + if (expression.type !== "ClassIntersection") { + return false + } + const { left, right } = expression + + let fixedLeft: ClassSetOperand | ClassIntersection, + fixedRight: ClassSetOperand & NegatableCharacterClassElement + if (isNegatableCharacterClassElement(left) && left.negate) { + fixedLeft = right + fixedRight = left + } else if ( + isNegatableCharacterClassElement(right) && + right.negate + ) { + fixedLeft = left + fixedRight = right + } else { + return false + } + const us = toUnicodeSet(expression, flags) + let fixedLeftText = fixedLeft.raw + if (fixedLeft.type === "ClassIntersection") { + // Wrap with brackets + fixedLeftText = `[${fixedLeftText}]` + } + let fixedRightText = getRawTextForNot(fixedRight) + if ( + fixedRight.type === "CharacterClass" && + fixedRight.negate && + fixedRight.elements.length === 1 + ) { + // Remove brackets + fixedRightText = fixedRightText.slice(1, -1) + } + let fixedText = `${fixedLeftText}--${fixedRightText}` + if (expressionRight) { + // Wrap with brackets + fixedText = `[${fixedText}]` + } + 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(expression), + messageId: "toSubtraction", + fix: fixReplaceNode(expression, fixedText), + }) + return true // reported + } + + /** + * 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, + ) { + if (expression.type !== "ClassSubtraction") { + return false + } + const { left, right } = expression + if (!isNegatableCharacterClassElement(right) || !right.negate) { + return false + } + + const us = toUnicodeSet(expression, flags) + let fixedLeftText = left.raw + if (left.type === "ClassSubtraction") { + // Wrap with brackets + fixedLeftText = `[${fixedLeftText}]` + } + let fixedRightText = getRawTextForNot(right) + if ( + right.type === "CharacterClass" && + right.negate && + right.elements.length === 1 + ) { + // Remove brackets + fixedRightText = fixedRightText.slice(1, -1) + } + let fixedText = `${fixedLeftText}&&${fixedRightText}` + + if (expressionRight) { + // Wrap with brackets + fixedText = `[${fixedText}]` + } + 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(expression), + messageId: "toIntersection", + fix: fixReplaceNode(expression, fixedText), + }) + return true // reported + } + } + + return defineRegexpVisitor(context, { + createVisitor, + }) + }, +}) + +/** 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 null + return element + } + } catch (_error) { + // ignore + } + return null +} diff --git a/lib/utils/rules.ts b/lib/utils/rules.ts index 53ec23cc7..0ecea4b98 100644 --- a/lib/utils/rules.ts +++ b/lib/utils/rules.ts @@ -71,6 +71,7 @@ import preferStarQuantifier from "../rules/prefer-star-quantifier" import preferT from "../rules/prefer-t" import preferUnicodeCodepointEscapes from "../rules/prefer-unicode-codepoint-escapes" import preferW from "../rules/prefer-w" +import requireReduceNegation from "../rules/require-reduce-negation" import requireUnicodeRegexp from "../rules/require-unicode-regexp" import sortAlternatives from "../rules/sort-alternatives" import sortCharacterClassElements from "../rules/sort-character-class-elements" @@ -152,6 +153,7 @@ export const rules = [ preferT, preferUnicodeCodepointEscapes, preferW, + requireReduceNegation, requireUnicodeRegexp, sortAlternatives, sortCharacterClassElements, diff --git a/tests/lib/rules/require-reduce-negation.ts b/tests/lib/rules/require-reduce-negation.ts new file mode 100644 index 000000000..2d10a42d4 --- /dev/null +++ b/tests/lib/rules/require-reduce-negation.ts @@ -0,0 +1,178 @@ +import { RuleTester } from "eslint" +import rule from "../../../lib/rules/require-reduce-negation" + +const tester = new RuleTester({ + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + }, +}) + +tester.run("require-reduce-negation", rule as any, { + valid: [ + String.raw`/[[abc]]/v`, + String.raw`/[\d]/u`, + String.raw`/[^\d]/v`, // Converting to `\D` does not reduce negation, so ignore it. The `negation` rule handles it. + 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`/[^[^abc]]/v`, + output: String.raw`/[abc]/v`, + errors: [ + "This character class can be double negation elimination.", + ], + }, + { + code: String.raw`/[^\D]/u`, + output: String.raw`/[\d]/u`, + errors: [ + "This character class can be double negation elimination.", + ], + }, + { + code: String.raw`/[^\P{ASCII}]/u`, + output: String.raw`/[\p{ASCII}]/u`, + errors: [ + "This character class can be double negation elimination.", + ], + }, + { + code: String.raw`/[^[^\q{a|1|A}&&\w]]/v`, + output: String.raw`/[[\q{a|1|A}&&\w]]/v`, + errors: [ + "This character class can be double negation elimination.", + ], + }, + { + code: String.raw`/[a&&[^b]]/v`, + output: String.raw`/[a--b]/v`, + errors: ["This expression can be converted to the subtraction."], + }, + // FIXME: https://github.com/eslint-community/regexpp/pull/136 + // { + // code: String.raw`/[a&&b&&[^c]]/v`, + // output: String.raw`/[[a&&b]--c]/v`, + // errors: ["This character class 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`/[[b--a]&&c]/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."], + }, + // FIXME: https://github.com/eslint-community/regexpp/pull/136 + // { + // 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`/[^ab]/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`/[ab]/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`/[^ab\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`/[ab\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`/[^abcdef\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]]/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.", + ], + }, + ], +}) From 271e1f2b850c52fb991cc1533a6f2be4a56df149 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Mon, 11 Sep 2023 22:38:20 +0900 Subject: [PATCH 02/14] Create early-islands-press.md --- .changeset/early-islands-press.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/early-islands-press.md diff --git a/.changeset/early-islands-press.md b/.changeset/early-islands-press.md new file mode 100644 index 000000000..2f68c8ba3 --- /dev/null +++ b/.changeset/early-islands-press.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-regexp": minor +--- + +Add `regexp/require-reduce-negation` rule From 50b778e0b6379ece93a73d2721f256d20a0c08ab Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Mon, 11 Sep 2023 23:47:34 +0900 Subject: [PATCH 03/14] refactor --- lib/rules/require-reduce-negation.ts | 408 ++++++++++----------- tests/lib/rules/require-reduce-negation.ts | 8 + 2 files changed, 211 insertions(+), 205 deletions(-) diff --git a/lib/rules/require-reduce-negation.ts b/lib/rules/require-reduce-negation.ts index 476639668..6d03352fb 100644 --- a/lib/rules/require-reduce-negation.ts +++ b/lib/rules/require-reduce-negation.ts @@ -94,13 +94,59 @@ export default createRule("require-reduce-negation", { toNegationOfConjunction(ccNode) }, onExpressionCharacterClassEnter(eccNode) { - if (toNegationOfDisjunctionForCharacterClass(eccNode)) { + if (toNegationOfDisjunction(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: + | "doubleNegationElimination" + | "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: @@ -116,12 +162,9 @@ export default createRule("require-reduce-negation", { operand.type === "ClassIntersection" || operand.type === "ClassSubtraction" ) { - if (toNegationOfDisjunctionForExpression(operand)) { - return - } void ( - toSubtraction(operand, right) || - toIntersection(operand, right) + toSubtraction(operand, right, eccNode) || + toIntersection(operand, right, eccNode) ) right = operand.right operand = operand.left @@ -148,21 +191,11 @@ export default createRule("require-reduce-negation", { ) { return false } - const complementElement: NegatableCharacterClassElement = { - ...element, - negate: false, - } - - const us = toUnicodeSet(ccNode, flags) - const convertedUs = toUnicodeSet(complementElement, flags) - if (!us.equals(convertedUs)) { - return false - } - context.report({ - node, - loc: getRegexpLocation(ccNode), + return reportWhenFixedIsCompatible({ + reportNode: ccNode, + targetNode: ccNode, messageId: "doubleNegationElimination", - fix: fixReplaceNode(ccNode, () => { + fix: () => { let fixedElementText = getRawTextForNot(element) if ( element.type === "CharacterClass" && @@ -173,9 +206,8 @@ export default createRule("require-reduce-negation", { } return `[${fixedElementText}]` - }), + }, }) - return true // reported } /** @@ -186,99 +218,85 @@ export default createRule("require-reduce-negation", { * e.g. * - `[[^a]&&[^b]]` -> `[^ab]` * - `[^[^a]&&[^b]]` -> `[ab]` - */ - function toNegationOfDisjunctionForCharacterClass( - eccNode: ExpressionCharacterClass, - ) { - return toNegationOfDisjunction( - eccNode.expression, - eccNode, - (fixedElements) => { - return `[${eccNode.negate ? "" : "^"}${fixedElements}]` - }, - ) - } - - /** - * Checks the given expression 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]&&c]` -> `[[^ab]&&c]` */ - function toNegationOfDisjunctionForExpression( - expression: ClassIntersection | ClassSubtraction, - ) { - return toNegationOfDisjunction( - expression, - expression, - (fixedElements) => { - return `[^${fixedElements}]` - }, - ) - } - - /** - * Checks the given expression and reports if it can be converted to the negation of a disjunction - * using De Morgan's laws. - * Returns true if reported. - */ function toNegationOfDisjunction( - expression: ClassIntersection | ClassSubtraction, - targetNode: - | ExpressionCharacterClass - | ClassIntersection - | ClassSubtraction, - postFix: (fixedElements: string) => string, + eccNode: ExpressionCharacterClass, ) { + const expression = eccNode.expression if (expression.type !== "ClassIntersection") { return false } const operands: ClassSetOperand[] = [] + const intersections = [expression] let operand: ClassIntersection | ClassSetOperand = expression while (operand.type === "ClassIntersection") { + intersections.unshift(operand) operands.unshift(operand.right) operand = operand.left } operands.unshift(operand) - const elements = operands - .filter(isNegatableCharacterClassElement) - .filter((e) => e.negate) - if (elements.length !== operands.length) { - return false - } - const us = toUnicodeSet(targetNode, flags) - const fixedElements = elements.map((element) => { - let fixedElementText = getRawTextForNot(element) - if (element.type === "CharacterClass" && element.negate) { - // Remove brackets - fixedElementText = fixedElementText.slice(1, -1) + const elements: (NegatableCharacterClassElement & + ClassSetOperand)[] = [] + const others: ClassSetOperand[] = [] + for (const e of operands) { + if (isNegatableCharacterClassElement(e) && e.negate) { + elements.push(e) + } else { + others.push(e) } - return fixedElementText - }) - const fixedText = postFix(fixedElements.join("")) - const convertedElement = getParsedElement(fixedText, flags) - if (!convertedElement) { - return false } - const convertedUs = toUnicodeSet(convertedElement, flags) - if (!us.equals(convertedUs)) { - return false + const fixedElements = elements + .map((element) => { + let fixedElementText = getRawTextForNot(element) + if ( + element.type === "CharacterClass" && + element.negate + ) { + // Remove brackets + fixedElementText = fixedElementText.slice(1, -1) + } + return fixedElementText + }) + .join("") + if (elements.length === operands.length) { + return reportWhenFixedIsCompatible({ + reportNode: eccNode, + targetNode: eccNode, + messageId: "toNegationOfDisjunction", + data: { + target: "character class", + }, + fix: () => + `[${eccNode.negate ? "" : "^"}${fixedElements}]`, + }) } - context.report({ - node, - loc: getRegexpLocation(targetNode), + if (elements.length < 2) { + return null + } + return reportWhenFixedIsCompatible({ + reportNode: intersections.find((intersection) => + elements.every( + (element) => + intersection.start <= element.start && + element.end <= intersection.end, + ), + )!, + targetNode: eccNode, messageId: "toNegationOfDisjunction", data: { - target: - targetNode.type === "ExpressionCharacterClass" - ? "character class" - : "expression", + target: "expression", + }, + fix: () => { + const operandTestList = [ + `[^${fixedElements}]`, + ...others.map((e) => e.raw), + ] + return `[${ + eccNode.negate ? "^" : "" + }${operandTestList.join("&&")}]` }, - fix: fixReplaceNode(targetNode, fixedText), }) - return true // reported } /** @@ -300,37 +318,28 @@ export default createRule("require-reduce-negation", { if (elements.length !== operands.length) { return false } - const us = toUnicodeSet(ccNode, flags) - const fixedElements = elements.map((element) => { - let fixedElementText = getRawTextForNot(element) - if ( - element.type === "CharacterClass" && - element.negate && - element.elements.length === 1 - ) { - // Remove brackets - fixedElementText = fixedElementText.slice(1, -1) - } - return fixedElementText - }) - const fixedText = `[${ - ccNode.negate ? "" : "^" - }${fixedElements.join("&&")}]` - 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(ccNode), + return reportWhenFixedIsCompatible({ + reportNode: ccNode, + targetNode: ccNode, messageId: "toNegationOfConjunction", - fix: fixReplaceNode(ccNode, fixedText), + fix: () => { + const fixedElements = elements.map((element) => { + let fixedElementText = getRawTextForNot(element) + if ( + element.type === "CharacterClass" && + element.negate && + element.elements.length === 1 + ) { + // Remove brackets + fixedElementText = fixedElementText.slice(1, -1) + } + return fixedElementText + }) + return `[${ + ccNode.negate ? "" : "^" + }${fixedElements.join("&&")}]` + }, }) - return true // reported } /** @@ -348,6 +357,7 @@ export default createRule("require-reduce-negation", { | ClassSubtraction | ClassSetOperand | null, + eccNode: ExpressionCharacterClass, ) { if (expression.type !== "ClassIntersection") { return false @@ -368,44 +378,39 @@ export default createRule("require-reduce-negation", { } else { return false } - const us = toUnicodeSet(expression, flags) - let fixedLeftText = fixedLeft.raw - if (fixedLeft.type === "ClassIntersection") { - // Wrap with brackets - fixedLeftText = `[${fixedLeftText}]` - } - let fixedRightText = getRawTextForNot(fixedRight) - if ( - fixedRight.type === "CharacterClass" && - fixedRight.negate && - fixedRight.elements.length === 1 - ) { - // Remove brackets - fixedRightText = fixedRightText.slice(1, -1) - } - let fixedText = `${fixedLeftText}--${fixedRightText}` - if (expressionRight) { - // Wrap with brackets - fixedText = `[${fixedText}]` - } - 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(expression), + return reportWhenFixedIsCompatible({ + reportNode: expression, + targetNode: eccNode, messageId: "toSubtraction", - fix: fixReplaceNode(expression, fixedText), + fix() { + let fixedLeftText = fixedLeft.raw + if (fixedLeft.type === "ClassIntersection") { + // Wrap with brackets + fixedLeftText = `[${fixedLeftText}]` + } + let fixedRightText = getRawTextForNot(fixedRight) + if ( + fixedRight.type === "CharacterClass" && + fixedRight.negate && + fixedRight.elements.length === 1 + ) { + // Remove brackets + fixedRightText = fixedRightText.slice(1, -1) + } + 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 true // reported } /** @@ -422,6 +427,7 @@ export default createRule("require-reduce-negation", { | ClassSubtraction | ClassSetOperand | null, + eccNode: ExpressionCharacterClass, ) { if (expression.type !== "ClassSubtraction") { return false @@ -430,47 +436,40 @@ export default createRule("require-reduce-negation", { if (!isNegatableCharacterClassElement(right) || !right.negate) { return false } - - const us = toUnicodeSet(expression, flags) - let fixedLeftText = left.raw - if (left.type === "ClassSubtraction") { - // Wrap with brackets - fixedLeftText = `[${fixedLeftText}]` - } - let fixedRightText = getRawTextForNot(right) - if ( - right.type === "CharacterClass" && - right.negate && - right.elements.length === 1 - ) { - // Remove brackets - fixedRightText = fixedRightText.slice(1, -1) - } - let fixedText = `${fixedLeftText}&&${fixedRightText}` - - if (expressionRight) { - // Wrap with brackets - fixedText = `[${fixedText}]` - } - 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(expression), + return reportWhenFixedIsCompatible({ + reportNode: expression, + targetNode: eccNode, messageId: "toIntersection", - fix: fixReplaceNode(expression, fixedText), + fix() { + let fixedLeftText = left.raw + if (left.type === "ClassSubtraction") { + // Wrap with brackets + fixedLeftText = `[${fixedLeftText}]` + } + let fixedRightText = getRawTextForNot(right) + if ( + right.type === "CharacterClass" && + right.negate && + right.elements.length === 1 + ) { + // Remove brackets + fixedRightText = fixedRightText.slice(1, -1) + } + 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 true // reported } } @@ -499,14 +498,13 @@ function getParsedElement( 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" + element.type !== "Assertion" && + element.type !== "Quantifier" && + element.type !== "CapturingGroup" && + element.type !== "Group" && + element.type !== "Backreference" ) - return null - return element + return element } } catch (_error) { // ignore diff --git a/tests/lib/rules/require-reduce-negation.ts b/tests/lib/rules/require-reduce-negation.ts index 2d10a42d4..09ebe6910 100644 --- a/tests/lib/rules/require-reduce-negation.ts +++ b/tests/lib/rules/require-reduce-negation.ts @@ -146,6 +146,14 @@ tester.run("require-reduce-negation", rule as any, { "This character class can be converted to the negation of a disjunction using De Morgan's laws.", ], }, + // FIXME: https://github.com/eslint-community/regexpp/pull/136 + // { + // code: String.raw`/[[^a]&&[b]&&[^c]]/v`, + // output: String.raw`/[[^ac]&&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`, From 2ecb64c74260ee4d4a0e8b23f67ed49fb971bb2a Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Tue, 12 Sep 2023 08:53:15 +0900 Subject: [PATCH 04/14] update regexpp and test cases --- package-lock.json | 18 +++++------ package.json | 2 +- tests/lib/rules/require-reduce-negation.ts | 37 ++++++++++------------ 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7bf425756..80ef53340 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "eslint-plugin-regexp", - "version": "2.0.0-next.0", + "version": "2.0.0-next.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "eslint-plugin-regexp", - "version": "2.0.0-next.0", + "version": "2.0.0-next.3", "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", @@ -1861,9 +1861,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" } @@ -12776,9 +12776,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 af53a8432..9ddf4d57e 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/require-reduce-negation.ts b/tests/lib/rules/require-reduce-negation.ts index 09ebe6910..e51f677e0 100644 --- a/tests/lib/rules/require-reduce-negation.ts +++ b/tests/lib/rules/require-reduce-negation.ts @@ -57,12 +57,11 @@ tester.run("require-reduce-negation", rule as any, { output: String.raw`/[a--b]/v`, errors: ["This expression can be converted to the subtraction."], }, - // FIXME: https://github.com/eslint-community/regexpp/pull/136 - // { - // code: String.raw`/[a&&b&&[^c]]/v`, - // output: String.raw`/[[a&&b]--c]/v`, - // errors: ["This character class 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--b]&&c]/v`, @@ -93,12 +92,11 @@ tester.run("require-reduce-negation", rule as any, { output: String.raw`/[[a&&b]--c]/v`, errors: ["This expression can be converted to the intersection."], }, - // FIXME: https://github.com/eslint-community/regexpp/pull/136 - // { - // 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`, @@ -146,14 +144,13 @@ tester.run("require-reduce-negation", rule as any, { "This character class can be converted to the negation of a disjunction using De Morgan's laws.", ], }, - // FIXME: https://github.com/eslint-community/regexpp/pull/136 - // { - // code: String.raw`/[[^a]&&[b]&&[^c]]/v`, - // output: String.raw`/[[^ac]&&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]&&[^c]]/v`, + output: String.raw`/[[^ac]&&[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`, From 1368dfcd1a726dbfbb99d208506f5e7e3054d179 Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Tue, 12 Sep 2023 09:45:18 +0900 Subject: [PATCH 05/14] fix for auto-fix --- lib/rules/require-reduce-negation.ts | 102 +++++++++------------ tests/lib/rules/require-reduce-negation.ts | 21 ++++- 2 files changed, 60 insertions(+), 63 deletions(-) diff --git a/lib/rules/require-reduce-negation.ts b/lib/rules/require-reduce-negation.ts index 6d03352fb..42f84bd3e 100644 --- a/lib/rules/require-reduce-negation.ts +++ b/lib/rules/require-reduce-negation.ts @@ -54,6 +54,20 @@ function getRawTextForNot(node: NegatableCharacterClassElement) { }${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 +} + export default createRule("require-reduce-negation", { meta: { docs: { @@ -97,6 +111,9 @@ export default createRule("require-reduce-negation", { if (toNegationOfDisjunction(eccNode)) { return } + if (toSubtraction(eccNode)) { + return + } verifyExpressions(eccNode) }, } @@ -162,10 +179,7 @@ export default createRule("require-reduce-negation", { operand.type === "ClassIntersection" || operand.type === "ClassSubtraction" ) { - void ( - toSubtraction(operand, right, eccNode) || - toIntersection(operand, right, eccNode) - ) + toIntersection(operand, right, eccNode) right = operand.right operand = operand.left } @@ -227,15 +241,7 @@ export default createRule("require-reduce-negation", { if (expression.type !== "ClassIntersection") { return false } - const operands: ClassSetOperand[] = [] - const intersections = [expression] - let operand: ClassIntersection | ClassSetOperand = expression - while (operand.type === "ClassIntersection") { - intersections.unshift(operand) - operands.unshift(operand.right) - operand = operand.left - } - operands.unshift(operand) + const operands = collectIntersectionOperands(expression) const elements: (NegatableCharacterClassElement & ClassSetOperand)[] = [] const others: ClassSetOperand[] = [] @@ -275,13 +281,8 @@ export default createRule("require-reduce-negation", { return null } return reportWhenFixedIsCompatible({ - reportNode: intersections.find((intersection) => - elements.every( - (element) => - intersection.start <= element.start && - element.end <= intersection.end, - ), - )!, + reportNode: elements[elements.length - 1] + .parent as ClassIntersection, targetNode: eccNode, messageId: "toNegationOfDisjunction", data: { @@ -349,33 +350,18 @@ export default createRule("require-reduce-negation", { * e.g. * - `[a&&[^b]]` -> `[a--b]` * - `[[^a]&&b]` -> `[b--a]` + * - `[a&&[^b]&&c]` -> `[[a&&c]--b]` */ - function toSubtraction( - expression: ClassIntersection | ClassSubtraction, - expressionRight: - | ClassIntersection - | ClassSubtraction - | ClassSetOperand - | null, - eccNode: ExpressionCharacterClass, - ) { + function toSubtraction(eccNode: ExpressionCharacterClass) { + const expression = eccNode.expression if (expression.type !== "ClassIntersection") { return false } - const { left, right } = expression - - let fixedLeft: ClassSetOperand | ClassIntersection, - fixedRight: ClassSetOperand & NegatableCharacterClassElement - if (isNegatableCharacterClassElement(left) && left.negate) { - fixedLeft = right - fixedRight = left - } else if ( - isNegatableCharacterClassElement(right) && - right.negate - ) { - fixedLeft = left - fixedRight = right - } else { + const operands = collectIntersectionOperands(expression) + const negativeOperand = operands + .filter(isNegatableCharacterClassElement) + .find((e) => e.negate) + if (!negativeOperand) { return false } return reportWhenFixedIsCompatible({ @@ -383,32 +369,26 @@ export default createRule("require-reduce-negation", { targetNode: eccNode, messageId: "toSubtraction", fix() { - let fixedLeftText = fixedLeft.raw - if (fixedLeft.type === "ClassIntersection") { + const others = operands.filter( + (e) => e !== negativeOperand, + ) + let fixedLeftText = others.map((e) => e.raw).join("&&") + if (others.length >= 2) { // Wrap with brackets fixedLeftText = `[${fixedLeftText}]` } - let fixedRightText = getRawTextForNot(fixedRight) + let fixedRightText = getRawTextForNot(negativeOperand) if ( - fixedRight.type === "CharacterClass" && - fixedRight.negate && - fixedRight.elements.length === 1 + negativeOperand.type === "CharacterClass" && + negativeOperand.negate && + negativeOperand.elements.length === 1 ) { // Remove brackets fixedRightText = fixedRightText.slice(1, -1) } - 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 `[${ + eccNode.negate ? "^" : "" + }${`${fixedLeftText}--${fixedRightText}`}]` }, }) } diff --git a/tests/lib/rules/require-reduce-negation.ts b/tests/lib/rules/require-reduce-negation.ts index e51f677e0..642316ab8 100644 --- a/tests/lib/rules/require-reduce-negation.ts +++ b/tests/lib/rules/require-reduce-negation.ts @@ -64,12 +64,17 @@ tester.run("require-reduce-negation", rule as any, { }, { code: String.raw`/[a&&[^b]&&c]/v`, - output: 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--a]&&c]/v`, + output: String.raw`/[[b&&c]--a]/v`, errors: ["This expression can be converted to the subtraction."], }, { @@ -179,5 +184,17 @@ tester.run("require-reduce-negation", rule as any, { "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`/[[^bc]&&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."], + }, ], }) From 214a4f60f6b8422cbb46aee921e6e99b85e2967a Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Tue, 12 Sep 2023 12:03:20 +0900 Subject: [PATCH 06/14] fix doc --- docs/rules/require-reduce-negation.md | 32 +++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/docs/rules/require-reduce-negation.md b/docs/rules/require-reduce-negation.md index b13d7e0a1..096250a03 100644 --- a/docs/rules/require-reduce-negation.md +++ b/docs/rules/require-reduce-negation.md @@ -24,7 +24,7 @@ This rule is aimed at optimizing patterns by reducing the negation (complement) /* eslint regexp/require-reduce-negation: "error" */ /* ✗ BAD */ -var re = /[^[^abc]]/v; // -> /[[abc]]/v +var re = /[^[^abc]]/v; // -> /[abc]/v var re = /[^\D]/u; // -> /[\d]/u var re = /[a&&[^b]]/v; // -> /[a--b]/v var re = /[[^b]&&a]/v; // -> /[a--b]/v @@ -33,7 +33,7 @@ var re = /[[^a]&&[^b]]/v; // -> /[^ab]/v var re = /[[^a][^b]]/v; // -> /[^a&&b]/v /* ✓ GOOD */ -var re = /[[abc]]/v; +var re = /[abc]/v; var re = /[\d]/u; var re = /[\D]/u; var re = /[a--b]/v; @@ -44,6 +44,34 @@ var re = /[^a&&b]/v; +This rule attempts to reduce complements in several ways: + +### Double negation elimination + +This rule look for patterns that can eliminate double negatives, report on them, and auto-fix them.\ +For example, `/[^[^abc]]/v` is equivalent to `/[abc]/v`. + +See . + +### 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, auto-fix them.\ +For example, `/[[^a]&&[^b]]/v` is equivalent to `/[^ab]/v`, `/[[^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`. + ## :wrench: Options Nothing. From 7fec1991b3d4161ed15d3be65ff1dccd1c2a2615 Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Tue, 12 Sep 2023 13:30:15 +0900 Subject: [PATCH 07/14] refactor --- lib/rules/require-reduce-negation.ts | 129 +++++++++++++-------------- 1 file changed, 62 insertions(+), 67 deletions(-) diff --git a/lib/rules/require-reduce-negation.ts b/lib/rules/require-reduce-negation.ts index 42f84bd3e..0ab35c11e 100644 --- a/lib/rules/require-reduce-negation.ts +++ b/lib/rules/require-reduce-negation.ts @@ -20,6 +20,9 @@ type NegatableCharacterClassElement = | ExpressionCharacterClass | EscapeCharacterSet | CharacterUnicodePropertyCharacterSet +type NegateCharacterClassElement = NegatableCharacterClassElement & { + negate: true +} /** Checks whether the given character class is negatable. */ function isNegatableCharacterClassElement< @@ -33,25 +36,27 @@ function isNegatableCharacterClassElement< ) } +/** 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 getRawTextForNot(node: NegatableCharacterClassElement) { - const raw = node.raw +function getRawTextToNot(negateNode: NegateCharacterClassElement) { + const raw = negateNode.raw if ( - node.type === "CharacterClass" || - node.type === "ExpressionCharacterClass" + negateNode.type === "CharacterClass" || + negateNode.type === "ExpressionCharacterClass" ) { - if (node.negate) { - return `${raw[0]}${raw.slice(2)}` - } - return `${raw[0]}^${raw.slice(1)}` + return `${raw[0]}${raw.slice(2)}` } // else if (node.type === "CharacterSet") { - const escapeChar = node.raw[1] - return `${raw[0]}${ - node.negate ? escapeChar.toLowerCase() : escapeChar.toUpperCase() - }${raw.slice(2)}` + const escapeChar = negateNode.raw[1].toLowerCase() + return `${raw[0]}${escapeChar}${raw.slice(2)}` } /** Collect the operands from the given intersection expression */ @@ -199,10 +204,7 @@ export default createRule("require-reduce-negation", { return false } const element = ccNode.elements[0] - if ( - !isNegatableCharacterClassElement(element) || - !element.negate - ) { + if (!isNegate(element)) { return false } return reportWhenFixedIsCompatible({ @@ -210,11 +212,8 @@ export default createRule("require-reduce-negation", { targetNode: ccNode, messageId: "doubleNegationElimination", fix: () => { - let fixedElementText = getRawTextForNot(element) - if ( - element.type === "CharacterClass" && - element.negate - ) { + let fixedElementText = getRawTextToNot(element) + if (element.type === "CharacterClass") { // Remove brackets fixedElementText = fixedElementText.slice(1, -1) } @@ -242,30 +241,27 @@ export default createRule("require-reduce-negation", { return false } const operands = collectIntersectionOperands(expression) - const elements: (NegatableCharacterClassElement & + const negateOperands: (NegateCharacterClassElement & ClassSetOperand)[] = [] const others: ClassSetOperand[] = [] for (const e of operands) { - if (isNegatableCharacterClassElement(e) && e.negate) { - elements.push(e) + if (isNegate(e)) { + negateOperands.push(e) } else { others.push(e) } } - const fixedElements = elements - .map((element) => { - let fixedElementText = getRawTextForNot(element) - if ( - element.type === "CharacterClass" && - element.negate - ) { + const fixedOperands = negateOperands + .map((negateOperand) => { + let fixedText = getRawTextToNot(negateOperand) + if (negateOperand.type === "CharacterClass") { // Remove brackets - fixedElementText = fixedElementText.slice(1, -1) + fixedText = fixedText.slice(1, -1) } - return fixedElementText + return fixedText }) .join("") - if (elements.length === operands.length) { + if (negateOperands.length === operands.length) { return reportWhenFixedIsCompatible({ reportNode: eccNode, targetNode: eccNode, @@ -274,14 +270,14 @@ export default createRule("require-reduce-negation", { target: "character class", }, fix: () => - `[${eccNode.negate ? "" : "^"}${fixedElements}]`, + `[${eccNode.negate ? "" : "^"}${fixedOperands}]`, }) } - if (elements.length < 2) { + if (negateOperands.length < 2) { return null } return reportWhenFixedIsCompatible({ - reportNode: elements[elements.length - 1] + reportNode: negateOperands[negateOperands.length - 1] .parent as ClassIntersection, targetNode: eccNode, messageId: "toNegationOfDisjunction", @@ -290,7 +286,7 @@ export default createRule("require-reduce-negation", { }, fix: () => { const operandTestList = [ - `[^${fixedElements}]`, + `[^${fixedOperands}]`, ...others.map((e) => e.raw), ] return `[${ @@ -312,11 +308,9 @@ export default createRule("require-reduce-negation", { if (ccNode.elements.length <= 1 || !flags.unicodeSets) { return false } - const operands: CharacterClassElement[] = ccNode.elements - const elements = operands - .filter(isNegatableCharacterClassElement) - .filter((e) => e.negate) - if (elements.length !== operands.length) { + const elements: CharacterClassElement[] = ccNode.elements + const negateElements = elements.filter(isNegate) + if (negateElements.length !== elements.length) { return false } return reportWhenFixedIsCompatible({ @@ -324,18 +318,23 @@ export default createRule("require-reduce-negation", { targetNode: ccNode, messageId: "toNegationOfConjunction", fix: () => { - const fixedElements = elements.map((element) => { - let fixedElementText = getRawTextForNot(element) - if ( - element.type === "CharacterClass" && - element.negate && - element.elements.length === 1 - ) { - // Remove brackets - fixedElementText = fixedElementText.slice(1, -1) - } - return fixedElementText - }) + const fixedElements = negateElements.map( + (negateElement) => { + let fixedElementText = + getRawTextToNot(negateElement) + if ( + negateElement.type === "CharacterClass" && + negateElement.elements.length === 1 + ) { + // Remove brackets + fixedElementText = fixedElementText.slice( + 1, + -1, + ) + } + return fixedElementText + }, + ) return `[${ ccNode.negate ? "" : "^" }${fixedElements.join("&&")}]` @@ -358,10 +357,8 @@ export default createRule("require-reduce-negation", { return false } const operands = collectIntersectionOperands(expression) - const negativeOperand = operands - .filter(isNegatableCharacterClassElement) - .find((e) => e.negate) - if (!negativeOperand) { + const negateOperand = operands.find(isNegate) + if (!negateOperand) { return false } return reportWhenFixedIsCompatible({ @@ -370,18 +367,17 @@ export default createRule("require-reduce-negation", { messageId: "toSubtraction", fix() { const others = operands.filter( - (e) => e !== negativeOperand, + (e) => e !== negateOperand, ) let fixedLeftText = others.map((e) => e.raw).join("&&") if (others.length >= 2) { // Wrap with brackets fixedLeftText = `[${fixedLeftText}]` } - let fixedRightText = getRawTextForNot(negativeOperand) + let fixedRightText = getRawTextToNot(negateOperand) if ( - negativeOperand.type === "CharacterClass" && - negativeOperand.negate && - negativeOperand.elements.length === 1 + negateOperand.type === "CharacterClass" && + negateOperand.elements.length === 1 ) { // Remove brackets fixedRightText = fixedRightText.slice(1, -1) @@ -413,7 +409,7 @@ export default createRule("require-reduce-negation", { return false } const { left, right } = expression - if (!isNegatableCharacterClassElement(right) || !right.negate) { + if (!isNegate(right)) { return false } return reportWhenFixedIsCompatible({ @@ -426,10 +422,9 @@ export default createRule("require-reduce-negation", { // Wrap with brackets fixedLeftText = `[${fixedLeftText}]` } - let fixedRightText = getRawTextForNot(right) + let fixedRightText = getRawTextToNot(right) if ( right.type === "CharacterClass" && - right.negate && right.elements.length === 1 ) { // Remove brackets From 2f3fe7f37a06d8ab0868361cf658a1185450871e Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Fri, 15 Sep 2023 13:20:59 +0900 Subject: [PATCH 08/14] fix --- docs/rules/require-reduce-negation.md | 17 +++++-- lib/rules/require-reduce-negation.ts | 53 ++-------------------- tests/lib/rules/require-reduce-negation.ts | 41 +++++++++-------- 3 files changed, 39 insertions(+), 72 deletions(-) diff --git a/docs/rules/require-reduce-negation.md b/docs/rules/require-reduce-negation.md index 096250a03..d52e58c58 100644 --- a/docs/rules/require-reduce-negation.md +++ b/docs/rules/require-reduce-negation.md @@ -44,34 +44,41 @@ var re = /[^a&&b]/v; +### How does this rule work? + This rule attempts to reduce complements in several ways: -### Double negation elimination +#### Double negation elimination This rule look for patterns that can eliminate double negatives, report on them, and auto-fix them.\ For example, `/[^[^abc]]/v` is equivalent to `/[abc]/v`. See . -### De Morgan's laws +#### 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, auto-fix them.\ For example, `/[[^a]&&[^b]]/v` is equivalent to `/[^ab]/v`, `/[[^a][^b]]/v` is equivalent to `/[^a&&b]/v`. See . -### Conversion from the intersection to the subtraction +#### 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 +#### 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. @@ -79,8 +86,10 @@ 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 diff --git a/lib/rules/require-reduce-negation.ts b/lib/rules/require-reduce-negation.ts index 0ab35c11e..ecf84d620 100644 --- a/lib/rules/require-reduce-negation.ts +++ b/lib/rules/require-reduce-negation.ts @@ -211,15 +211,7 @@ export default createRule("require-reduce-negation", { reportNode: ccNode, targetNode: ccNode, messageId: "doubleNegationElimination", - fix: () => { - let fixedElementText = getRawTextToNot(element) - if (element.type === "CharacterClass") { - // Remove brackets - fixedElementText = fixedElementText.slice(1, -1) - } - - return `[${fixedElementText}]` - }, + fix: () => `[${getRawTextToNot(element)}]`, }) } @@ -252,14 +244,7 @@ export default createRule("require-reduce-negation", { } } const fixedOperands = negateOperands - .map((negateOperand) => { - let fixedText = getRawTextToNot(negateOperand) - if (negateOperand.type === "CharacterClass") { - // Remove brackets - fixedText = fixedText.slice(1, -1) - } - return fixedText - }) + .map((negateOperand) => getRawTextToNot(negateOperand)) .join("") if (negateOperands.length === operands.length) { return reportWhenFixedIsCompatible({ @@ -319,21 +304,7 @@ export default createRule("require-reduce-negation", { messageId: "toNegationOfConjunction", fix: () => { const fixedElements = negateElements.map( - (negateElement) => { - let fixedElementText = - getRawTextToNot(negateElement) - if ( - negateElement.type === "CharacterClass" && - negateElement.elements.length === 1 - ) { - // Remove brackets - fixedElementText = fixedElementText.slice( - 1, - -1, - ) - } - return fixedElementText - }, + (negateElement) => getRawTextToNot(negateElement), ) return `[${ ccNode.negate ? "" : "^" @@ -374,14 +345,7 @@ export default createRule("require-reduce-negation", { // Wrap with brackets fixedLeftText = `[${fixedLeftText}]` } - let fixedRightText = getRawTextToNot(negateOperand) - if ( - negateOperand.type === "CharacterClass" && - negateOperand.elements.length === 1 - ) { - // Remove brackets - fixedRightText = fixedRightText.slice(1, -1) - } + const fixedRightText = getRawTextToNot(negateOperand) return `[${ eccNode.negate ? "^" : "" }${`${fixedLeftText}--${fixedRightText}`}]` @@ -422,14 +386,7 @@ export default createRule("require-reduce-negation", { // Wrap with brackets fixedLeftText = `[${fixedLeftText}]` } - let fixedRightText = getRawTextToNot(right) - if ( - right.type === "CharacterClass" && - right.elements.length === 1 - ) { - // Remove brackets - fixedRightText = fixedRightText.slice(1, -1) - } + const fixedRightText = getRawTextToNot(right) let fixedText = `${fixedLeftText}&&${fixedRightText}` if (expressionRight) { diff --git a/tests/lib/rules/require-reduce-negation.ts b/tests/lib/rules/require-reduce-negation.ts index 642316ab8..22d9e3b86 100644 --- a/tests/lib/rules/require-reduce-negation.ts +++ b/tests/lib/rules/require-reduce-negation.ts @@ -13,6 +13,7 @@ tester.run("require-reduce-negation", rule as any, { String.raw`/[[abc]]/v`, String.raw`/[\d]/u`, String.raw`/[^\d]/v`, // Converting to `\D` does not reduce negation, so ignore it. The `negation` rule handles it. + String.raw`/[^\P{ASCII}]/iu`, String.raw`/[a--b]/v`, String.raw`/[a&&b]/v`, String.raw`/[^ab]/v`, @@ -26,7 +27,7 @@ tester.run("require-reduce-negation", rule as any, { invalid: [ { code: String.raw`/[^[^abc]]/v`, - output: String.raw`/[abc]/v`, + output: String.raw`/[[abc]]/v`, errors: [ "This character class can be double negation elimination.", ], @@ -54,32 +55,32 @@ tester.run("require-reduce-negation", rule as any, { }, { code: String.raw`/[a&&[^b]]/v`, - output: 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`, + 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`, + 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`, + 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`, + 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`, + output: String.raw`/[a--[b]]/v`, errors: ["This expression can be converted to the subtraction."], }, { @@ -89,17 +90,17 @@ tester.run("require-reduce-negation", rule as any, { }, { code: String.raw`/[a--[^b]]/v`, - output: 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`, + 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`, + output: String.raw`/[[a--b]&&[c]]/v`, errors: ["This expression can be converted to the intersection."], }, { @@ -109,56 +110,56 @@ tester.run("require-reduce-negation", rule as any, { }, { code: String.raw`/[[^a]&&[^b]]/v`, - output: String.raw`/[^ab]/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`/[ab]/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`/[^ab\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`/[ab\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`, + 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`/[^abcdef\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`/[[^ac]&&[b]]/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`, + output: String.raw`/[^[a]&&[b]]/v`, errors: [ "This character class can be converted to the negation of a conjunction using De Morgan's laws.", ], @@ -172,7 +173,7 @@ tester.run("require-reduce-negation", rule as any, { }, { code: String.raw`/[^[^a][^b]]/v`, - output: 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.", ], @@ -186,7 +187,7 @@ tester.run("require-reduce-negation", rule as any, { }, { code: String.raw`/[a&&[^b]&&[^c]&&d]/v`, - output: String.raw`/[[^bc]&&a&&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.", ], From 42504014e866cf4ef71cfd4b9b13e701494fed14 Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Fri, 22 Sep 2023 16:15:40 +0900 Subject: [PATCH 09/14] Moved some checks to `negation` rule. --- .changeset/early-islands-press2.md | 5 ++ docs/rules/require-reduce-negation.md | 17 ++---- lib/rules/negation.ts | 65 +++++++++++++++------- lib/rules/require-reduce-negation.ts | 28 ---------- tests/lib/rules/negation.ts | 42 ++++++++++++++ tests/lib/rules/require-reduce-negation.ts | 31 +---------- 6 files changed, 98 insertions(+), 90 deletions(-) create mode 100644 .changeset/early-islands-press2.md 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/docs/rules/require-reduce-negation.md b/docs/rules/require-reduce-negation.md index d52e58c58..47252b5b2 100644 --- a/docs/rules/require-reduce-negation.md +++ b/docs/rules/require-reduce-negation.md @@ -18,14 +18,15 @@ description: "require to reduce negation of character classes" This rule is aimed at optimizing patterns by reducing the negation (complement) representation of 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 [regexp/negation] rule together. + ```js /* eslint regexp/require-reduce-negation: "error" */ /* ✗ BAD */ -var re = /[^[^abc]]/v; // -> /[abc]/v -var re = /[^\D]/u; // -> /[\d]/u var re = /[a&&[^b]]/v; // -> /[a--b]/v var re = /[[^b]&&a]/v; // -> /[a--b]/v var re = /[a--[^b]]/v; // -> /[a&&b]/v @@ -33,9 +34,6 @@ var re = /[[^a]&&[^b]]/v; // -> /[^ab]/v var re = /[[^a][^b]]/v; // -> /[^a&&b]/v /* ✓ GOOD */ -var re = /[abc]/v; -var re = /[\d]/u; -var re = /[\D]/u; var re = /[a--b]/v; var re = /[a&&b]/v; var re = /[^ab]/v; @@ -46,14 +44,7 @@ var re = /[^a&&b]/v; ### How does this rule work? -This rule attempts to reduce complements in several ways: - -#### Double negation elimination - -This rule look for patterns that can eliminate double negatives, report on them, and auto-fix them.\ -For example, `/[^[^abc]]/v` is equivalent to `/[abc]/v`. - -See . +This rule attempts to reduce complements in the ways listed below: #### De Morgan's laws 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/require-reduce-negation.ts b/lib/rules/require-reduce-negation.ts index ecf84d620..dc99ccab4 100644 --- a/lib/rules/require-reduce-negation.ts +++ b/lib/rules/require-reduce-negation.ts @@ -107,9 +107,6 @@ export default createRule("require-reduce-negation", { regexpContext return { onCharacterClassEnter(ccNode) { - if (doubleNegationElimination(ccNode)) { - return - } toNegationOfConjunction(ccNode) }, onExpressionCharacterClassEnter(eccNode) { @@ -190,31 +187,6 @@ export default createRule("require-reduce-negation", { } } - /** - * Checks the given character class and reports if double negation elimination - * is possible. - * Returns true if reported. - * - * e.g. - * - `[^[^abc]]` -> `[abc]` - * - `[^\D]` -> `[\d]` - */ - function doubleNegationElimination(ccNode: CharacterClass) { - if (!ccNode.negate && ccNode.elements.length !== 1) { - return false - } - const element = ccNode.elements[0] - if (!isNegate(element)) { - return false - } - return reportWhenFixedIsCompatible({ - reportNode: ccNode, - targetNode: ccNode, - messageId: "doubleNegationElimination", - fix: () => `[${getRawTextToNot(element)}]`, - }) - } - /** * Checks the given character class and reports if it can be converted to the negation of a disjunction * using De Morgan's laws. 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/require-reduce-negation.ts b/tests/lib/rules/require-reduce-negation.ts index 22d9e3b86..f33fb1212 100644 --- a/tests/lib/rules/require-reduce-negation.ts +++ b/tests/lib/rules/require-reduce-negation.ts @@ -12,8 +12,7 @@ tester.run("require-reduce-negation", rule as any, { valid: [ String.raw`/[[abc]]/v`, String.raw`/[\d]/u`, - String.raw`/[^\d]/v`, // Converting to `\D` does not reduce negation, so ignore it. The `negation` rule handles it. - String.raw`/[^\P{ASCII}]/iu`, + String.raw`/[^\d]/v`, String.raw`/[a--b]/v`, String.raw`/[a&&b]/v`, String.raw`/[^ab]/v`, @@ -25,34 +24,6 @@ tester.run("require-reduce-negation", rule as any, { String.raw`/[a--b--[c]]/v`, ], invalid: [ - { - code: String.raw`/[^[^abc]]/v`, - output: String.raw`/[[abc]]/v`, - errors: [ - "This character class can be double negation elimination.", - ], - }, - { - code: String.raw`/[^\D]/u`, - output: String.raw`/[\d]/u`, - errors: [ - "This character class can be double negation elimination.", - ], - }, - { - code: String.raw`/[^\P{ASCII}]/u`, - output: String.raw`/[\p{ASCII}]/u`, - errors: [ - "This character class can be double negation elimination.", - ], - }, - { - code: String.raw`/[^[^\q{a|1|A}&&\w]]/v`, - output: String.raw`/[[\q{a|1|A}&&\w]]/v`, - errors: [ - "This character class can be double negation elimination.", - ], - }, { code: String.raw`/[a&&[^b]]/v`, output: String.raw`/[a--[b]]/v`, From 68b761e9ef7ada3a7c0444f9b75ae367c2c08a69 Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Sun, 1 Oct 2023 16:40:03 +0900 Subject: [PATCH 10/14] rename to simplify-set-operations --- .changeset/early-islands-press.md | 2 +- README.md | 2 +- docs/rules/index.md | 2 +- docs/rules/negation.md | 4 ++-- ...-negation.md => simplify-set-operations.md} | 18 +++++++++--------- lib/configs/recommended.ts | 2 +- ...-negation.ts => simplify-set-operations.ts} | 4 ++-- lib/utils/rules.ts | 4 ++-- ...-negation.ts => simplify-set-operations.ts} | 4 ++-- 9 files changed, 21 insertions(+), 21 deletions(-) rename docs/rules/{require-reduce-negation.md => simplify-set-operations.md} (83%) rename lib/rules/{require-reduce-negation.ts => simplify-set-operations.ts} (99%) rename tests/lib/rules/{require-reduce-negation.ts => simplify-set-operations.ts} (98%) diff --git a/.changeset/early-islands-press.md b/.changeset/early-islands-press.md index 2f68c8ba3..d04828f03 100644 --- a/.changeset/early-islands-press.md +++ b/.changeset/early-islands-press.md @@ -2,4 +2,4 @@ "eslint-plugin-regexp": minor --- -Add `regexp/require-reduce-negation` rule +Add `regexp/simplify-set-operations` rule diff --git a/README.md b/README.md index b2df69641..8a77f0340 100644 --- a/README.md +++ b/README.md @@ -165,9 +165,9 @@ The `plugin:regexp/all` config enables all rules. It's meant for testing, not fo | [prefer-range](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-range.html) | enforce using character class range | ✅ | | 🔧 | | | [prefer-regexp-exec](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-regexp-exec.html) | enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided | | | | | | [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-reduce-negation](https://ota-meshi.github.io/eslint-plugin-regexp/rules/require-reduce-negation.html) | require to reduce negation of character classes | ✅ | | 🔧 | | | [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 the set operations to be simple | ✅ | | 🔧 | | | [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 aed0ba6d6..455cbf2aa 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -72,9 +72,9 @@ sidebarDepth: 0 | [prefer-range](prefer-range.md) | enforce using character class range | ✅ | | 🔧 | | | [prefer-regexp-exec](prefer-regexp-exec.md) | enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided | | | | | | [prefer-regexp-test](prefer-regexp-test.md) | enforce that `RegExp#test` is used instead of `String#match` and `RegExp#exec` | | | 🔧 | | -| [require-reduce-negation](require-reduce-negation.md) | require to reduce negation of character classes | ✅ | | 🔧 | | | [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 the set operations to be simple | ✅ | | 🔧 | | | [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 5b871f54e..2d3d75e04 100644 --- a/docs/rules/negation.md +++ b/docs/rules/negation.md @@ -55,9 +55,9 @@ Nothing. ## :couple: Related rules -- [regexp/require-reduce-negation] +- [regexp/simplify-set-operations] -[regexp/require-reduce-negation]: ./require-reduce-negation.md +[regexp/simplify-set-operations]: ./simplify-set-operations.md ## :rocket: Version diff --git a/docs/rules/require-reduce-negation.md b/docs/rules/simplify-set-operations.md similarity index 83% rename from docs/rules/require-reduce-negation.md rename to docs/rules/simplify-set-operations.md index 47252b5b2..fc8b2c03a 100644 --- a/docs/rules/require-reduce-negation.md +++ b/docs/rules/simplify-set-operations.md @@ -1,10 +1,10 @@ --- pageClass: "rule-details" sidebarDepth: 0 -title: "regexp/require-reduce-negation" -description: "require to reduce negation of character classes" +title: "regexp/simplify-set-operations" +description: "require the set operations to be simple" --- -# regexp/require-reduce-negation +# regexp/simplify-set-operations 💼 This rule is enabled in the ✅ `plugin:regexp/recommended` config. @@ -12,11 +12,11 @@ description: "require to reduce negation of character classes" -> require to reduce negation of character classes +> require the set operations to be simple ## :book: Rule Details -This rule is aimed at optimizing patterns by reducing the negation (complement) representation of character classes (with `v` flag). +This rule aims to optimize patterns by simplifying set operations on 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 [regexp/negation] rule together. @@ -24,7 +24,7 @@ If you want to report simple nested negations, use [regexp/negation] rule togeth ```js -/* eslint regexp/require-reduce-negation: "error" */ +/* eslint regexp/simplify-set-operations: "error" */ /* ✗ BAD */ var re = /[a&&[^b]]/v; // -> /[a--b]/v @@ -44,7 +44,7 @@ var re = /[^a&&b]/v; ### How does this rule work? -This rule attempts to reduce complements in the ways listed below: +This rule attempts to simplify set operations in the ways listed below: #### De Morgan's laws @@ -88,5 +88,5 @@ Nothing. ## :mag: Implementation -- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/require-reduce-negation.ts) -- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/require-reduce-negation.ts) +- [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 17666671f..e7d46d2fe 100644 --- a/lib/configs/recommended.ts +++ b/lib/configs/recommended.ts @@ -63,7 +63,7 @@ export const rules = { "regexp/prefer-star-quantifier": "error", "regexp/prefer-unicode-codepoint-escapes": "error", "regexp/prefer-w": "error", - "regexp/require-reduce-negation": "error", + "regexp/simplify-set-operations": "error", "regexp/sort-flags": "error", "regexp/strict": "error", "regexp/use-ignore-case": "error", diff --git a/lib/rules/require-reduce-negation.ts b/lib/rules/simplify-set-operations.ts similarity index 99% rename from lib/rules/require-reduce-negation.ts rename to lib/rules/simplify-set-operations.ts index dc99ccab4..f644b7011 100644 --- a/lib/rules/require-reduce-negation.ts +++ b/lib/rules/simplify-set-operations.ts @@ -73,10 +73,10 @@ function collectIntersectionOperands( return operands } -export default createRule("require-reduce-negation", { +export default createRule("simplify-set-operations", { meta: { docs: { - description: "require to reduce negation of character classes", + description: "require simplify set operations", category: "Best Practices", recommended: true, }, diff --git a/lib/utils/rules.ts b/lib/utils/rules.ts index e0ca0414a..b440c9e4c 100644 --- a/lib/utils/rules.ts +++ b/lib/utils/rules.ts @@ -71,9 +71,9 @@ import preferStarQuantifier from "../rules/prefer-star-quantifier" import preferT from "../rules/prefer-t" import preferUnicodeCodepointEscapes from "../rules/prefer-unicode-codepoint-escapes" import preferW from "../rules/prefer-w" -import requireReduceNegation from "../rules/require-reduce-negation" 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" @@ -154,9 +154,9 @@ export const rules = [ preferT, preferUnicodeCodepointEscapes, preferW, - requireReduceNegation, requireUnicodeRegexp, requireUnicodeSetsRegexp, + simplifySetOperations, sortAlternatives, sortCharacterClassElements, sortFlags, diff --git a/tests/lib/rules/require-reduce-negation.ts b/tests/lib/rules/simplify-set-operations.ts similarity index 98% rename from tests/lib/rules/require-reduce-negation.ts rename to tests/lib/rules/simplify-set-operations.ts index f33fb1212..9acc4ce73 100644 --- a/tests/lib/rules/require-reduce-negation.ts +++ b/tests/lib/rules/simplify-set-operations.ts @@ -1,5 +1,5 @@ import { RuleTester } from "eslint" -import rule from "../../../lib/rules/require-reduce-negation" +import rule from "../../../lib/rules/simplify-set-operations" const tester = new RuleTester({ parserOptions: { @@ -8,7 +8,7 @@ const tester = new RuleTester({ }, }) -tester.run("require-reduce-negation", rule as any, { +tester.run("simplify-set-operations", rule as any, { valid: [ String.raw`/[[abc]]/v`, String.raw`/[\d]/u`, From a4f26c1ee899eeda5d2014d788e5aa8f023797d0 Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Sun, 1 Oct 2023 16:44:21 +0900 Subject: [PATCH 11/14] fix --- lib/rules/simplify-set-operations.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/rules/simplify-set-operations.ts b/lib/rules/simplify-set-operations.ts index f644b7011..ac6d39344 100644 --- a/lib/rules/simplify-set-operations.ts +++ b/lib/rules/simplify-set-operations.ts @@ -82,8 +82,6 @@ export default createRule("simplify-set-operations", { }, schema: [], messages: { - doubleNegationElimination: - "This character class can be double negation elimination.", toNegationOfDisjunction: "This {{target}} can be converted to the negation of a disjunction using De Morgan's laws.", toNegationOfConjunction: @@ -105,6 +103,11 @@ export default createRule("simplify-set-operations", { ): 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) @@ -138,7 +141,6 @@ export default createRule("simplify-set-operations", { | ClassSubtraction targetNode: CharacterClass | ExpressionCharacterClass messageId: - | "doubleNegationElimination" | "toNegationOfDisjunction" | "toNegationOfConjunction" | "toSubtraction" From b24601a27258bbaba5990bb8d23576cace375a43 Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Sun, 1 Oct 2023 16:47:21 +0900 Subject: [PATCH 12/14] update --- .changeset/early-islands-press.md | 2 +- README.md | 2 +- docs/rules/index.md | 2 +- docs/rules/simplify-set-operations.md | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.changeset/early-islands-press.md b/.changeset/early-islands-press.md index d04828f03..16389b689 100644 --- a/.changeset/early-islands-press.md +++ b/.changeset/early-islands-press.md @@ -1,5 +1,5 @@ --- -"eslint-plugin-regexp": minor +"eslint-plugin-regexp": major --- Add `regexp/simplify-set-operations` rule diff --git a/README.md b/README.md index 8a77f0340..80dafb974 100644 --- a/README.md +++ b/README.md @@ -167,7 +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 the set operations to be simple | ✅ | | 🔧 | | +| [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 455cbf2aa..2af408f20 100644 --- a/docs/rules/index.md +++ b/docs/rules/index.md @@ -74,7 +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 the set operations to be simple | ✅ | | 🔧 | | +| [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/simplify-set-operations.md b/docs/rules/simplify-set-operations.md index fc8b2c03a..7c133bc04 100644 --- a/docs/rules/simplify-set-operations.md +++ b/docs/rules/simplify-set-operations.md @@ -2,7 +2,7 @@ pageClass: "rule-details" sidebarDepth: 0 title: "regexp/simplify-set-operations" -description: "require the set operations to be simple" +description: "require simplify set operations" --- # regexp/simplify-set-operations @@ -12,7 +12,7 @@ description: "require the set operations to be simple" -> require the set operations to be simple +> require simplify set operations ## :book: Rule Details From 9dea8e61ce85d29e06ac3cd5862ba2d036641b70 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Mon, 2 Oct 2023 08:20:14 +0900 Subject: [PATCH 13/14] Apply suggestions from code review Co-authored-by: Michael Schmidt --- docs/rules/simplify-set-operations.md | 10 +++++----- lib/rules/simplify-set-operations.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/rules/simplify-set-operations.md b/docs/rules/simplify-set-operations.md index 7c133bc04..986fb3831 100644 --- a/docs/rules/simplify-set-operations.md +++ b/docs/rules/simplify-set-operations.md @@ -16,10 +16,10 @@ description: "require simplify set operations" ## :book: Rule Details -This rule aims to optimize patterns by simplifying set operations on character classes (with `v` flag). +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 [regexp/negation] rule together. +If you want to report simple nested negations, use the [regexp/negation] rule. @@ -48,8 +48,8 @@ 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, auto-fix them.\ -For example, `/[[^a]&&[^b]]/v` is equivalent to `/[^ab]/v`, `/[[^a][^b]]/v` is equivalent to `/[^a&&b]/v`. +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 . @@ -67,7 +67,7 @@ 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`.\ +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 diff --git a/lib/rules/simplify-set-operations.ts b/lib/rules/simplify-set-operations.ts index ac6d39344..e2c3388fd 100644 --- a/lib/rules/simplify-set-operations.ts +++ b/lib/rules/simplify-set-operations.ts @@ -264,7 +264,7 @@ export default createRule("simplify-set-operations", { * - `[[^a][^b]]` -> `[^a&&b]` */ function toNegationOfConjunction(ccNode: CharacterClass) { - if (ccNode.elements.length <= 1 || !flags.unicodeSets) { + if (ccNode.elements.length <= 1) { return false } const elements: CharacterClassElement[] = ccNode.elements From efca081ae194b64b40abc1d766036a7e0ecc64ec Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Mon, 2 Oct 2023 08:25:10 +0900 Subject: [PATCH 14/14] move getParsedElement --- lib/rules/simplify-set-operations.ts | 66 ++++++++++++++-------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/lib/rules/simplify-set-operations.ts b/lib/rules/simplify-set-operations.ts index e2c3388fd..e4c5a650f 100644 --- a/lib/rules/simplify-set-operations.ts +++ b/lib/rules/simplify-set-operations.ts @@ -73,6 +73,39 @@ function collectIntersectionOperands( 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: { @@ -384,36 +417,3 @@ export default createRule("simplify-set-operations", { }) }, }) - -/** 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 -}