Skip to content

Commit 79748e1

Browse files
JLHwungsindresorhus
andcommitted
Apply escape-case to regex literal and remove transformation of \c escape on string literal (#294)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 3f2e9a6 commit 79748e1

File tree

3 files changed

+120
-18
lines changed

3 files changed

+120
-18
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"lodash.snakecase": "^4.0.1",
4141
"lodash.topairs": "^4.3.0",
4242
"lodash.upperfirst": "^4.2.0",
43+
"regexpp": "^2.0.1",
4344
"reserved-words": "^0.1.2",
4445
"safe-regex": "^2.0.1"
4546
},

rules/escape-case.js

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
'use strict';
2+
const {
3+
visitRegExpAST,
4+
parseRegExpLiteral
5+
} = require('regexpp');
6+
27
const getDocsUrl = require('./utils/get-docs-url');
38

4-
const escapeWithLowercase = /((?:^|[^\\])(?:\\\\)*)\\(x[a-f\d]{2}|u[a-f\d]{4}|u\{(?:[a-f\d]{1,})\}|c[a-z])/;
9+
const escapeWithLowercase = /((?:^|[^\\])(?:\\\\)*)\\(x[a-f\d]{2}|u[a-f\d]{4}|u{(?:[a-f\d]+)})/;
10+
const escapePatternWithLowercase = /((?:^|[^\\])(?:\\\\)*)\\(x[a-f\d]{2}|u[a-f\d]{4}|u{(?:[a-f\d]+)}|c[a-z])/;
511
const hasLowercaseCharacter = /[a-z]+/;
612
const message = 'Use uppercase characters for the value of the escape sequence.';
713

8-
const fix = value => {
9-
const results = escapeWithLowercase.exec(value);
14+
const fix = (value, regexp) => {
15+
const results = regexp.exec(value);
1016

1117
if (results) {
1218
const prefix = results[1].length + 1;
@@ -17,6 +23,56 @@ const fix = value => {
1723
return value;
1824
};
1925

26+
/**
27+
Find the `[start, end]` position of the lowercase escape sequence in a regular expression literal ASTNode.
28+
29+
@param {string} value - String representation of a literal ASTNode.
30+
@returns {number[] | undefined} The `[start, end]` pair if found, or null if not.
31+
*/
32+
const findLowercaseEscape = value => {
33+
const ast = parseRegExpLiteral(value);
34+
35+
let escapeNodePosition;
36+
visitRegExpAST(ast, {
37+
/**
38+
Record escaped node position in regexpp ASTNode. Returns undefined if not found.
39+
@param {ASTNode} node A regexpp ASTNode. Note that it is of different type to the ASTNode of ESLint parsers
40+
@returns {undefined}
41+
*/
42+
onCharacterLeave(node) {
43+
if (escapeNodePosition) {
44+
return;
45+
}
46+
47+
const matches = node.raw.match(escapePatternWithLowercase);
48+
49+
if (matches && matches[2].slice(1).match(hasLowercaseCharacter)) {
50+
escapeNodePosition = [node.start, node.end];
51+
}
52+
}
53+
});
54+
55+
return escapeNodePosition;
56+
};
57+
58+
/**
59+
Produce a fix if there is a lowercase escape sequence in the node.
60+
61+
@param {ASTNode} node - The regular expression literal ASTNode to check.
62+
@returns {string} The fixed `node.raw` string.
63+
*/
64+
const fixRegExp = node => {
65+
const escapeNodePosition = findLowercaseEscape(node.raw);
66+
const {raw} = node;
67+
68+
if (escapeNodePosition) {
69+
const [start, end] = escapeNodePosition;
70+
return raw.slice(0, start) + fix(raw.slice(start, end), escapePatternWithLowercase) + raw.slice(end, raw.length);
71+
}
72+
73+
return raw;
74+
};
75+
2076
const create = context => {
2177
return {
2278
Literal(node) {
@@ -30,7 +86,18 @@ const create = context => {
3086
context.report({
3187
node,
3288
message,
33-
fix: fixer => fixer.replaceTextRange([node.start, node.end], fix(node.raw))
89+
fix: fixer => fixer.replaceText(node, fix(node.raw, escapeWithLowercase))
90+
});
91+
}
92+
},
93+
'Literal[regex]'(node) {
94+
const escapeNodePosition = findLowercaseEscape(node.raw);
95+
96+
if (escapeNodePosition) {
97+
context.report({
98+
node,
99+
message,
100+
fix: fixer => fixer.replaceText(node, fixRegExp(node))
34101
});
35102
}
36103
},
@@ -42,10 +109,13 @@ const create = context => {
42109
const matches = node.value.raw.match(escapeWithLowercase);
43110

44111
if (matches && matches[2].slice(1).match(hasLowercaseCharacter)) {
112+
// Move cursor inside the head and tail apostrophe
113+
const start = node.range[0] + 1;
114+
const end = node.range[1] - 1;
45115
context.report({
46116
node,
47117
message,
48-
fix: fixer => fixer.replaceTextRange([node.start, node.end], fix(node.value.raw))
118+
fix: fixer => fixer.replaceTextRange([start, end], fix(node.value.raw, escapeWithLowercase))
49119
});
50120
}
51121
}

test/escape-case.js

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,9 @@ ruleTester.run('escape-case', rule, {
2121
'const foo = "\\xA9";',
2222
'const foo = "\\uD834";',
2323
'const foo = "\\u{1D306}";',
24-
'const foo = "\\cA";',
2524
'const foo = `\\xA9`;',
2625
'const foo = `\\uD834`;',
2726
'const foo = `\\u{1D306}`;',
28-
'const foo = `\\cA`;',
2927
'const foo = `\\uD834foo`;',
3028
'const foo = `foo\\uD834`;',
3129
'const foo = `foo \\uD834`;',
@@ -44,7 +42,15 @@ ruleTester.run('escape-case', rule, {
4442
'const foo = `foo\\\\xbar`;',
4543
'const foo = `foo\\\\ubarbaz`;',
4644
'const foo = `foo\\\\\\\\xbar`;',
47-
'const foo = `foo\\\\\\\\ubarbaz`;'
45+
'const foo = `foo\\\\\\\\ubarbaz`;',
46+
'const foo = /\\xA9/',
47+
'const foo = /\\uD834/',
48+
'const foo = /\\u{1D306}/u',
49+
'const foo = /\\cA/',
50+
'const foo = new RegExp("/\\xA9")',
51+
'const foo = new RegExp("/\\uD834/")',
52+
'const foo = new RegExp("/\\u{1D306}/", "u")',
53+
'const foo = new RegExp("/\\cA/")'
4854
],
4955
invalid: [
5056
{
@@ -62,11 +68,6 @@ ruleTester.run('escape-case', rule, {
6268
errors,
6369
output: 'const foo = "\\u{1D306}";'
6470
},
65-
{
66-
code: 'const foo = "\\ca";',
67-
errors,
68-
output: 'const foo = "\\cA";'
69-
},
7071
{
7172
code: 'const foo = `\\xa9`;',
7273
errors,
@@ -82,11 +83,6 @@ ruleTester.run('escape-case', rule, {
8283
errors,
8384
output: 'const foo = `\\u{1D306}`;'
8485
},
85-
{
86-
code: 'const foo = `\\ca`;',
87-
errors,
88-
output: 'const foo = `\\cA`;'
89-
},
9086
{
9187
code: 'const foo = `\\ud834foo`;',
9288
errors,
@@ -151,6 +147,41 @@ ruleTester.run('escape-case', rule, {
151147
code: 'const foo = `foo \\\\\\ud834`;',
152148
errors,
153149
output: 'const foo = `foo \\\\\\uD834`;'
150+
},
151+
{
152+
code: 'const foo = /\\xa9/;',
153+
errors,
154+
output: 'const foo = /\\xA9/;'
155+
},
156+
{
157+
code: 'const foo = /\\ud834/',
158+
errors,
159+
output: 'const foo = /\\uD834/'
160+
},
161+
{
162+
code: 'const foo = /\\u{1d306}/u',
163+
errors,
164+
output: 'const foo = /\\u{1D306}/u'
165+
},
166+
{
167+
code: 'const foo = /\\ca/',
168+
errors,
169+
output: 'const foo = /\\cA/'
170+
},
171+
{
172+
code: 'const foo = new RegExp("/\\xa9")',
173+
errors,
174+
output: 'const foo = new RegExp("/\\xA9")'
175+
},
176+
{
177+
code: 'const foo = new RegExp("/\\ud834/")',
178+
errors,
179+
output: 'const foo = new RegExp("/\\uD834/")'
180+
},
181+
{
182+
code: 'const foo = new RegExp("/\\u{1d306}/", "u")',
183+
errors,
184+
output: 'const foo = new RegExp("/\\u{1D306}/", "u")'
154185
}
155186
]
156187
});

0 commit comments

Comments
 (0)