diff --git a/docs/rules/prefer-string-raw.md b/docs/rules/prefer-string-raw.md index 2f8b608bb1..bc2d229258 100644 --- a/docs/rules/prefer-string-raw.md +++ b/docs/rules/prefer-string-raw.md @@ -19,6 +19,10 @@ const file = "C:\\windows\\style\\path\\to\\file.js"; const regexp = new RegExp('foo\\.bar'); ``` +```js +const file = `C:\\windows\\temp\\myapp-${process.pid}.log`; +``` + ## Pass ```js @@ -28,3 +32,7 @@ const file = String.raw`C:\windows\style\path\to\file.js`; ```js const regexp = new RegExp(String.raw`foo\.bar`); ``` + +```js +const file = String.raw`C:\windows\temp\myapp-${process.pid}.log`; +``` diff --git a/rules/prefer-string-raw.js b/rules/prefer-string-raw.js index 32ea732218..2dcd4e5456 100644 --- a/rules/prefer-string-raw.js +++ b/rules/prefer-string-raw.js @@ -1,5 +1,5 @@ import {isStringLiteral, isDirective} from './ast/index.js'; -import {fixSpaceAroundKeyword} from './fix/index.js'; +import {fixSpaceAroundKeyword, replaceTemplateElement} from './fix/index.js'; const MESSAGE_ID = 'prefer-string-raw'; const messages = { @@ -8,12 +8,8 @@ const messages = { const BACKSLASH = '\\'; -function unescapeBackslash(raw) { - const quote = raw.charAt(0); - - return raw - .slice(1, -1) - .replaceAll(new RegExp(String.raw`\\(?[\\${quote}])`, 'g'), '$'); +function unescapeBackslash(text, quote = '') { + return text.replaceAll(new RegExp(String.raw`\\(?[\\${quote}])`, 'g'), '$'); } /** @param {import('eslint').Rule.RuleContext} context */ @@ -50,7 +46,7 @@ const create = context => { return; } - const unescaped = unescapeBackslash(raw); + const unescaped = unescapeBackslash(raw.slice(1, -1), raw.charAt(0)); if (unescaped !== node.value) { return; } @@ -59,8 +55,36 @@ const create = context => { node, messageId: MESSAGE_ID, * fix(fixer) { - yield fixer.replaceText(node, `String.raw\`${unescaped}\``); yield * fixSpaceAroundKeyword(fixer, node, sourceCode); + yield fixer.replaceText(node, `String.raw\`${unescaped}\``); + }, + }; + }); + + context.on('TemplateLiteral', node => { + if ( + (node.parent.type === 'TaggedTemplateExpression' && node.parent.quasi === node) + || node.quasis.every(({value: {cooked, raw}}) => cooked === raw) + || node.quasis.some(({value: {cooked, raw}}) => cooked.at(-1) === BACKSLASH || unescapeBackslash(raw) !== cooked) + ) { + return; + } + + return { + node, + messageId: MESSAGE_ID, + * fix(fixer) { + yield * fixSpaceAroundKeyword(fixer, node, context.sourceCode); + yield fixer.insertTextBefore(node, 'String.raw'); + + for (const quasis of node.quasis) { + const {cooked, raw} = quasis.value; + if (cooked === raw) { + continue; + } + + yield replaceTemplateElement(fixer, quasis, cooked); + } }, }; }); diff --git a/rules/prefer-string-replace-all.js b/rules/prefer-string-replace-all.js index 226a879141..cfae07956f 100644 --- a/rules/prefer-string-replace-all.js +++ b/rules/prefer-string-replace-all.js @@ -57,11 +57,11 @@ function getPatternReplacement(node) { return String.raw`\t`; } - return `\\u{${codePoint.toString(16)}}`; + return String.raw`\u{${codePoint.toString(16)}}`; } if (kind === 'octal') { - return `\\u{${codePoint.toString(16)}}`; + return String.raw`\u{${codePoint.toString(16)}}`; } let character = raw; diff --git a/test/prefer-string-raw.js b/test/prefer-string-raw.js index b0c463b0f9..064b8215b2 100644 --- a/test/prefer-string-raw.js +++ b/test/prefer-string-raw.js @@ -1,8 +1,10 @@ +/* eslint-disable no-template-curly-in-string */ import outdent from 'outdent'; import {getTester} from './utils/test.js'; const {test} = getTester(import.meta); +// String literal to `String.raw` test.snapshot({ valid: [ String.raw`a = '\''`, @@ -18,7 +20,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 +48,55 @@ test.snapshot({ ], }); +// `TemplateLiteral` to `String.raw` +test.snapshot({ + valid: [ + // No backslash + 'a = `a`', + 'a = `${foo}`', + 'a = `a${100}b`', + + // Escaped characters other than backslash + 'a = `a\\t${foo.bar}b\\\\c`', // \t + 'a = `${foo}\\\\a${bar}\\``', // \` + 'a = `a\\${`', // \$ + 'a = `a$\\{`', // \{ + 'a = `${a}\\\'${b}\\\\`', // \' + 'a = `\\"a\\\\b`', // \" + + // Ending with backslash + 'a = `\\\\a${foo}b\\\\${foo}`', + + // Tagged template expression + 'a = String.raw`a\\\\b`', + + // Slash before newline (spread into multiple lines) + outdent` + a = \`\\\\a \\ + b\` + `, + ], + invalid: [ + 'a = `a\\\\b`', + 'function a() {return`a\\\\b${foo}cd`}', + 'a = {[`a\\\\b${foo}cd${foo.bar}e\\\\f`]: b}', + 'a = `a${foo}${foo.bar}b\\\\c`', + 'a = `a\\\\b${"c\\\\d"}e`', + outdent` + a = \`\\\\a + b\` + `, + outdent` + a = \`\\\\a\${foo} + b\${bar}c + d\\\\\\\\e\` + `, + 'a = `a\\\\b${ foo /* bar */}c\\\\d`', + 'a = `a\\\\b${ foo + bar }`', + 'a = `${ foo .bar }a\\\\b`', + ], +}); + test.typescript({ valid: [ outdent` diff --git a/test/snapshots/prefer-string-raw.js.md b/test/snapshots/prefer-string-raw.js.md index cd8cc4004b..a77e25b02c 100644 --- a/test/snapshots/prefer-string-raw.js.md +++ b/test/snapshots/prefer-string-raw.js.md @@ -129,3 +129,232 @@ Generated by [AVA](https://avajs.dev). > 1 | a = "a\\\\b\\""␊ | ^^^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊ ` + +## invalid(1): a = `a\\b` + +> Input + + `␊ + 1 | a = \`a\\\\b\`␊ + ` + +> Output + + `␊ + 1 | a = String.raw\`a\\b\`␊ + ` + +> Error 1/1 + + `␊ + > 1 | a = \`a\\\\b\`␊ + | ^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊ + ` + +## invalid(2): function a() {return`a\\b${foo}cd`} + +> Input + + `␊ + 1 | function a() {return\`a\\\\b${foo}cd\`}␊ + ` + +> Output + + `␊ + 1 | function a() {return String.raw\`a\\b${foo}cd\`}␊ + ` + +> Error 1/1 + + `␊ + > 1 | function a() {return\`a\\\\b${foo}cd\`}␊ + | ^^^^^^^^^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊ + ` + +## invalid(3): a = {[`a\\b${foo}cd${foo.bar}e\\f`]: b} + +> Input + + `␊ + 1 | a = {[\`a\\\\b${foo}cd${foo.bar}e\\\\f\`]: b}␊ + ` + +> Output + + `␊ + 1 | a = {[String.raw\`a\\b${foo}cd${foo.bar}e\\f\`]: b}␊ + ` + +> Error 1/1 + + `␊ + > 1 | a = {[\`a\\\\b${foo}cd${foo.bar}e\\\\f\`]: b}␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊ + ` + +## invalid(4): a = `a${foo}${foo.bar}b\\c` + +> Input + + `␊ + 1 | a = \`a${foo}${foo.bar}b\\\\c\`␊ + ` + +> Output + + `␊ + 1 | a = String.raw\`a${foo}${foo.bar}b\\c\`␊ + ` + +> Error 1/1 + + `␊ + > 1 | a = \`a${foo}${foo.bar}b\\\\c\`␊ + | ^^^^^^^^^^^^^^^^^^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊ + ` + +## invalid(5): a = `a\\b${"c\\d"}e` + +> Input + + `␊ + 1 | a = \`a\\\\b${"c\\\\d"}e\`␊ + ` + +> Output + + `␊ + 1 | a = String.raw\`a\\b${String.raw\`c\\d\`}e\`␊ + ` + +> Error 1/2 + + `␊ + > 1 | a = \`a\\\\b${"c\\\\d"}e\`␊ + | ^^^^^^^^^^^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊ + ` + +> Error 2/2 + + `␊ + > 1 | a = \`a\\\\b${"c\\\\d"}e\`␊ + | ^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊ + ` + +## invalid(6): a = `\\a b` + +> Input + + `␊ + 1 | a = \`\\\\a␊ + 2 | b\`␊ + ` + +> Output + + `␊ + 1 | a = String.raw\`\\a␊ + 2 | b\`␊ + ` + +> Error 1/1 + + `␊ + > 1 | a = \`\\\\a␊ + | ^^^^␊ + > 2 | b\`␊ + | ^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊ + ` + +## invalid(7): a = `\\a${foo} b${bar}c d\\\\e` + +> Input + + `␊ + 1 | a = \`\\\\a${foo}␊ + 2 | b${bar}c␊ + 3 | d\\\\\\\\e\`␊ + ` + +> Output + + `␊ + 1 | a = String.raw\`\\a${foo}␊ + 2 | b${bar}c␊ + 3 | d\\\\e\`␊ + ` + +> Error 1/1 + + `␊ + > 1 | a = \`\\\\a${foo}␊ + | ^^^^^^^^^^␊ + > 2 | b${bar}c␊ + | ^^^^^^^^␊ + > 3 | d\\\\\\\\e\`␊ + | ^^^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊ + ` + +## invalid(8): a = `a\\b${ foo /* bar */}c\\d` + +> Input + + `␊ + 1 | a = \`a\\\\b${ foo /* bar */}c\\\\d\`␊ + ` + +> Output + + `␊ + 1 | a = String.raw\`a\\b${ foo /* bar */}c\\d\`␊ + ` + +> Error 1/1 + + `␊ + > 1 | a = \`a\\\\b${ foo /* bar */}c\\\\d\`␊ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊ + ` + +## invalid(9): a = `a\\b${ foo + bar }` + +> Input + + `␊ + 1 | a = \`a\\\\b${ foo + bar }\`␊ + ` + +> Output + + `␊ + 1 | a = String.raw\`a\\b${ foo + bar }\`␊ + ` + +> Error 1/1 + + `␊ + > 1 | a = \`a\\\\b${ foo + bar }\`␊ + | ^^^^^^^^^^^^^^^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊ + ` + +## invalid(10): a = `${ foo .bar }a\\b` + +> Input + + `␊ + 1 | a = \`${ foo .bar }a\\\\b\`␊ + ` + +> Output + + `␊ + 1 | a = String.raw\`${ foo .bar }a\\b\`␊ + ` + +> Error 1/1 + + `␊ + > 1 | a = \`${ foo .bar }a\\\\b\`␊ + | ^^^^^^^^^^^^^^^^^^^ \`String.raw\` should be used to avoid escaping \`\\\`.␊ + ` diff --git a/test/snapshots/prefer-string-raw.js.snap b/test/snapshots/prefer-string-raw.js.snap index 2afb8c0aa6..f2a57deab9 100644 Binary files a/test/snapshots/prefer-string-raw.js.snap and b/test/snapshots/prefer-string-raw.js.snap differ