Skip to content

Commit 52723a2

Browse files
som-smfisker
andauthored
prefer-string-raw: Add support for template literals (#2691)
Co-authored-by: fisker <lionkay@gmail.com>
1 parent 0c93998 commit 52723a2

File tree

6 files changed

+323
-12
lines changed

6 files changed

+323
-12
lines changed

docs/rules/prefer-string-raw.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,11 @@ const regexp = new RegExp('foo\\.bar');
2626
//
2727
const regexp = new RegExp(String.raw`foo\.bar`);
2828
```
29+
30+
```js
31+
//
32+
const file = `C:\\windows\\temp\\myapp-${process.pid}.log`;
33+
34+
//
35+
const file = String.raw`C:\windows\temp\myapp-${process.pid}.log`;
36+
```

rules/prefer-string-raw.js

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {isStringLiteral, isDirective} from './ast/index.js';
2-
import {fixSpaceAroundKeyword} from './fix/index.js';
2+
import {fixSpaceAroundKeyword, replaceTemplateElement} from './fix/index.js';
33

44
const MESSAGE_ID = 'prefer-string-raw';
55
const messages = {
@@ -8,12 +8,8 @@ const messages = {
88

99
const BACKSLASH = '\\';
1010

11-
function unescapeBackslash(raw) {
12-
const quote = raw.charAt(0);
13-
14-
return raw
15-
.slice(1, -1)
16-
.replaceAll(new RegExp(String.raw`\\(?<escapedCharacter>[\\${quote}])`, 'g'), '$<escapedCharacter>');
11+
function unescapeBackslash(text, quote = '') {
12+
return text.replaceAll(new RegExp(String.raw`\\(?<escapedCharacter>[\\${quote}])`, 'g'), '$<escapedCharacter>');
1713
}
1814

1915
/** @param {import('eslint').Rule.RuleContext} context */
@@ -50,7 +46,7 @@ const create = context => {
5046
return;
5147
}
5248

53-
const unescaped = unescapeBackslash(raw);
49+
const unescaped = unescapeBackslash(raw.slice(1, -1), raw.charAt(0));
5450
if (unescaped !== node.value) {
5551
return;
5652
}
@@ -59,8 +55,36 @@ const create = context => {
5955
node,
6056
messageId: MESSAGE_ID,
6157
* fix(fixer) {
62-
yield fixer.replaceText(node, `String.raw\`${unescaped}\``);
6358
yield * fixSpaceAroundKeyword(fixer, node, sourceCode);
59+
yield fixer.replaceText(node, `String.raw\`${unescaped}\``);
60+
},
61+
};
62+
});
63+
64+
context.on('TemplateLiteral', node => {
65+
if (
66+
(node.parent.type === 'TaggedTemplateExpression' && node.parent.quasi === node)
67+
|| node.quasis.every(({value: {cooked, raw}}) => cooked === raw)
68+
|| node.quasis.some(({value: {cooked, raw}}) => cooked.at(-1) === BACKSLASH || unescapeBackslash(raw) !== cooked)
69+
) {
70+
return;
71+
}
72+
73+
return {
74+
node,
75+
messageId: MESSAGE_ID,
76+
* fix(fixer) {
77+
yield * fixSpaceAroundKeyword(fixer, node, context.sourceCode);
78+
yield fixer.insertTextBefore(node, 'String.raw');
79+
80+
for (const quasis of node.quasis) {
81+
const {cooked, raw} = quasis.value;
82+
if (cooked === raw) {
83+
continue;
84+
}
85+
86+
yield replaceTemplateElement(fixer, quasis, cooked);
87+
}
6488
},
6589
};
6690
});

rules/prefer-string-replace-all.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,11 @@ function getPatternReplacement(node) {
5757
return String.raw`\t`;
5858
}
5959

60-
return `\\u{${codePoint.toString(16)}}`;
60+
return String.raw`\u{${codePoint.toString(16)}}`;
6161
}
6262

6363
if (kind === 'octal') {
64-
return `\\u{${codePoint.toString(16)}}`;
64+
return String.raw`\u{${codePoint.toString(16)}}`;
6565
}
6666

6767
let character = raw;

test/prefer-string-raw.js

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

45
const {test} = getTester(import.meta);
56

7+
// String literal to `String.raw`
68
test.snapshot({
79
valid: [
810
String.raw`a = '\''`,
@@ -18,7 +20,6 @@ test.snapshot({
1820
`,
1921
String.raw`a = 'a\\b\u{51}c'`,
2022
'a = "a\\\\b`"',
21-
// eslint-disable-next-line no-template-curly-in-string
2223
'a = "a\\\\b${foo}"',
2324
{
2425
code: String.raw`<Component attribute="a\\b" />`,
@@ -47,6 +48,55 @@ test.snapshot({
4748
],
4849
});
4950

51+
// `TemplateLiteral` to `String.raw`
52+
test.snapshot({
53+
valid: [
54+
// No backslash
55+
'a = `a`',
56+
'a = `${foo}`',
57+
'a = `a${100}b`',
58+
59+
// Escaped characters other than backslash
60+
'a = `a\\t${foo.bar}b\\\\c`', // \t
61+
'a = `${foo}\\\\a${bar}\\``', // \`
62+
'a = `a\\${`', // \$
63+
'a = `a$\\{`', // \{
64+
'a = `${a}\\\'${b}\\\\`', // \'
65+
'a = `\\"a\\\\b`', // \"
66+
67+
// Ending with backslash
68+
'a = `\\\\a${foo}b\\\\${foo}`',
69+
70+
// Tagged template expression
71+
'a = String.raw`a\\\\b`',
72+
73+
// Slash before newline (spread into multiple lines)
74+
outdent`
75+
a = \`\\\\a \\
76+
b\`
77+
`,
78+
],
79+
invalid: [
80+
'a = `a\\\\b`',
81+
'function a() {return`a\\\\b${foo}cd`}',
82+
'a = {[`a\\\\b${foo}cd${foo.bar}e\\\\f`]: b}',
83+
'a = `a${foo}${foo.bar}b\\\\c`',
84+
'a = `a\\\\b${"c\\\\d"}e`',
85+
outdent`
86+
a = \`\\\\a
87+
b\`
88+
`,
89+
outdent`
90+
a = \`\\\\a\${foo}
91+
b\${bar}c
92+
d\\\\\\\\e\`
93+
`,
94+
'a = `a\\\\b${ foo /* bar */}c\\\\d`',
95+
'a = `a\\\\b${ foo + bar }`',
96+
'a = `${ foo .bar }a\\\\b`',
97+
],
98+
});
99+
50100
test.typescript({
51101
valid: [
52102
outdent`

0 commit comments

Comments
 (0)