Skip to content

Commit 28f9147

Browse files
committed
feat: add support for template literals
1 parent 944cb50 commit 28f9147

File tree

4 files changed

+189
-23
lines changed

4 files changed

+189
-23
lines changed

rules/prefer-string-raw.js

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,9 @@ const messages = {
88

99
const BACKSLASH = '\\';
1010

11-
function unescapeBackslash(raw) {
12-
const quote = raw.charAt(0);
13-
14-
raw = raw.slice(1, -1);
15-
16-
let result = '';
17-
for (let position = 0; position < raw.length; position++) {
18-
const character = raw[position];
19-
if (character === BACKSLASH) {
20-
const nextCharacter = raw[position + 1];
21-
if (nextCharacter === BACKSLASH || nextCharacter === quote) {
22-
result += nextCharacter;
23-
position++;
24-
continue;
25-
}
26-
}
27-
28-
result += character;
29-
}
30-
31-
return result;
11+
function unescapeBackslash(value, quote = '') {
12+
return value
13+
.replaceAll(new RegExp(String.raw`\\([\\${quote}])`, 'g'), '$1');
3214
}
3315

3416
/** @param {import('eslint').Rule.RuleContext} context */
@@ -65,7 +47,7 @@ const create = context => {
6547
return;
6648
}
6749

68-
const unescaped = unescapeBackslash(raw);
50+
const unescaped = unescapeBackslash(raw.slice(1, -1), raw.charAt(0));
6951
if (unescaped !== node.value) {
7052
return;
7153
}
@@ -79,6 +61,52 @@ const create = context => {
7961
},
8062
};
8163
});
64+
65+
context.on('TemplateLiteral', node => {
66+
if (node.parent.type === 'TaggedTemplateExpression') {
67+
return;
68+
}
69+
70+
let suggestedValue = '';
71+
let hasBackslash = false;
72+
73+
for (let index = 0; index < node.quasis.length; index++) {
74+
const quasi = node.quasis[index];
75+
const {raw, cooked} = quasi.value;
76+
77+
if (cooked.at(-1) === BACKSLASH) {
78+
return;
79+
}
80+
81+
const unescaped = unescapeBackslash(raw);
82+
if (unescaped !== cooked) {
83+
return;
84+
}
85+
86+
if (cooked.includes(BACKSLASH)) {
87+
hasBackslash = true;
88+
}
89+
90+
if (index > 0) {
91+
suggestedValue += '${' + context.sourceCode.getText(node.expressions[index - 1]) + '}';
92+
}
93+
94+
suggestedValue += unescaped;
95+
}
96+
97+
if (!hasBackslash) {
98+
return;
99+
}
100+
101+
return {
102+
node,
103+
messageId: MESSAGE_ID,
104+
* fix(fixer) {
105+
yield fixer.replaceText(node, `String.raw\`${suggestedValue}\``);
106+
yield * fixSpaceAroundKeyword(fixer, node, context.sourceCode);
107+
},
108+
};
109+
});
82110
};
83111

84112
/** @type {import('eslint').Rule.RuleModule} */

test/prefer-string-raw.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable no-template-curly-in-string */
12
import outdent from 'outdent';
23
import {getTester} from './utils/test.js';
34

@@ -18,7 +19,6 @@ test.snapshot({
1819
`,
1920
String.raw`a = 'a\\b\u{51}c'`,
2021
'a = "a\\\\b`"',
21-
// eslint-disable-next-line no-template-curly-in-string
2222
'a = "a\\\\b${foo}"',
2323
{
2424
code: String.raw`<Component attribute="a\\b" />`,
@@ -43,6 +43,32 @@ test.snapshot({
4343
],
4444
});
4545

46+
test.snapshot({
47+
valid: [
48+
'a = `a`',
49+
'a = `${foo}`',
50+
'a = `a\\tb`',
51+
'a = `a\\``',
52+
'a = `a\\${`',
53+
'a = `a\\\\b\\\'`',
54+
'a = `a\\\\b\\"`',
55+
'a = `\\\\`',
56+
'a = `a${foo}b`',
57+
'a = `a\\\\b${foo}a\\tb`',
58+
outdent`
59+
a = \`\\\\a \\
60+
b\`
61+
`,
62+
],
63+
invalid: [
64+
'a = `a\\\\b`',
65+
'a = `a\\\\b${foo}cd`',
66+
'a = `a\\\\b${foo}cd${foo.bar}e\\\\f`',
67+
'a = `b${foo}${foo.bar}a\\\\b`',
68+
'a = `a\\\\b${"c\\\\d"}e`',
69+
],
70+
});
71+
4672
test.typescript({
4773
valid: [
4874
outdent`

test/snapshots/prefer-string-raw.js.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,115 @@ Generated by [AVA](https://avajs.dev).
8787
> 1 | const foo = "foo \\\\x46";␊
8888
| ^^^^^^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
8989
`
90+
91+
## invalid(1): a = `a\\b`
92+
93+
> Input
94+
95+
`␊
96+
1 | a = \`a\\\\b\`␊
97+
`
98+
99+
> Output
100+
101+
`␊
102+
1 | a = String.raw\`a\\b\`␊
103+
`
104+
105+
> Error 1/1
106+
107+
`␊
108+
> 1 | a = \`a\\\\b\`␊
109+
| ^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
110+
`
111+
112+
## invalid(2): a = `a\\b${foo}cd`
113+
114+
> Input
115+
116+
`␊
117+
1 | a = \`a\\\\b${foo}cd\`␊
118+
`
119+
120+
> Output
121+
122+
`␊
123+
1 | a = String.raw\`a\\b${foo}cd\`␊
124+
`
125+
126+
> Error 1/1
127+
128+
`␊
129+
> 1 | a = \`a\\\\b${foo}cd\`␊
130+
| ^^^^^^^^^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
131+
`
132+
133+
## invalid(3): a = `a\\b${foo}cd${foo.bar}e\\f`
134+
135+
> Input
136+
137+
`␊
138+
1 | a = \`a\\\\b${foo}cd${foo.bar}e\\\\f\`␊
139+
`
140+
141+
> Output
142+
143+
`␊
144+
1 | a = String.raw\`a\\b${foo}cd${foo.bar}e\\f\`␊
145+
`
146+
147+
> Error 1/1
148+
149+
`␊
150+
> 1 | a = \`a\\\\b${foo}cd${foo.bar}e\\\\f\`␊
151+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
152+
`
153+
154+
## invalid(4): a = `b${foo}${foo.bar}a\\b`
155+
156+
> Input
157+
158+
`␊
159+
1 | a = \`b${foo}${foo.bar}a\\\\b\`␊
160+
`
161+
162+
> Output
163+
164+
`␊
165+
1 | a = String.raw\`b${foo}${foo.bar}a\\b\`␊
166+
`
167+
168+
> Error 1/1
169+
170+
`␊
171+
> 1 | a = \`b${foo}${foo.bar}a\\\\b\`␊
172+
| ^^^^^^^^^^^^^^^^^^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
173+
`
174+
175+
## invalid(5): a = `a\\b${"c\\d"}e`
176+
177+
> Input
178+
179+
`␊
180+
1 | a = \`a\\\\b${"c\\\\d"}e\`␊
181+
`
182+
183+
> Output
184+
185+
`␊
186+
1 | a = String.raw\`a\\b${String.raw\`c\\d\`}e\`␊
187+
`
188+
189+
> Error 1/2
190+
191+
`␊
192+
> 1 | a = \`a\\\\b${"c\\\\d"}e\`␊
193+
| ^^^^^^^^^^^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
194+
`
195+
196+
> Error 2/2
197+
198+
`␊
199+
> 1 | a = \`a\\\\b${"c\\\\d"}e\`␊
200+
| ^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
201+
`
239 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)