Skip to content

Commit 4f1400a

Browse files
fiskersindresorhus
andauthored
Add prefer-string-raw rule (#2339)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent aabcf1d commit 4f1400a

24 files changed

+571
-303
lines changed

docs/rules/prefer-string-raw.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Prefer using the `String.raw` tag to avoid escaping `\`
2+
3+
💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs-eslintconfigjs).
4+
5+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
6+
7+
<!-- end auto-generated rule header -->
8+
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->
9+
10+
[`String.raw`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/raw) can be used to avoid escaping `\`.
11+
12+
## Fail
13+
14+
```js
15+
const file = "C:\\windows\\style\\path\\to\\file.js";
16+
```
17+
18+
```js
19+
const regexp = new RegExp('foo\\.bar');
20+
```
21+
22+
## Pass
23+
24+
```js
25+
const file = String.raw`C:\windows\style\path\to\file.js`;
26+
```
27+
28+
```js
29+
const regexp = new RegExp(String.raw`foo\.bar`);
30+
```

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@
124124
"test/integration/{fixtures,fixtures-local}/**"
125125
],
126126
"rules": {
127+
"unicorn/escape-case": "off",
127128
"unicorn/expiring-todo-comments": "off",
129+
"unicorn/no-hex-escape": "off",
128130
"unicorn/no-null": "error",
129131
"unicorn/prefer-array-flat": [
130132
"error",

readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
207207
| [prefer-set-has](docs/rules/prefer-set-has.md) | Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence. || 🔧 | 💡 |
208208
| [prefer-set-size](docs/rules/prefer-set-size.md) | Prefer using `Set#size` instead of `Array#length`. || 🔧 | |
209209
| [prefer-spread](docs/rules/prefer-spread.md) | Prefer the spread operator over `Array.from(…)`, `Array#concat(…)`, `Array#{slice,toSpliced}()` and `String#split('')`. || 🔧 | 💡 |
210+
| [prefer-string-raw](docs/rules/prefer-string-raw.md) | Prefer using the `String.raw` tag to avoid escaping `\`. || 🔧 | |
210211
| [prefer-string-replace-all](docs/rules/prefer-string-replace-all.md) | Prefer `String#replaceAll()` over regex searches with the global flag. || 🔧 | |
211212
| [prefer-string-slice](docs/rules/prefer-string-slice.md) | Prefer `String#slice()` over `String#substr()` and `String#substring()`. || 🔧 | |
212213
| [prefer-string-starts-ends-with](docs/rules/prefer-string-starts-ends-with.md) | Prefer `String#startsWith()` & `String#endsWith()` over `RegExp#test()`. || 🔧 | 💡 |

rules/ast/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module.exports = {
2525
isArrowFunctionBody: require('./is-arrow-function-body.js'),
2626
isCallExpression,
2727
isCallOrNewExpression,
28+
isDirective: require('./is-directive.js'),
2829
isEmptyNode: require('./is-empty-node.js'),
2930
isExpressionStatement: require('./is-expression-statement.js'),
3031
isFunction: require('./is-function.js'),

rules/ast/is-directive.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
3+
const isDirective = node =>
4+
node.type === 'ExpressionStatement'
5+
&& typeof node.directive === 'string';
6+
7+
module.exports = isDirective;

rules/no-empty-file.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
'use strict';
2-
const {isEmptyNode} = require('./ast/index.js');
2+
const {isEmptyNode, isDirective} = require('./ast/index.js');
33

44
const MESSAGE_ID = 'no-empty-file';
55
const messages = {
66
[MESSAGE_ID]: 'Empty files are not allowed.',
77
};
88

9-
const isDirective = node => node.type === 'ExpressionStatement' && typeof node.directive === 'string';
109
const isEmpty = node => isEmptyNode(node, isDirective);
1110

1211
const isTripleSlashDirective = node =>

rules/no-unnecessary-polyfills.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@ const additionalPolyfillPatterns = {
2323

2424
const prefixes = '(mdn-polyfills/|polyfill-)';
2525
const suffixes = '(-polyfill)';
26-
const delimiter = '(\\.|-|\\.prototype\\.|/)?';
26+
const delimiter = String.raw`(\.|-|\.prototype\.|/)?`;
2727

2828
const polyfills = Object.keys(compatData).map(feature => {
2929
let [ecmaVersion, constructorName, methodName = ''] = feature.split('.');
3030

3131
if (ecmaVersion === 'es') {
32-
ecmaVersion = '(es\\d*)';
32+
ecmaVersion = String.raw`(es\d*)`;
3333
}
3434

3535
constructorName = `(${constructorName}|${camelCase(constructorName)})`;

rules/prefer-string-raw.js

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
'use strict';
2+
const {isStringLiteral, isDirective} = require('./ast/index.js');
3+
const {fixSpaceAroundKeyword} = require('./fix/index.js');
4+
5+
const MESSAGE_ID = 'prefer-string-raw';
6+
const messages = {
7+
[MESSAGE_ID]: '`String.raw` should be used to avoid escaping `\\`.',
8+
};
9+
10+
const BACKSLASH = '\\';
11+
12+
function unescapeBackslash(raw) {
13+
const quote = raw.charAt(0);
14+
15+
raw = raw.slice(1, -1);
16+
17+
let result = '';
18+
for (let position = 0; position < raw.length; position++) {
19+
const character = raw[position];
20+
if (character === BACKSLASH) {
21+
const nextCharacter = raw[position + 1];
22+
if (nextCharacter === BACKSLASH || nextCharacter === quote) {
23+
result += nextCharacter;
24+
position++;
25+
continue;
26+
}
27+
}
28+
29+
result += character;
30+
}
31+
32+
return result;
33+
}
34+
35+
/** @param {import('eslint').Rule.RuleContext} context */
36+
const create = context => {
37+
context.on('Literal', node => {
38+
if (
39+
!isStringLiteral(node)
40+
|| isDirective(node.parent)
41+
|| (
42+
(
43+
node.parent.type === 'ImportDeclaration'
44+
|| node.parent.type === 'ExportNamedDeclaration'
45+
|| node.parent.type === 'ExportAllDeclaration'
46+
) && node.parent.source === node
47+
)
48+
|| (node.parent.type === 'Property' && !node.parent.computed && node.parent.key === node)
49+
|| (node.parent.type === 'JSXAttribute' && node.parent.value === node)
50+
) {
51+
return;
52+
}
53+
54+
const {raw} = node;
55+
if (
56+
raw.at(-2) === BACKSLASH
57+
|| !raw.includes(BACKSLASH + BACKSLASH)
58+
|| raw.includes('`')
59+
|| raw.includes('${')
60+
|| node.loc.start.line !== node.loc.end.line
61+
) {
62+
return;
63+
}
64+
65+
const unescaped = unescapeBackslash(raw);
66+
if (unescaped !== node.value) {
67+
return;
68+
}
69+
70+
return {
71+
node,
72+
messageId: MESSAGE_ID,
73+
* fix(fixer) {
74+
yield fixer.replaceText(node, `String.raw\`${unescaped}\``);
75+
yield * fixSpaceAroundKeyword(fixer, node, context.sourceCode);
76+
},
77+
};
78+
});
79+
};
80+
81+
/** @type {import('eslint').Rule.RuleModule} */
82+
module.exports = {
83+
create,
84+
meta: {
85+
type: 'suggestion',
86+
docs: {
87+
description: 'Prefer using the `String.raw` tag to avoid escaping `\\`.',
88+
recommended: true,
89+
},
90+
fixable: 'code',
91+
messages,
92+
},
93+
};

rules/utils/escape-template-element-raw.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
const escapeTemplateElementRaw = string => string.replaceAll(
44
/(?<=(?:^|[^\\])(?:\\\\)*)(?<symbol>(?:`|\$(?={)))/g,
5-
'\\$<symbol>',
5+
String.raw`\$<symbol>`,
66
);
77
module.exports = escapeTemplateElementRaw;

0 commit comments

Comments
 (0)