Skip to content

Commit

Permalink
Add prefer-string-raw rule (#2339)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
fisker and sindresorhus authored May 9, 2024
1 parent aabcf1d commit 4f1400a
Show file tree
Hide file tree
Showing 24 changed files with 571 additions and 303 deletions.
30 changes: 30 additions & 0 deletions docs/rules/prefer-string-raw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Prefer using the `String.raw` tag to avoid escaping `\`

💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#preset-configs-eslintconfigjs).

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->

[`String.raw`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/raw) can be used to avoid escaping `\`.

## Fail

```js
const file = "C:\\windows\\style\\path\\to\\file.js";
```

```js
const regexp = new RegExp('foo\\.bar');
```

## Pass

```js
const file = String.raw`C:\windows\style\path\to\file.js`;
```

```js
const regexp = new RegExp(String.raw`foo\.bar`);
```
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@
"test/integration/{fixtures,fixtures-local}/**"
],
"rules": {
"unicorn/escape-case": "off",
"unicorn/expiring-todo-comments": "off",
"unicorn/no-hex-escape": "off",
"unicorn/no-null": "error",
"unicorn/prefer-array-flat": [
"error",
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
| [prefer-set-has](docs/rules/prefer-set-has.md) | Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence. || 🔧 | 💡 |
| [prefer-set-size](docs/rules/prefer-set-size.md) | Prefer using `Set#size` instead of `Array#length`. || 🔧 | |
| [prefer-spread](docs/rules/prefer-spread.md) | Prefer the spread operator over `Array.from(…)`, `Array#concat(…)`, `Array#{slice,toSpliced}()` and `String#split('')`. || 🔧 | 💡 |
| [prefer-string-raw](docs/rules/prefer-string-raw.md) | Prefer using the `String.raw` tag to avoid escaping `\`. || 🔧 | |
| [prefer-string-replace-all](docs/rules/prefer-string-replace-all.md) | Prefer `String#replaceAll()` over regex searches with the global flag. || 🔧 | |
| [prefer-string-slice](docs/rules/prefer-string-slice.md) | Prefer `String#slice()` over `String#substr()` and `String#substring()`. || 🔧 | |
| [prefer-string-starts-ends-with](docs/rules/prefer-string-starts-ends-with.md) | Prefer `String#startsWith()` & `String#endsWith()` over `RegExp#test()`. || 🔧 | 💡 |
Expand Down
1 change: 1 addition & 0 deletions rules/ast/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module.exports = {
isArrowFunctionBody: require('./is-arrow-function-body.js'),
isCallExpression,
isCallOrNewExpression,
isDirective: require('./is-directive.js'),
isEmptyNode: require('./is-empty-node.js'),
isExpressionStatement: require('./is-expression-statement.js'),
isFunction: require('./is-function.js'),
Expand Down
7 changes: 7 additions & 0 deletions rules/ast/is-directive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

const isDirective = node =>
node.type === 'ExpressionStatement'
&& typeof node.directive === 'string';

module.exports = isDirective;
3 changes: 1 addition & 2 deletions rules/no-empty-file.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
'use strict';
const {isEmptyNode} = require('./ast/index.js');
const {isEmptyNode, isDirective} = require('./ast/index.js');

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

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

const isTripleSlashDirective = node =>
Expand Down
4 changes: 2 additions & 2 deletions rules/no-unnecessary-polyfills.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ const additionalPolyfillPatterns = {

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

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

if (ecmaVersion === 'es') {
ecmaVersion = '(es\\d*)';
ecmaVersion = String.raw`(es\d*)`;
}

constructorName = `(${constructorName}|${camelCase(constructorName)})`;
Expand Down
93 changes: 93 additions & 0 deletions rules/prefer-string-raw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use strict';
const {isStringLiteral, isDirective} = require('./ast/index.js');
const {fixSpaceAroundKeyword} = require('./fix/index.js');

const MESSAGE_ID = 'prefer-string-raw';
const messages = {
[MESSAGE_ID]: '`String.raw` should be used to avoid escaping `\\`.',
};

const BACKSLASH = '\\';

function unescapeBackslash(raw) {
const quote = raw.charAt(0);

raw = raw.slice(1, -1);

let result = '';
for (let position = 0; position < raw.length; position++) {
const character = raw[position];
if (character === BACKSLASH) {
const nextCharacter = raw[position + 1];
if (nextCharacter === BACKSLASH || nextCharacter === quote) {
result += nextCharacter;
position++;
continue;
}
}

result += character;
}

return result;
}

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('Literal', node => {
if (
!isStringLiteral(node)
|| isDirective(node.parent)
|| (
(
node.parent.type === 'ImportDeclaration'
|| node.parent.type === 'ExportNamedDeclaration'
|| node.parent.type === 'ExportAllDeclaration'
) && node.parent.source === node
)
|| (node.parent.type === 'Property' && !node.parent.computed && node.parent.key === node)
|| (node.parent.type === 'JSXAttribute' && node.parent.value === node)
) {
return;
}

const {raw} = node;
if (
raw.at(-2) === BACKSLASH
|| !raw.includes(BACKSLASH + BACKSLASH)
|| raw.includes('`')
|| raw.includes('${')
|| node.loc.start.line !== node.loc.end.line
) {
return;
}

const unescaped = unescapeBackslash(raw);
if (unescaped !== node.value) {
return;
}

return {
node,
messageId: MESSAGE_ID,
* fix(fixer) {
yield fixer.replaceText(node, `String.raw\`${unescaped}\``);
yield * fixSpaceAroundKeyword(fixer, node, context.sourceCode);
},
};
});
};

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer using the `String.raw` tag to avoid escaping `\\`.',
recommended: true,
},
fixable: 'code',
messages,
},
};
2 changes: 1 addition & 1 deletion rules/utils/escape-template-element-raw.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

const escapeTemplateElementRaw = string => string.replaceAll(
/(?<=(?:^|[^\\])(?:\\\\)*)(?<symbol>(?:`|\$(?={)))/g,
'\\$<symbol>',
String.raw`\$<symbol>`,
);
module.exports = escapeTemplateElementRaw;
Loading

0 comments on commit 4f1400a

Please sign in to comment.