Skip to content

Commit 417a68d

Browse files
authored
Add support for case checking of hex escapes and control escapes to regexp/letter-case rule. (#45)
* Add support for case checking of control escapes to `regexp/letter-case` rule. * Add support for case checking of hex escapes to `regexp/letter-case` rule. * update
1 parent 7fffe0b commit 417a68d

File tree

3 files changed

+155
-10
lines changed

3 files changed

+155
-10
lines changed

docs/rules/letter-case.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,19 @@ This rule is aimed to unify the case of letters.
1818
<eslint-code-block fix>
1919

2020
```js
21-
/* eslint regexp/letter-case: "error" */
21+
/* eslint regexp/letter-case: ["error", { hexadecimalEscape: 'lowercase', controlEscape: 'uppercase' }] */
2222

2323
/* ✓ GOOD */
2424
var foo = /a/i
2525
var foo = /\u000a/
26+
var foo = /\x0a/
27+
var foo = /\cA/
2628

2729
/* ✗ BAD */
2830
var foo = /A/i
2931
var foo = /\u000A/
32+
var foo = /\x0A/
33+
var foo = /\ca/
3034
```
3135

3236
</eslint-code-block>
@@ -37,18 +41,24 @@ var foo = /\u000A/
3741
{
3842
"regexp/letter-case": ["error", {
3943
"caseInsensitive": "lowercase", // or "uppercase" or "ignore"
40-
"unicodeEscape": "lowercase" // or "uppercase" or "ignore"
44+
"unicodeEscape": "lowercase", // or "uppercase" or "ignore"
45+
"hexadecimalEscape": "ignore", // or "lowercase" or "uppercase"
46+
"controlEscape": "ignore", // or "lowercase" or "uppercase"
4147
}]
4248
}
4349
```
4450

4551
- String options
46-
- `"lowercase"` ... Enforce lowercase letters. This is default.
52+
- `"lowercase"` ... Enforce lowercase letters.
4753
- `"uppercase"` ... Enforce uppercase letters.
4854
- `"ignore"` ... Does not force case.
4955
- Properties
50-
- `caseInsensitive` ... Specifies the letter case when the `i` flag is present.
51-
- `unicodeEscape` ... Specifies the letter case when the unicode escapes.
56+
- `caseInsensitive` ... Specifies the letter case when the `i` flag is present. Default is `"lowercase"`.
57+
- `unicodeEscape` ... Specifies the letter case when the unicode escapes. Default is `"lowercase"`.
58+
- `hexadecimalEscape` ... Specifies the letter case when the hexadecimal escapes. Default is `"ignore"`.
59+
(The default value will change to `"lowercase"` in the next major version.)
60+
- `controlEscape` ... Specifies the letter case when the control escapes (e.g. `\cX`). Default is `"ignore"`.
61+
(The default value will change to `"uppercase"` in the next major version.)
5262

5363
## :rocket: Version
5464

lib/rules/letter-case.ts

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,37 @@ import {
1414
const CASE_SCHEMA = ["lowercase", "uppercase", "ignore"] as const
1515
type Case = typeof CASE_SCHEMA[number]
1616

17+
const DEFAULTS = {
18+
caseInsensitive: "lowercase" as const,
19+
unicodeEscape: "lowercase" as const,
20+
// TODO In the major version
21+
// hexadecimalEscape: "lowercase" as const,
22+
// controlEscape: "uppercase" as const,
23+
hexadecimalEscape: "ignore" as const,
24+
controlEscape: "ignore" as const,
25+
}
26+
1727
/** Parse option */
1828
function parseOptions(option?: {
1929
caseInsensitive?: Case
2030
unicodeEscape?: Case
21-
}): { caseInsensitive: Case; unicodeEscape: Case } {
31+
hexadecimalEscape?: Case
32+
controlEscape?: Case
33+
}): {
34+
caseInsensitive: Case
35+
unicodeEscape: Case
36+
hexadecimalEscape: Case
37+
controlEscape: Case
38+
} {
2239
if (!option) {
23-
return { caseInsensitive: "lowercase", unicodeEscape: "lowercase" }
40+
return DEFAULTS
2441
}
2542
return {
26-
caseInsensitive: option.caseInsensitive || "lowercase",
27-
unicodeEscape: option.unicodeEscape || "lowercase",
43+
caseInsensitive: option.caseInsensitive || DEFAULTS.caseInsensitive,
44+
unicodeEscape: option.unicodeEscape || DEFAULTS.unicodeEscape,
45+
hexadecimalEscape:
46+
option.hexadecimalEscape || DEFAULTS.hexadecimalEscape,
47+
controlEscape: option.controlEscape || DEFAULTS.controlEscape,
2848
}
2949
}
3050

@@ -54,6 +74,8 @@ export default createRule("letter-case", {
5474
properties: {
5575
caseInsensitive: { enum: CASE_SCHEMA },
5676
unicodeEscape: { enum: CASE_SCHEMA },
77+
hexadecimalEscape: { enum: CASE_SCHEMA },
78+
controlEscape: { enum: CASE_SCHEMA },
5779
},
5880
additionalProperties: false,
5981
},
@@ -157,7 +179,7 @@ export default createRule("letter-case", {
157179
if (options.unicodeEscape === "ignore") {
158180
return
159181
}
160-
const parts = /(\\u\{?)(.*)(\}?)/u.exec(cNode.raw)!
182+
const parts = /^(\\u\{?)(.*)(\}?)$/u.exec(cNode.raw)!
161183
if (STRING_CASE_CHECKER[options.unicodeEscape](parts[2])) {
162184
return
163185
}
@@ -169,6 +191,43 @@ export default createRule("letter-case", {
169191
)
170192
}
171193

194+
/** Verify for Character in hexadecimal escape */
195+
function verifyCharacterInHexadecimalEscape(
196+
node: Expression,
197+
cNode: Character,
198+
) {
199+
if (options.hexadecimalEscape === "ignore") {
200+
return
201+
}
202+
const parts = /^\\x(.*)$/u.exec(cNode.raw)!
203+
if (STRING_CASE_CHECKER[options.hexadecimalEscape](parts[1])) {
204+
return
205+
}
206+
report(
207+
node,
208+
cNode,
209+
options.hexadecimalEscape,
210+
(converter) => `\\x${converter(parts[1])}`,
211+
)
212+
}
213+
214+
/** Verify for Character in control escape */
215+
function verifyCharacterInControl(node: Expression, cNode: Character) {
216+
if (options.controlEscape === "ignore") {
217+
return
218+
}
219+
const parts = /^\\c(.*)$/u.exec(cNode.raw)!
220+
if (STRING_CASE_CHECKER[options.controlEscape](parts[1])) {
221+
return
222+
}
223+
report(
224+
node,
225+
cNode,
226+
options.controlEscape,
227+
(converter) => `\\c${converter(parts[1])}`,
228+
)
229+
}
230+
172231
/**
173232
* Create visitor
174233
* @param node
@@ -186,6 +245,12 @@ export default createRule("letter-case", {
186245
if (cNode.raw.startsWith("\\u")) {
187246
verifyCharacterInUnicodeEscape(node, cNode)
188247
}
248+
if (/^\\x.+$/u.test(cNode.raw)) {
249+
verifyCharacterInHexadecimalEscape(node, cNode)
250+
}
251+
if (/^\\c[a-zA-Z]$/u.test(cNode.raw)) {
252+
verifyCharacterInControl(node, cNode)
253+
}
189254
},
190255
...(flags.includes("i")
191256
? {

tests/lib/rules/letter-case.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,40 @@ tester.run("letter-case", rule as any, {
7575
code: String.raw`/\u{a}\u{A}/u`,
7676
options: [{ unicodeEscape: "ignore" }],
7777
},
78+
String.raw`/\x0a/`,
79+
{
80+
code: String.raw`/\x0a/`,
81+
options: [{ hexadecimalEscape: "lowercase" }],
82+
},
83+
{
84+
code: String.raw`/\x0A/`,
85+
options: [{ hexadecimalEscape: "uppercase" }],
86+
},
87+
{
88+
code: String.raw`/\x0a\x0A/`,
89+
options: [{ hexadecimalEscape: "ignore" }],
90+
},
91+
String.raw`/\cA/u`,
92+
{
93+
code: String.raw`/\ca/u`,
94+
options: [{ controlEscape: "lowercase" }],
95+
},
96+
{
97+
code: String.raw`/\cA/u`,
98+
options: [{ controlEscape: "uppercase" }],
99+
},
100+
{
101+
code: String.raw`/\cA\ca/u`,
102+
options: [{ controlEscape: "ignore" }],
103+
},
104+
{
105+
code: String.raw`/[\c_][\c_]/`,
106+
options: [{ controlEscape: "lowercase" }],
107+
},
108+
{
109+
code: String.raw`/[\c_][\c_]/`,
110+
options: [{ controlEscape: "uppercase" }],
111+
},
78112
],
79113
invalid: [
80114
{
@@ -183,6 +217,42 @@ tester.run("letter-case", rule as any, {
183217
options: [{ unicodeEscape: "uppercase" }],
184218
errors: [String.raw`'\u{a}' is not in uppercase`],
185219
},
220+
// TODO In the major version
221+
// {
222+
// code: String.raw`/\x0A/`,
223+
// output: String.raw`/\x0a/`,
224+
// errors: [String.raw`'\x0A' is not in lowercase`],
225+
// },
226+
{
227+
code: String.raw`/\x0A/`,
228+
output: String.raw`/\x0a/`,
229+
options: [{ hexadecimalEscape: "lowercase" }],
230+
errors: [String.raw`'\x0A' is not in lowercase`],
231+
},
232+
{
233+
code: String.raw`/\x0a/`,
234+
output: String.raw`/\x0A/`,
235+
options: [{ hexadecimalEscape: "uppercase" }],
236+
errors: [String.raw`'\x0a' is not in uppercase`],
237+
},
238+
// TODO In the major version
239+
// {
240+
// code: String.raw`/\ca/u`,
241+
// output: String.raw`/\cA/u`,
242+
// errors: [String.raw`'\ca' is not in uppercase`],
243+
// },
244+
{
245+
code: String.raw`/\cA/u`,
246+
output: String.raw`/\ca/u`,
247+
options: [{ controlEscape: "lowercase" }],
248+
errors: [String.raw`'\cA' is not in lowercase`],
249+
},
250+
{
251+
code: String.raw`/\ca/u`,
252+
output: String.raw`/\cA/u`,
253+
options: [{ controlEscape: "uppercase" }],
254+
errors: [String.raw`'\ca' is not in uppercase`],
255+
},
186256
{
187257
code: String.raw`const s = "\\u000A";
188258
new RegExp(s)`,

0 commit comments

Comments
 (0)