From f943121c55c9b1d193e172ec4f7c14283cd3c6d5 Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Sun, 24 Sep 2023 22:12:44 +0200 Subject: [PATCH 1/2] Add `regexp/prefer-set-operation` rule --- README.md | 1 + docs/rules/index.md | 1 + docs/rules/prefer-set-operation.md | 48 ++++++++ lib/configs/recommended.ts | 1 + lib/rules/prefer-set-operation.ts | 146 ++++++++++++++++++++++++ lib/utils/rules.ts | 2 + tests/lib/rules/prefer-set-operation.ts | 42 +++++++ 7 files changed, 241 insertions(+) create mode 100644 docs/rules/prefer-set-operation.md create mode 100644 lib/rules/prefer-set-operation.ts create mode 100644 tests/lib/rules/prefer-set-operation.ts diff --git a/README.md b/README.md index 561ec723e..2d8398eb3 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` | | | 🔧 | | +| [prefer-set-operation](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-set-operation.html) | prefer character class set operations instead of lookarounds | ✅ | | 🔧 | | | [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 | | | 🔧 | | | [sort-alternatives](https://ota-meshi.github.io/eslint-plugin-regexp/rules/sort-alternatives.html) | sort alternatives if order doesn't matter | | | 🔧 | | diff --git a/docs/rules/index.md b/docs/rules/index.md index 31e251aac..dc8f59f83 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` | | | 🔧 | | +| [prefer-set-operation](prefer-set-operation.md) | prefer character class set operations instead of lookarounds | ✅ | | 🔧 | | | [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 | | | 🔧 | | | [sort-alternatives](sort-alternatives.md) | sort alternatives if order doesn't matter | | | 🔧 | | diff --git a/docs/rules/prefer-set-operation.md b/docs/rules/prefer-set-operation.md new file mode 100644 index 000000000..d8e65b19d --- /dev/null +++ b/docs/rules/prefer-set-operation.md @@ -0,0 +1,48 @@ +--- +pageClass: "rule-details" +sidebarDepth: 0 +title: "regexp/prefer-set-operation" +description: "prefer character class set operations instead of lookarounds" +--- +# regexp/prefer-set-operation + +💼 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). + + + +> prefer character class set operations instead of lookarounds + +## :book: Rule Details + +Regular expressions with the `v` flag have access to character class set operations (e.g. `/[\s&&\p{ASCII}]/v`, `/[\w--\d]/v`). These are more readable and performant than using lookarounds to achieve the same effect. For example, `/(?!\d)\w/v` is the same as `/[\w--\d]/v`. + + + +```js +/* eslint regexp/prefer-set-operation: "error" */ + +/* ✓ GOOD */ +var foo = /(?!\d)\w/ // requires the v flag +var foo = /(?!\d)\w/u // requires the v flag + +/* ✗ BAD */ +var foo = /(?!\d)\w/v +var foo = /(?!\s)[\w\P{ASCII}]/v +``` + + + +## :wrench: Options + +Nothing. + +## :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/prefer-set-operation.ts) +- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/prefer-set-operation.ts) diff --git a/lib/configs/recommended.ts b/lib/configs/recommended.ts index 3d0979ee0..d11220902 100644 --- a/lib/configs/recommended.ts +++ b/lib/configs/recommended.ts @@ -60,6 +60,7 @@ export const rules = { "regexp/prefer-predefined-assertion": "error", "regexp/prefer-question-quantifier": "error", "regexp/prefer-range": "error", + "regexp/prefer-set-operation": "error", "regexp/prefer-star-quantifier": "error", "regexp/prefer-unicode-codepoint-escapes": "error", "regexp/prefer-w": "error", diff --git a/lib/rules/prefer-set-operation.ts b/lib/rules/prefer-set-operation.ts new file mode 100644 index 000000000..86cdf0053 --- /dev/null +++ b/lib/rules/prefer-set-operation.ts @@ -0,0 +1,146 @@ +import type { RegExpVisitor } from "@eslint-community/regexpp/visitor" +import type { + Alternative, + Character, + CharacterClass, + CharacterSet, + ExpressionCharacterClass, + LookaroundAssertion, + Node, +} from "@eslint-community/regexpp/ast" +import type { RegExpContext } from "../utils" +import { createRule, defineRegexpVisitor } from "../utils" +import { hasStrings } from "regexp-ast-analysis" + +type CharElement = + | Character + | CharacterSet + | CharacterClass + | ExpressionCharacterClass + +function isCharElement(node: Node): node is CharElement { + return ( + node.type === "Character" || + node.type === "CharacterSet" || + node.type === "CharacterClass" || + node.type === "ExpressionCharacterClass" + ) +} + +type CharLookaround = LookaroundAssertion & { + alternatives: [Alternative & { elements: [CharElement] }] +} + +function isCharLookaround(node: Node): node is CharLookaround { + return ( + node.type === "Assertion" && + (node.kind === "lookahead" || node.kind === "lookbehind") && + node.alternatives.length === 1 && + node.alternatives[0].elements.length === 1 && + isCharElement(node.alternatives[0].elements[0]) + ) +} + +function escapeRaw(raw: string): string { + if (/^[&\-^]$/u.test(raw)) { + return `\\${raw}` + } + return raw +} + +export default createRule("prefer-set-operation", { + meta: { + docs: { + description: + "prefer character class set operations instead of lookarounds", + category: "Best Practices", + recommended: true, + }, + fixable: "code", + schema: [], + messages: { + unexpected: + "This lookaround can be combined with '{{char}}' using a set operation.", + }, + type: "suggestion", + }, + create(context) { + function createVisitor( + regexpContext: RegExpContext, + ): RegExpVisitor.Handlers { + const { node, flags, getRegexpLocation, fixReplaceNode } = + regexpContext + + if (!flags.unicodeSets) { + // set operations are exclusive to the v flag. + return {} + } + + function tryApply( + element: CharElement, + assertion: CharLookaround, + parent: Alternative, + ): void { + const assertElement = assertion.alternatives[0].elements[0] + if (hasStrings(assertElement, flags)) { + return + } + + context.report({ + node, + loc: getRegexpLocation(assertion), + messageId: "unexpected", + data: { + char: element.raw, + }, + fix: fixReplaceNode(parent, () => { + const op = assertion.negate ? "--" : "&&" + const left = escapeRaw(element.raw) + const right = escapeRaw(assertElement.raw) + const replacement = `[${left}${op}${right}]` + + return parent.elements + .map((e) => { + if (e === assertion) { + return "" + } else if (e === element) { + return replacement + } + return e.raw + }) + .join("") + }), + }) + } + + return { + onAlternativeEnter(alternative) { + const { elements } = alternative + for (let i = 1; i < elements.length; i++) { + const a = elements[i - 1] + const b = elements[i] + + if ( + isCharElement(a) && + isCharLookaround(b) && + b.kind === "lookbehind" + ) { + tryApply(a, b, alternative) + } + if ( + isCharLookaround(a) && + a.kind === "lookahead" && + isCharElement(b) + ) { + tryApply(b, a, alternative) + } + } + }, + } + } + + return defineRegexpVisitor(context, { + createVisitor, + }) + }, +}) diff --git a/lib/utils/rules.ts b/lib/utils/rules.ts index 0d44409c9..e69b79c44 100644 --- a/lib/utils/rules.ts +++ b/lib/utils/rules.ts @@ -67,6 +67,7 @@ import preferRange from "../rules/prefer-range" import preferRegexpExec from "../rules/prefer-regexp-exec" import preferRegexpTest from "../rules/prefer-regexp-test" import preferResultArrayGroups from "../rules/prefer-result-array-groups" +import preferSetOperation from "../rules/prefer-set-operation" import preferStarQuantifier from "../rules/prefer-star-quantifier" import preferT from "../rules/prefer-t" import preferUnicodeCodepointEscapes from "../rules/prefer-unicode-codepoint-escapes" @@ -149,6 +150,7 @@ export const rules = [ preferRegexpExec, preferRegexpTest, preferResultArrayGroups, + preferSetOperation, preferStarQuantifier, preferT, preferUnicodeCodepointEscapes, diff --git a/tests/lib/rules/prefer-set-operation.ts b/tests/lib/rules/prefer-set-operation.ts new file mode 100644 index 000000000..f88503f76 --- /dev/null +++ b/tests/lib/rules/prefer-set-operation.ts @@ -0,0 +1,42 @@ +import { RuleTester } from "eslint" +import rule from "../../../lib/rules/prefer-set-operation" + +const tester = new RuleTester({ + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + }, +}) + +tester.run("prefer-set-operation", rule as any, { + valid: [ + String.raw`/a\b/`, + String.raw`/a\b/u`, + String.raw`/a\b/v`, + String.raw`/(?!a)\w/`, + String.raw`/(?!a)\w/u`, + ], + invalid: [ + { + code: String.raw`/(?!a)\w/v`, + output: String.raw`/[\w--a]/v`, + errors: [ + "This lookaround can be combined with '\\w' using a set operation.", + ], + }, + { + code: String.raw`/\w(?<=\d)/v`, + output: String.raw`/[\w&&\d]/v`, + errors: [ + "This lookaround can be combined with '\\w' using a set operation.", + ], + }, + { + code: String.raw`/(?!-)&/v`, + output: String.raw`/[\&--\-]/v`, + errors: [ + "This lookaround can be combined with '&' using a set operation.", + ], + }, + ], +}) From 47bcc636fc35b25763841df6e1ec7a212af75169 Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Sun, 24 Sep 2023 22:21:16 +0200 Subject: [PATCH 2/2] Create spotty-phones-deliver.md --- .changeset/spotty-phones-deliver.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/spotty-phones-deliver.md diff --git a/.changeset/spotty-phones-deliver.md b/.changeset/spotty-phones-deliver.md new file mode 100644 index 000000000..e068beb48 --- /dev/null +++ b/.changeset/spotty-phones-deliver.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-regexp": major +--- + +Add `regexp/prefer-set-operation` rule