Skip to content

Commit 024c6d7

Browse files
Improved prefer-w rule (#155)
1 parent d074f40 commit 024c6d7

File tree

3 files changed

+83
-61
lines changed

3 files changed

+83
-61
lines changed

docs/rules/prefer-w.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ var foo = /\W/;
2929
var foo = /[0-9a-zA-Z_]/;
3030
var foo = /[^0-9a-zA-Z_]/;
3131
var foo = /[0-9a-z_]/i;
32+
var foo = /[0-9a-z_-]/i;
3233
```
3334

3435
</eslint-code-block>

lib/rules/prefer-w.ts

Lines changed: 67 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ import {
1313
CP_DIGIT_NINE,
1414
CP_LOW_LINE,
1515
} from "../utils"
16+
import { Chars } from "regexp-ast-analysis"
1617

1718
/**
1819
* Checks if small letter char class range
1920
* @param node The node to check
2021
*/
21-
function isSmallLetterCharacterClassRange(node: CharacterClassElement) {
22+
function isSmallLetterRange(node: CharacterClassElement) {
2223
return (
2324
node.type === "CharacterClassRange" &&
2425
node.min.value === CP_SMALL_A &&
@@ -30,7 +31,7 @@ function isSmallLetterCharacterClassRange(node: CharacterClassElement) {
3031
* Checks if capital letter char class range
3132
* @param node The node to check
3233
*/
33-
function isCapitalLetterCharacterClassRange(node: CharacterClassElement) {
34+
function isCapitalLetterRange(node: CharacterClassElement) {
3435
return (
3536
node.type === "CharacterClassRange" &&
3637
node.min.value === CP_CAPITAL_A &&
@@ -42,7 +43,7 @@ function isCapitalLetterCharacterClassRange(node: CharacterClassElement) {
4243
* Checks if digit char class
4344
* @param node The node to check
4445
*/
45-
function isDigitCharacterClass(node: CharacterClassElement) {
46+
function isDigitRangeOrSet(node: CharacterClassElement) {
4647
return (
4748
(node.type === "CharacterClassRange" &&
4849
node.min.value === CP_DIGIT_ZERO &&
@@ -55,7 +56,7 @@ function isDigitCharacterClass(node: CharacterClassElement) {
5556
* Checks if includes `_`
5657
* @param node The node to check
5758
*/
58-
function includesLowLineCharacterClass(node: CharacterClassElement) {
59+
function isUnderscoreCharacter(node: CharacterClassElement) {
5960
return node.type === "Character" && node.value === CP_LOW_LINE
6061
}
6162

@@ -84,93 +85,100 @@ export default createRule("prefer-w", {
8485
fixReplaceNode,
8586
getRegexpRange,
8687
fixerApplyEscape,
88+
toCharSet,
8789
}: RegExpContext): RegExpVisitor.Handlers {
8890
return {
8991
onCharacterClassEnter(ccNode: CharacterClass) {
92+
const charSet = toCharSet(ccNode)
93+
94+
let predefined: string | undefined = undefined
95+
if (charSet.equals(Chars.word(flags))) {
96+
predefined = "\\w"
97+
} else if (charSet.equals(Chars.word(flags).negate())) {
98+
predefined = "\\W"
99+
}
100+
101+
if (predefined) {
102+
context.report({
103+
node,
104+
loc: getRegexpLocation(ccNode),
105+
messageId: "unexpected",
106+
data: {
107+
type: "character class",
108+
expr: ccNode.raw,
109+
instead: predefined,
110+
},
111+
fix: fixReplaceNode(ccNode, predefined),
112+
})
113+
return
114+
}
115+
90116
const lowerAToZ: CharacterClassElement[] = []
91117
const capitalAToZ: CharacterClassElement[] = []
92118
const digit: CharacterClassElement[] = []
93-
const lowLine: CharacterClassElement[] = []
119+
const underscore: CharacterClassElement[] = []
94120
for (const element of ccNode.elements) {
95-
if (isSmallLetterCharacterClassRange(element)) {
121+
if (isSmallLetterRange(element)) {
96122
lowerAToZ.push(element)
97123
if (flags.ignoreCase) {
98124
capitalAToZ.push(element)
99125
}
100-
} else if (
101-
isCapitalLetterCharacterClassRange(element)
102-
) {
126+
} else if (isCapitalLetterRange(element)) {
103127
capitalAToZ.push(element)
104128
if (flags.ignoreCase) {
105129
lowerAToZ.push(element)
106130
}
107-
} else if (isDigitCharacterClass(element)) {
131+
} else if (isDigitRangeOrSet(element)) {
108132
digit.push(element)
109-
} else if (includesLowLineCharacterClass(element)) {
110-
lowLine.push(element)
133+
} else if (isUnderscoreCharacter(element)) {
134+
underscore.push(element)
111135
}
112136
}
137+
113138
if (
114139
lowerAToZ.length &&
115140
capitalAToZ.length &&
116141
digit.length &&
117-
lowLine.length
142+
underscore.length
118143
) {
119144
const unexpectedElements = [
120145
...new Set([
121146
...lowerAToZ,
122147
...capitalAToZ,
123148
...digit,
124-
...lowLine,
149+
...underscore,
125150
]),
126151
].sort((a, b) => a.start - b.start)
127152

128-
if (
129-
ccNode.elements.length === unexpectedElements.length
130-
) {
131-
const instead = ccNode.negate ? "\\W" : "\\w"
132-
context.report({
133-
node,
134-
loc: getRegexpLocation(ccNode),
135-
messageId: "unexpected",
136-
data: {
137-
type: "character class",
138-
expr: ccNode.raw,
139-
instead,
140-
},
141-
fix: fixReplaceNode(ccNode, instead),
142-
})
143-
} else {
144-
context.report({
145-
node,
146-
loc: getRegexpLocation(ccNode),
147-
messageId: "unexpected",
148-
data: {
149-
type: "character class ranges",
150-
expr: `[${unexpectedElements
151-
.map((e) => e.raw)
152-
.join("")}]`,
153-
instead: "\\w",
154-
},
155-
*fix(fixer: Rule.RuleFixer) {
156-
const range = getRegexpRange(ccNode)
157-
if (range == null) {
158-
return
159-
}
160-
yield fixer.replaceTextRange(
161-
getRegexpRange(
162-
unexpectedElements.shift()!,
163-
)!,
164-
fixerApplyEscape("\\w"),
153+
context.report({
154+
node,
155+
loc: getRegexpLocation(ccNode),
156+
messageId: "unexpected",
157+
data: {
158+
type: "character class ranges",
159+
expr: `[${unexpectedElements
160+
.map((e) => e.raw)
161+
.join("")}]`,
162+
instead: "\\w",
163+
},
164+
*fix(fixer: Rule.RuleFixer) {
165+
const range = getRegexpRange(ccNode)
166+
if (range == null) {
167+
return
168+
}
169+
yield fixer.replaceTextRange(
170+
getRegexpRange(
171+
unexpectedElements.shift()!,
172+
)!,
173+
fixerApplyEscape("\\w"),
174+
)
175+
for (const element of unexpectedElements) {
176+
yield fixer.removeRange(
177+
getRegexpRange(element)!,
165178
)
166-
for (const element of unexpectedElements) {
167-
yield fixer.removeRange(
168-
getRegexpRange(element)!,
169-
)
170-
}
171-
},
172-
})
173-
}
179+
}
180+
},
181+
})
174182
}
175183
},
176184
}

tests/lib/rules/prefer-w.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,11 @@ tester.run("prefer-w", rule as any, {
8585
new RegExp(s, 'i')
8686
`,
8787
output: `
88-
const s = "[\\\\wc]"
88+
const s = "\\\\w"
8989
new RegExp(s, 'i')
9090
`,
9191
errors: [
92-
"Unexpected character class ranges '[0-9A-Z_]'. Use '\\w' instead.",
92+
"Unexpected character class '[0-9A-Z_c]'. Use '\\w' instead.",
9393
],
9494
},
9595
{
@@ -98,6 +98,19 @@ tester.run("prefer-w", rule as any, {
9898
new RegExp(s, 'i')
9999
`,
100100
output: null,
101+
errors: [
102+
"Unexpected character class '[0-9A-Z_c]'. Use '\\w' instead.",
103+
],
104+
},
105+
{
106+
code: `
107+
const s = "[0-9A-Z_-]"
108+
new RegExp(s, 'i')
109+
`,
110+
output: `
111+
const s = "[\\\\w-]"
112+
new RegExp(s, 'i')
113+
`,
101114
errors: [
102115
"Unexpected character class ranges '[0-9A-Z_]'. Use '\\w' instead.",
103116
],

0 commit comments

Comments
 (0)