Skip to content

prefer-string-raw: Forbid unnecessary String.raw #2695

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/rules/prefer-string-raw.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,17 @@ const file = String.raw`C:\windows\style\path\to\file.js`;
```js
const regexp = new RegExp(String.raw`foo\.bar`);
```

[`String.raw`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/raw) should not be used if the string does not contain any `\`.

## Fail

```js
const noBackslash = String.raw`foobar`
```

## Pass

```js
const noBackslash = 'foobar'
```
36 changes: 36 additions & 0 deletions rules/prefer-string-raw.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import {isStringLiteral, isDirective} from './ast/index.js';
import {fixSpaceAroundKeyword} from './fix/index.js';

const MESSAGE_ID = 'prefer-string-raw';
const MESSAGE_ID_UNNECESSARY_STRING_RAW = 'unnecessary-string-raw';
const messages = {
[MESSAGE_ID]: '`String.raw` should be used to avoid escaping `\\`.',
[MESSAGE_ID_UNNECESSARY_STRING_RAW]: 'Using `String.raw` is unnecessary as the string does not contain any `\\`.',
};

const BACKSLASH = '\\';
Expand Down Expand Up @@ -64,6 +66,40 @@ const create = context => {
},
};
});

context.on('TaggedTemplateExpression', node => {
const {quasi, tag} = node;

if (tag.type !== 'MemberExpression'
|| tag.object.type !== 'Identifier'
|| tag.property.type !== 'Identifier'
|| tag.object.name !== 'String'
|| tag.property.name !== 'raw'
) {
return;
}

const hasBackslash = quasi.quasis.some(
quasi => quasi.value.raw.includes(BACKSLASH),
);

if (hasBackslash) {
return;
}

const rawQuasi = context.sourceCode.getText(quasi);
const suggestion = quasi.expressions.length > 0 || /\r?\n/.test(rawQuasi)
? rawQuasi
: `'${rawQuasi.slice(1, -1).replaceAll('\'', String.raw`\'`)}'`;

return {
node,
messageId: MESSAGE_ID_UNNECESSARY_STRING_RAW,
* fix(fixer) {
yield fixer.replaceText(node, suggestion);
},
};
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need check this.

`\u0040`
'@'
String.raw`\u0040`
'\\u0040'

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe check .cooked === .raw?

Copy link
Contributor Author

@som-sm som-sm Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I didn't get this.


The following tests already work:

valid: [
  'a = String.raw`\\u0040`'
]
invalid: [
  'a = String.raw`\u0040`'
]

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I thought we suggest `\u0040` over String.raw`\u0040`, I must made a mistake.

Anyway, do you think we can check .cooked === .raw instead of checking contains \?

Copy link
Contributor Author

@som-sm som-sm Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway, do you think we can check .cooked === .raw instead of checking contains \?

Yeah, we can, updated in 2eb20bf.

Copy link
Contributor Author

@som-sm som-sm Jun 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above update, surfaced an interesting issue.

So, after this update, I'm getting a parsing error in the puppeteer codebase at the following line:
https://github.com/puppeteer/puppeteer/blob/832831d21c42ff542300c194c2b4f07274d8f82f/test/src/queryhandler.spec.ts#L373


Here's a simple reproduction of the same:

Source code:

let a = "\\38";

This correctly gets fixed to:

let a = String.raw`\38`;

However, the above String.raw`\38` expression fails with the typescript parser, throwing the following error:

Parsing error: Octal escape sequences are not allowed. Use the syntax '\x03'.

The includes(BACKSLASH) check wasn’t failing because it allowed the function to exit early when a backslash was detected in the raw value.

But, with the .cooked === .raw check, it fails because both cooked and raw values here are the same ('\\38'), causing the check to pass.
cooked value can't be '\38' because that '\38' is syntactically incorrect, maybe because of this the parser keeps the cooked value same as raw.

value: { cooked: '\\38', raw: '\\38' }

This works fine with the babel parser because looks like it sets the cooked value to null in cases like these.

value: { raw: '\\38', cooked: null }

So, I think it's better to keep the includes(BACKSLASH) check because it operates on the raw value, which seems to be a better choice in this case.

LMK if this makes sense, I'll add the let a = String.raw`\38`; test with the typescript parser.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is bug in typescript-eslint, can you report to them? I can do it if you don't want to.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd appreciate if you could, I'm fairly new to parsers, so I might not be able to explain it clearly.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

};

/** @type {import('eslint').Rule.RuleModule} */
Expand Down
40 changes: 39 additions & 1 deletion test/prefer-string-raw.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-template-curly-in-string */
import outdent from 'outdent';
import {getTester} from './utils/test.js';

Expand All @@ -18,7 +19,6 @@ test.snapshot({
`,
String.raw`a = 'a\\b\u{51}c'`,
'a = "a\\\\b`"',
// eslint-disable-next-line no-template-curly-in-string
'a = "a\\\\b${foo}"',
{
code: String.raw`<Component attribute="a\\b" />`,
Expand Down Expand Up @@ -47,6 +47,44 @@ test.snapshot({
],
});

test.snapshot({
valid: [
'a = foo`ab`',
'a = foo().bar`ab`',
'a = foo.bar()`ab`',
'a = String["raw"]`ab`',
'a = foo.raw`ab`',
'a = String.foo`ab`',
'a = String.raw`a\\b`',
'a = String.raw`a\\b`',
'a = String.raw`a\\b${foo}cd`',
'a = String.raw`ab${foo}c\\nd`',
outdent`
a = String.raw\`a
b\\c
de\`
`,
],
invalid: [
'a = String.raw`abc`',
'a = String.raw`ab${foo}cd`',
'a = String.raw`ab"c`',
'a = String.raw`ab\'c`',
'a = String.raw`ab\'"c`',
'a = String.raw`ab\r\nc`',
outdent`
a = String.raw\`a
bc
de\`
`,
outdent`
a = String.raw\`
a\${foo}b
\${bar}cd\`
`,
],
});

test.typescript({
valid: [
outdent`
Expand Down
188 changes: 188 additions & 0 deletions test/snapshots/prefer-string-raw.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,191 @@ Generated by [AVA](https://avajs.dev).
> 1 | a = "a\\\\b\\""␊
| ^^^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊
`

## invalid(1): a = String.raw`abc`

> Input

`␊
1 | a = String.raw\`abc\`␊
`

> Output

`␊
1 | a = 'abc'␊
`

> Error 1/1

`␊
> 1 | a = String.raw\`abc\`␊
| ^^^^^^^^^^^^^^^ Using \`String.raw\` is unnecessary as the string does not contain any \`\\\`.␊
`

## invalid(2): a = String.raw`ab${foo}cd`

> Input

`␊
1 | a = String.raw\`ab${foo}cd\`␊
`

> Output

`␊
1 | a = \`ab${foo}cd\`␊
`

> Error 1/1

`␊
> 1 | a = String.raw\`ab${foo}cd\`␊
| ^^^^^^^^^^^^^^^^^^^^^^ Using \`String.raw\` is unnecessary as the string does not contain any \`\\\`.␊
`

## invalid(3): a = String.raw`ab"c`

> Input

`␊
1 | a = String.raw\`ab"c\`␊
`

> Output

`␊
1 | a = 'ab"c'␊
`

> Error 1/1

`␊
> 1 | a = String.raw\`ab"c\`␊
| ^^^^^^^^^^^^^^^^ Using \`String.raw\` is unnecessary as the string does not contain any \`\\\`.␊
`

## invalid(4): a = String.raw`ab'c`

> Input

`␊
1 | a = String.raw\`ab'c\`␊
`

> Output

`␊
1 | a = 'ab\\'c'␊
`

> Error 1/1

`␊
> 1 | a = String.raw\`ab'c\`␊
| ^^^^^^^^^^^^^^^^ Using \`String.raw\` is unnecessary as the string does not contain any \`\\\`.␊
`

## invalid(5): a = String.raw`ab'"c`

> Input

`␊
1 | a = String.raw\`ab'"c\`␊
`

> Output

`␊
1 | a = 'ab\\'"c'␊
`

> Error 1/1

`␊
> 1 | a = String.raw\`ab'"c\`␊
| ^^^^^^^^^^^^^^^^^ Using \`String.raw\` is unnecessary as the string does not contain any \`\\\`.␊
`

## invalid(6): a = String.raw`ab c`

> Input

`␊
1 | a = String.raw\`ab␊
2 | c\`␊
`

> Output

`␊
1 | a = \`ab␊
2 | c\`␊
`

> Error 1/1

`␊
> 1 | a = String.raw\`ab␊
| ^^^^^^^^^^^^^␊
> 2 | c\`␊
| ^^^ Using \`String.raw\` is unnecessary as the string does not contain any \`\\\`.␊
`

## invalid(7): a = String.raw`a bc de`

> Input

`␊
1 | a = String.raw\`a␊
2 | bc␊
3 | de\`␊
`

> Output

`␊
1 | a = \`a␊
2 | bc␊
3 | de\`␊
`

> Error 1/1

`␊
> 1 | a = String.raw\`a␊
| ^^^^^^^^^^^^␊
> 2 | bc␊
| ^^^␊
> 3 | de\`␊
| ^^^^^ Using \`String.raw\` is unnecessary as the string does not contain any \`\\\`.␊
`

## invalid(8): a = String.raw` a${foo}b ${bar}cd`

> Input

`␊
1 | a = String.raw\`␊
2 | a${foo}b␊
3 | ${bar}cd\`␊
`

> Output

`␊
1 | a = \`␊
2 | a${foo}b␊
3 | ${bar}cd\`␊
`

> Error 1/1

`␊
> 1 | a = String.raw\`␊
| ^^^^^^^^^^^␊
> 2 | a${foo}b␊
| ^^^^^^^^␊
> 3 | ${bar}cd\`␊
| ^^^^^^^^^^ Using \`String.raw\` is unnecessary as the string does not contain any \`\\\`.␊
`
Binary file modified test/snapshots/prefer-string-raw.js.snap
Binary file not shown.
Loading