Skip to content

Commit 47dc791

Browse files
Add regexp/prefer-set-operation rule (#616)
* Add `regexp/prefer-set-operation` rule * Create spotty-phones-deliver.md
1 parent 7a38486 commit 47dc791

File tree

8 files changed

+246
-0
lines changed

8 files changed

+246
-0
lines changed

.changeset/spotty-phones-deliver.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-regexp": major
3+
---
4+
5+
Add `regexp/prefer-set-operation` rule

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ The `plugin:regexp/all` config enables all rules. It's meant for testing, not fo
165165
| [prefer-range](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-range.html) | enforce using character class range || | 🔧 | |
166166
| [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 | | | | |
167167
| [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` | | | 🔧 | |
168+
| [prefer-set-operation](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-set-operation.html) | prefer character class set operations instead of lookarounds || | 🔧 | |
168169
| [require-unicode-regexp](https://ota-meshi.github.io/eslint-plugin-regexp/rules/require-unicode-regexp.html) | enforce the use of the `u` flag | | | 🔧 | |
169170
| [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 | | | 🔧 | |
170171
| [sort-alternatives](https://ota-meshi.github.io/eslint-plugin-regexp/rules/sort-alternatives.html) | sort alternatives if order doesn't matter | | | 🔧 | |

docs/rules/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ sidebarDepth: 0
7272
| [prefer-range](prefer-range.md) | enforce using character class range || | 🔧 | |
7373
| [prefer-regexp-exec](prefer-regexp-exec.md) | enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided | | | | |
7474
| [prefer-regexp-test](prefer-regexp-test.md) | enforce that `RegExp#test` is used instead of `String#match` and `RegExp#exec` | | | 🔧 | |
75+
| [prefer-set-operation](prefer-set-operation.md) | prefer character class set operations instead of lookarounds || | 🔧 | |
7576
| [require-unicode-regexp](require-unicode-regexp.md) | enforce the use of the `u` flag | | | 🔧 | |
7677
| [require-unicode-sets-regexp](require-unicode-sets-regexp.md) | enforce the use of the `v` flag | | | 🔧 | |
7778
| [sort-alternatives](sort-alternatives.md) | sort alternatives if order doesn't matter | | | 🔧 | |

docs/rules/prefer-set-operation.md

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/prefer-set-operation"
5+
description: "prefer character class set operations instead of lookarounds"
6+
---
7+
# regexp/prefer-set-operation
8+
9+
💼 This rule is enabled in the ✅ `plugin:regexp/recommended` config.
10+
11+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
12+
13+
<!-- end auto-generated rule header -->
14+
15+
> prefer character class set operations instead of lookarounds
16+
17+
## :book: Rule Details
18+
19+
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`.
20+
21+
<eslint-code-block fix>
22+
23+
```js
24+
/* eslint regexp/prefer-set-operation: "error" */
25+
26+
/* ✓ GOOD */
27+
var foo = /(?!\d)\w/ // requires the v flag
28+
var foo = /(?!\d)\w/u // requires the v flag
29+
30+
/* ✗ BAD */
31+
var foo = /(?!\d)\w/v
32+
var foo = /(?!\s)[\w\P{ASCII}]/v
33+
```
34+
35+
</eslint-code-block>
36+
37+
## :wrench: Options
38+
39+
Nothing.
40+
41+
## :rocket: Version
42+
43+
:exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
44+
45+
## :mag: Implementation
46+
47+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/prefer-set-operation.ts)
48+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/prefer-set-operation.ts)

lib/configs/recommended.ts

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export const rules = {
6060
"regexp/prefer-predefined-assertion": "error",
6161
"regexp/prefer-question-quantifier": "error",
6262
"regexp/prefer-range": "error",
63+
"regexp/prefer-set-operation": "error",
6364
"regexp/prefer-star-quantifier": "error",
6465
"regexp/prefer-unicode-codepoint-escapes": "error",
6566
"regexp/prefer-w": "error",

lib/rules/prefer-set-operation.ts

+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
2+
import type {
3+
Alternative,
4+
Character,
5+
CharacterClass,
6+
CharacterSet,
7+
ExpressionCharacterClass,
8+
LookaroundAssertion,
9+
Node,
10+
} from "@eslint-community/regexpp/ast"
11+
import type { RegExpContext } from "../utils"
12+
import { createRule, defineRegexpVisitor } from "../utils"
13+
import { hasStrings } from "regexp-ast-analysis"
14+
15+
type CharElement =
16+
| Character
17+
| CharacterSet
18+
| CharacterClass
19+
| ExpressionCharacterClass
20+
21+
function isCharElement(node: Node): node is CharElement {
22+
return (
23+
node.type === "Character" ||
24+
node.type === "CharacterSet" ||
25+
node.type === "CharacterClass" ||
26+
node.type === "ExpressionCharacterClass"
27+
)
28+
}
29+
30+
type CharLookaround = LookaroundAssertion & {
31+
alternatives: [Alternative & { elements: [CharElement] }]
32+
}
33+
34+
function isCharLookaround(node: Node): node is CharLookaround {
35+
return (
36+
node.type === "Assertion" &&
37+
(node.kind === "lookahead" || node.kind === "lookbehind") &&
38+
node.alternatives.length === 1 &&
39+
node.alternatives[0].elements.length === 1 &&
40+
isCharElement(node.alternatives[0].elements[0])
41+
)
42+
}
43+
44+
function escapeRaw(raw: string): string {
45+
if (/^[&\-^]$/u.test(raw)) {
46+
return `\\${raw}`
47+
}
48+
return raw
49+
}
50+
51+
export default createRule("prefer-set-operation", {
52+
meta: {
53+
docs: {
54+
description:
55+
"prefer character class set operations instead of lookarounds",
56+
category: "Best Practices",
57+
recommended: true,
58+
},
59+
fixable: "code",
60+
schema: [],
61+
messages: {
62+
unexpected:
63+
"This lookaround can be combined with '{{char}}' using a set operation.",
64+
},
65+
type: "suggestion",
66+
},
67+
create(context) {
68+
function createVisitor(
69+
regexpContext: RegExpContext,
70+
): RegExpVisitor.Handlers {
71+
const { node, flags, getRegexpLocation, fixReplaceNode } =
72+
regexpContext
73+
74+
if (!flags.unicodeSets) {
75+
// set operations are exclusive to the v flag.
76+
return {}
77+
}
78+
79+
function tryApply(
80+
element: CharElement,
81+
assertion: CharLookaround,
82+
parent: Alternative,
83+
): void {
84+
const assertElement = assertion.alternatives[0].elements[0]
85+
if (hasStrings(assertElement, flags)) {
86+
return
87+
}
88+
89+
context.report({
90+
node,
91+
loc: getRegexpLocation(assertion),
92+
messageId: "unexpected",
93+
data: {
94+
char: element.raw,
95+
},
96+
fix: fixReplaceNode(parent, () => {
97+
const op = assertion.negate ? "--" : "&&"
98+
const left = escapeRaw(element.raw)
99+
const right = escapeRaw(assertElement.raw)
100+
const replacement = `[${left}${op}${right}]`
101+
102+
return parent.elements
103+
.map((e) => {
104+
if (e === assertion) {
105+
return ""
106+
} else if (e === element) {
107+
return replacement
108+
}
109+
return e.raw
110+
})
111+
.join("")
112+
}),
113+
})
114+
}
115+
116+
return {
117+
onAlternativeEnter(alternative) {
118+
const { elements } = alternative
119+
for (let i = 1; i < elements.length; i++) {
120+
const a = elements[i - 1]
121+
const b = elements[i]
122+
123+
if (
124+
isCharElement(a) &&
125+
isCharLookaround(b) &&
126+
b.kind === "lookbehind"
127+
) {
128+
tryApply(a, b, alternative)
129+
}
130+
if (
131+
isCharLookaround(a) &&
132+
a.kind === "lookahead" &&
133+
isCharElement(b)
134+
) {
135+
tryApply(b, a, alternative)
136+
}
137+
}
138+
},
139+
}
140+
}
141+
142+
return defineRegexpVisitor(context, {
143+
createVisitor,
144+
})
145+
},
146+
})

lib/utils/rules.ts

+2
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import preferRange from "../rules/prefer-range"
6767
import preferRegexpExec from "../rules/prefer-regexp-exec"
6868
import preferRegexpTest from "../rules/prefer-regexp-test"
6969
import preferResultArrayGroups from "../rules/prefer-result-array-groups"
70+
import preferSetOperation from "../rules/prefer-set-operation"
7071
import preferStarQuantifier from "../rules/prefer-star-quantifier"
7172
import preferT from "../rules/prefer-t"
7273
import preferUnicodeCodepointEscapes from "../rules/prefer-unicode-codepoint-escapes"
@@ -149,6 +150,7 @@ export const rules = [
149150
preferRegexpExec,
150151
preferRegexpTest,
151152
preferResultArrayGroups,
153+
preferSetOperation,
152154
preferStarQuantifier,
153155
preferT,
154156
preferUnicodeCodepointEscapes,
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../lib/rules/prefer-set-operation"
3+
4+
const tester = new RuleTester({
5+
parserOptions: {
6+
ecmaVersion: "latest",
7+
sourceType: "module",
8+
},
9+
})
10+
11+
tester.run("prefer-set-operation", rule as any, {
12+
valid: [
13+
String.raw`/a\b/`,
14+
String.raw`/a\b/u`,
15+
String.raw`/a\b/v`,
16+
String.raw`/(?!a)\w/`,
17+
String.raw`/(?!a)\w/u`,
18+
],
19+
invalid: [
20+
{
21+
code: String.raw`/(?!a)\w/v`,
22+
output: String.raw`/[\w--a]/v`,
23+
errors: [
24+
"This lookaround can be combined with '\\w' using a set operation.",
25+
],
26+
},
27+
{
28+
code: String.raw`/\w(?<=\d)/v`,
29+
output: String.raw`/[\w&&\d]/v`,
30+
errors: [
31+
"This lookaround can be combined with '\\w' using a set operation.",
32+
],
33+
},
34+
{
35+
code: String.raw`/(?!-)&/v`,
36+
output: String.raw`/[\&--\-]/v`,
37+
errors: [
38+
"This lookaround can be combined with '&' using a set operation.",
39+
],
40+
},
41+
],
42+
})

0 commit comments

Comments
 (0)