Skip to content

Commit 43bc429

Browse files
authored
prefer-string-raw: Ignore more places that requires a string (#2776)
1 parent 52723a2 commit 43bc429

File tree

4 files changed

+219
-81
lines changed

4 files changed

+219
-81
lines changed

rules/prefer-string-raw.js

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,67 @@ function unescapeBackslash(text, quote = '') {
1212
return text.replaceAll(new RegExp(String.raw`\\(?<escapedCharacter>[\\${quote}])`, 'g'), '$<escapedCharacter>');
1313
}
1414

15+
/**
16+
Check if a string literal is restricted to replace with a `String.raw`
17+
*/
18+
// eslint-disable-next-line complexity
19+
function isStringRawRestricted(node) {
20+
const {parent} = node;
21+
const {type} = parent;
22+
return (
23+
// Directive
24+
isDirective(parent)
25+
// Property, method, or accessor key (only non-computed)
26+
|| (
27+
(
28+
type === 'Property'
29+
|| type === 'PropertyDefinition'
30+
|| type === 'MethodDefinition'
31+
|| type === 'AccessorProperty'
32+
)
33+
&& !parent.computed && parent.key === node
34+
)
35+
// Property, method, or accessor key (always)
36+
|| (
37+
(
38+
type === 'TSAbstractPropertyDefinition'
39+
|| type === 'TSAbstractMethodDefinition'
40+
|| type === 'TSAbstractAccessorProperty'
41+
|| type === 'TSPropertySignature'
42+
)
43+
&& parent.key === node
44+
)
45+
// Module source
46+
|| (
47+
(
48+
type === 'ImportDeclaration'
49+
|| type === 'ExportNamedDeclaration'
50+
|| type === 'ExportAllDeclaration'
51+
) && parent.source === node
52+
)
53+
// Import attribute key and value
54+
|| (type === 'ImportAttribute' && (parent.key === node || parent.value === node))
55+
// Module specifier
56+
|| (type === 'ImportSpecifier' && parent.imported === node)
57+
|| (type === 'ExportSpecifier' && (parent.local === node || parent.exported === node))
58+
|| (type === 'ExportAllDeclaration' && parent.exported === node)
59+
// JSX attribute value
60+
|| (type === 'JSXAttribute' && parent.value === node)
61+
// (TypeScript) Enum member key and value
62+
|| (type === 'TSEnumMember' && (parent.initializer === node || parent.id === node))
63+
// (TypeScript) Module declaration
64+
|| (type === 'TSModuleDeclaration' && parent.id === node)
65+
// (TypeScript) CommonJS module reference
66+
|| (type === 'TSExternalModuleReference' && parent.expression === node)
67+
// (TypeScript) Literal type
68+
|| (type === 'TSLiteralType' && parent.literal === node)
69+
);
70+
}
71+
1572
/** @param {import('eslint').Rule.RuleContext} context */
1673
const create = context => {
17-
// eslint-disable-next-line complexity
1874
context.on('Literal', node => {
19-
if (
20-
!isStringLiteral(node)
21-
|| isDirective(node.parent)
22-
|| (
23-
(
24-
node.parent.type === 'ImportDeclaration'
25-
|| node.parent.type === 'ExportNamedDeclaration'
26-
|| node.parent.type === 'ExportAllDeclaration'
27-
) && node.parent.source === node
28-
)
29-
|| (node.parent.type === 'Property' && !node.parent.computed && node.parent.key === node)
30-
|| (node.parent.type === 'JSXAttribute' && node.parent.value === node)
31-
|| (node.parent.type === 'TSEnumMember' && (node.parent.initializer === node || node.parent.id === node))
32-
|| (node.parent.type === 'ImportAttribute' && (node.parent.key === node || node.parent.value === node))
33-
) {
75+
if (!isStringLiteral(node) || isStringRawRestricted(node)) {
3476
return;
3577
}
3678

test/prefer-string-raw.js

Lines changed: 68 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,27 @@
11
/* eslint-disable no-template-curly-in-string */
22
import outdent from 'outdent';
3-
import {getTester} from './utils/test.js';
3+
import {getTester, parsers} from './utils/test.js';
44

55
const {test} = getTester(import.meta);
66

7+
const TEST_STRING = String.raw`a\\b`;
8+
79
// String literal to `String.raw`
810
test.snapshot({
911
valid: [
1012
String.raw`a = '\''`,
11-
// Cannot use `String.raw`
12-
String.raw`'a\\b'`,
13-
String.raw`import foo from "./foo\\bar.js";`,
14-
String.raw`export {foo} from "./foo\\bar.js";`,
15-
String.raw`export * from "./foo\\bar.js";`,
16-
String.raw`a = {'a\\b': ''}`,
1713
outdent`
1814
a = "\\\\a \\
1915
b"
2016
`,
2117
String.raw`a = 'a\\b\u{51}c'`,
2218
'a = "a\\\\b`"',
2319
'a = "a\\\\b${foo}"',
24-
{
25-
code: String.raw`<Component attribute="a\\b" />`,
26-
languageOptions: {
27-
parserOptions: {
28-
ecmaFeatures: {
29-
jsx: true,
30-
},
31-
},
32-
},
33-
},
34-
String.raw`import {} from "foo" with {key: "value\\value"}`,
35-
String.raw`import {} from "foo" with {"key\\key": "value"}`,
36-
String.raw`export {} from "foo" with {key: "value\\value"}`,
37-
String.raw`export {} from "foo" with {"key\\key": "value"}`,
3820
String.raw`a = '\\'`,
3921
String.raw`a = 'a\\b\"'`,
4022
],
4123
invalid: [
42-
String.raw`a = 'a\\b'`,
43-
String.raw`a = {['a\\b']: b}`,
24+
String.raw`TEST_STRING = '${TEST_STRING}';`,
4425
String.raw`function a() {return'a\\b'}`,
4526
String.raw`const foo = "foo \\x46";`,
4627
String.raw`a = 'a\\b\''`,
@@ -97,18 +78,70 @@ test.snapshot({
9778
],
9879
});
9980

100-
test.typescript({
81+
// Restricted places
82+
const keyTestsComputedIsInvalid = [
83+
// Object property key
84+
String.raw`({ '${TEST_STRING}': 1 })`,
85+
// Class members key
86+
String.raw`class C { '${TEST_STRING}' = 1 }`,
87+
String.raw`class C { '${TEST_STRING}'(){} }`,
88+
String.raw`class C { accessor '${TEST_STRING}' = 1 }`,
89+
];
90+
const keyTestsComputedIsValid = [
91+
// Abstract class members key
92+
String.raw`abstract class C { abstract '${TEST_STRING}' }`,
93+
String.raw`abstract class C { abstract '${TEST_STRING}'() }`,
94+
String.raw`abstract class C { abstract accessor '${TEST_STRING}' }`,
95+
// Interface members key
96+
String.raw`interface I { '${TEST_STRING}' }`,
97+
];
98+
const toComputed = code => code.replace(String.raw`'${TEST_STRING}'`, String.raw`['${TEST_STRING}']`);
99+
test.snapshot({
100+
testerOptions: {
101+
languageOptions: {parser: parsers.typescript},
102+
},
101103
valid: [
102-
outdent`
103-
enum Files {
104-
Foo = "C:\\\\path\\\\to\\\\foo.js",
105-
}
106-
`,
107-
outdent`
108-
enum Foo {
109-
"\\\\a\\\\b" = "baz",
110-
}
111-
`,
104+
// Directive
105+
String.raw`'${TEST_STRING}';`,
106+
// Module source
107+
String.raw`import '${TEST_STRING}';`,
108+
String.raw`export {} from '${TEST_STRING}';`,
109+
String.raw`export * from '${TEST_STRING}';`,
110+
// Import attribute key
111+
String.raw`import 'm' with {'${TEST_STRING}': 'v'};`,
112+
String.raw`export {} from 'm' with {'${TEST_STRING}': 'v'};`,
113+
// Import attribute value
114+
String.raw`import 'm' with {k: '${TEST_STRING}'};`,
115+
String.raw`export {} from 'm' with {k: '${TEST_STRING}'};`,
116+
// Module specifier
117+
String.raw`import {'${TEST_STRING}' as s} from 'm';`,
118+
String.raw`export {'${TEST_STRING}' as s} from 'm';`,
119+
String.raw`export {s as '${TEST_STRING}'} from 'm';`,
120+
String.raw`export * as '${TEST_STRING}' from 'm';`,
121+
122+
// JSX attribute value
123+
{
124+
code: String.raw`<Component attribute='${TEST_STRING}' />`,
125+
languageOptions: {
126+
parserOptions: {
127+
ecmaFeatures: {
128+
jsx: true,
129+
},
130+
},
131+
},
132+
},
133+
// (TypeScript) Enum member key and value
134+
String.raw`enum E {'${TEST_STRING}' = 1}`,
135+
String.raw`enum E {K = '${TEST_STRING}'}`,
136+
// (TypeScript) Module declaration
137+
String.raw`module '${TEST_STRING}' {}`,
138+
// (TypeScript) CommonJS module reference
139+
String.raw`import type T = require('${TEST_STRING}');`,
140+
// (TypeScript) Literal type
141+
String.raw`type T = '${TEST_STRING}';`,
142+
...keyTestsComputedIsInvalid,
143+
...keyTestsComputedIsValid.flatMap(code => [code, toComputed(code)]),
112144
],
113-
invalid: [],
145+
invalid: keyTestsComputedIsInvalid.map(code => toComputed(code)),
114146
});
147+

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

Lines changed: 93 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,49 +4,28 @@ The actual snapshot is saved in `prefer-string-raw.js.snap`.
44

55
Generated by [AVA](https://avajs.dev).
66

7-
## invalid(1): a = 'a\\b'
7+
## invalid(1): TEST_STRING = 'a\\b';
88

99
> Input
1010
1111
`␊
12-
1 | a = 'a\\\\b'␊
12+
1 | TEST_STRING = 'a\\\\b';
1313
`
1414

1515
> Output
1616
1717
`␊
18-
1 | a = String.raw\`a\\b\`␊
19-
`
20-
21-
> Error 1/1
22-
23-
`␊
24-
> 1 | a = 'a\\\\b'␊
25-
| ^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
26-
`
27-
28-
## invalid(2): a = {['a\\b']: b}
29-
30-
> Input
31-
32-
`␊
33-
1 | a = {['a\\\\b']: b}␊
34-
`
35-
36-
> Output
37-
38-
`␊
39-
1 | a = {[String.raw\`a\\b\`]: b}␊
18+
1 | TEST_STRING = String.raw\`a\\b\`;␊
4019
`
4120

4221
> Error 1/1
4322
4423
`␊
45-
> 1 | a = {['a\\\\b']: b}
46-
| ^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
24+
> 1 | TEST_STRING = 'a\\\\b';
25+
| ^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
4726
`
4827

49-
## invalid(3): function a() {return'a\\b'}
28+
## invalid(2): function a() {return'a\\b'}
5029

5130
> Input
5231
@@ -67,7 +46,7 @@ Generated by [AVA](https://avajs.dev).
6746
| ^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
6847
`
6948

70-
## invalid(4): const foo = "foo \\x46";
49+
## invalid(3): const foo = "foo \\x46";
7150

7251
> Input
7352
@@ -88,7 +67,7 @@ Generated by [AVA](https://avajs.dev).
8867
| ^^^^^^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
8968
`
9069

91-
## invalid(5): a = 'a\\b\''
70+
## invalid(4): a = 'a\\b\''
9271

9372
> Input
9473
@@ -109,7 +88,7 @@ Generated by [AVA](https://avajs.dev).
10988
| ^^^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
11089
`
11190

112-
## invalid(6): a = "a\\b\""
91+
## invalid(5): a = "a\\b\""
11392

11493
> Input
11594
@@ -358,3 +337,87 @@ Generated by [AVA](https://avajs.dev).
358337
> 1 | a = \`${ foo .bar }a\\\\b\`␊
359338
| ^^^^^^^^^^^^^^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
360339
`
340+
341+
## invalid(1): ({ ['a\\b']: 1 })
342+
343+
> Input
344+
345+
`␊
346+
1 | ({ ['a\\\\b']: 1 })␊
347+
`
348+
349+
> Output
350+
351+
`␊
352+
1 | ({ [String.raw\`a\\b\`]: 1 })␊
353+
`
354+
355+
> Error 1/1
356+
357+
`␊
358+
> 1 | ({ ['a\\\\b']: 1 })␊
359+
| ^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
360+
`
361+
362+
## invalid(2): class C { ['a\\b'] = 1 }
363+
364+
> Input
365+
366+
`␊
367+
1 | class C { ['a\\\\b'] = 1 }␊
368+
`
369+
370+
> Output
371+
372+
`␊
373+
1 | class C { [String.raw\`a\\b\`] = 1 }␊
374+
`
375+
376+
> Error 1/1
377+
378+
`␊
379+
> 1 | class C { ['a\\\\b'] = 1 }␊
380+
| ^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
381+
`
382+
383+
## invalid(3): class C { ['a\\b'](){} }
384+
385+
> Input
386+
387+
`␊
388+
1 | class C { ['a\\\\b'](){} }␊
389+
`
390+
391+
> Output
392+
393+
`␊
394+
1 | class C { [String.raw\`a\\b\`](){} }␊
395+
`
396+
397+
> Error 1/1
398+
399+
`␊
400+
> 1 | class C { ['a\\\\b'](){} }␊
401+
| ^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
402+
`
403+
404+
## invalid(4): class C { accessor ['a\\b'] = 1 }
405+
406+
> Input
407+
408+
`␊
409+
1 | class C { accessor ['a\\\\b'] = 1 }␊
410+
`
411+
412+
> Output
413+
414+
`␊
415+
1 | class C { accessor [String.raw\`a\\b\`] = 1 }␊
416+
`
417+
418+
> Error 1/1
419+
420+
`␊
421+
> 1 | class C { accessor ['a\\\\b'] = 1 }␊
422+
| ^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
423+
`
149 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)