diff --git a/docs/rules/prefer-string-raw.md b/docs/rules/prefer-string-raw.md index 2f8b608bb1..678caabb54 100644 --- a/docs/rules/prefer-string-raw.md +++ b/docs/rules/prefer-string-raw.md @@ -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' +``` diff --git a/rules/prefer-string-raw.js b/rules/prefer-string-raw.js index 32ea732218..e70c128765 100644 --- a/rules/prefer-string-raw.js +++ b/rules/prefer-string-raw.js @@ -1,9 +1,12 @@ -import {isStringLiteral, isDirective} from './ast/index.js'; +import {isStringLiteral, isDirective, isMemberExpression} from './ast/index.js'; import {fixSpaceAroundKeyword} from './fix/index.js'; +import needsSemicolon from './utils/needs-semicolon.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 = '\\'; @@ -64,6 +67,42 @@ const create = context => { }, }; }); + + context.on('TaggedTemplateExpression', node => { + const {quasi, tag} = node; + const {sourceCode} = context; + + if (!isMemberExpression(tag, {object: 'String', property: 'raw', optional: false})) { + return; + } + + const hasBackslash = quasi.quasis.some( + quasi => quasi.value.raw.includes(BACKSLASH), + ); + + if (hasBackslash) { + return; + } + + const rawQuasi = sourceCode.getText(quasi); + const suggestion = quasi.expressions.length > 0 || /\r?\n/.test(rawQuasi) + ? rawQuasi + : `'${rawQuasi.slice(1, -1).replaceAll('\'', String.raw`\'`)}'`; + + return { + node: tag, + messageId: MESSAGE_ID_UNNECESSARY_STRING_RAW, + * fix(fixer) { + const tokenBefore = sourceCode.getTokenBefore(node); + if (needsSemicolon(tokenBefore, sourceCode, suggestion)) { + yield fixer.insertTextBefore(node, ';'); + } + + yield fixer.replaceText(node.quasi, suggestion); + yield fixer.remove(node.tag); + }, + }; + }); }; /** @type {import('eslint').Rule.RuleModule} */ diff --git a/test/prefer-string-raw.js b/test/prefer-string-raw.js index b0c463b0f9..83450ad838 100644 --- a/test/prefer-string-raw.js +++ b/test/prefer-string-raw.js @@ -1,3 +1,4 @@ +/* eslint-disable no-template-curly-in-string */ import outdent from 'outdent'; import {getTester} from './utils/test.js'; @@ -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``, @@ -47,6 +47,68 @@ 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\` + `, + outdent` + a = String.raw + // Comment + \`ab\\c\` + `, + ], + 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\` + `, + outdent` + a + String.raw\`abc\` + `, + // ASI + outdent` + a + String.raw\`a\${b}\` + `, + outdent` + a = String.raw + // Comment + \`ab + c\` + `, + outdent` + a = String.raw /* comment */ \`ab + c\` + `, + ], +}); + test.typescript({ valid: [ outdent` diff --git a/test/snapshots/prefer-string-raw.js.md b/test/snapshots/prefer-string-raw.js.md index cd8cc4004b..003390a67c 100644 --- a/test/snapshots/prefer-string-raw.js.md +++ b/test/snapshots/prefer-string-raw.js.md @@ -129,3 +129,288 @@ 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␊ + | ^^^^^^^^^^ Using \`String.raw\` is unnecessary as the string does not contain any \`\\\`.␊ + 2 | c\`␊ + ` + +## 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␊ + | ^^^^^^^^^^ Using \`String.raw\` is unnecessary as the string does not contain any \`\\\`.␊ + 2 | bc␊ + 3 | de\`␊ + ` + +## 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\`␊ + | ^^^^^^^^^^ Using \`String.raw\` is unnecessary as the string does not contain any \`\\\`.␊ + 2 | a${foo}b␊ + 3 | ${bar}cd\`␊ + ` + +## invalid(9): a String.raw`abc` + +> Input + + `␊ + 1 | a␊ + 2 | String.raw\`abc\`␊ + ` + +> Output + + `␊ + 1 | a␊ + 2 | 'abc'␊ + ` + +> Error 1/1 + + `␊ + 1 | a␊ + > 2 | String.raw\`abc\`␊ + | ^^^^^^^^^^ Using \`String.raw\` is unnecessary as the string does not contain any \`\\\`.␊ + ` + +## invalid(10): a String.raw`a${b}` + +> Input + + `␊ + 1 | a␊ + 2 | String.raw\`a${b}\`␊ + ` + +> Output + + `␊ + 1 | a␊ + 2 | ;\`a${b}\`␊ + ` + +> Error 1/1 + + `␊ + 1 | a␊ + > 2 | String.raw\`a${b}\`␊ + | ^^^^^^^^^^ Using \`String.raw\` is unnecessary as the string does not contain any \`\\\`.␊ + ` + +## invalid(11): a = String.raw // Comment `ab c` + +> Input + + `␊ + 1 | a = String.raw␊ + 2 | // Comment␊ + 3 | \`ab␊ + 4 | c\`␊ + ` + +> Output + + `␊ + 1 | a = ␊ + 2 | // Comment␊ + 3 | \`ab␊ + 4 | c\`␊ + ` + +> Error 1/1 + + `␊ + > 1 | a = String.raw␊ + | ^^^^^^^^^^ Using \`String.raw\` is unnecessary as the string does not contain any \`\\\`.␊ + 2 | // Comment␊ + 3 | \`ab␊ + 4 | c\`␊ + ` + +## invalid(12): a = String.raw /* comment */ `ab c` + +> Input + + `␊ + 1 | a = String.raw /* comment */ \`ab␊ + 2 | c\`␊ + ` + +> Output + + `␊ + 1 | a = /* comment */ \`ab␊ + 2 | c\`␊ + ` + +> Error 1/1 + + `␊ + > 1 | a = String.raw /* comment */ \`ab␊ + | ^^^^^^^^^^ Using \`String.raw\` is unnecessary as the string does not contain any \`\\\`.␊ + 2 | c\`␊ + ` diff --git a/test/snapshots/prefer-string-raw.js.snap b/test/snapshots/prefer-string-raw.js.snap index 2afb8c0aa6..0efee92089 100644 Binary files a/test/snapshots/prefer-string-raw.js.snap and b/test/snapshots/prefer-string-raw.js.snap differ