diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts b/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts index 44e10c5e33c8..3f6e7f69ea8b 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts @@ -6,13 +6,23 @@ import { createRule, getConstrainedTypeAtLocation, getParserServices, - getStaticStringValue, isTypeFlagSet, isUndefinedIdentifier, } from '../util'; type MessageId = 'noUnnecessaryTemplateExpression'; +const evenNumOfBackslashesRegExp = /(?({ name: 'no-unnecessary-template-expression', meta: { @@ -53,11 +63,15 @@ export default createRule<[], MessageId>({ return isString(type); } - function isLiteral(expression: TSESTree.Expression): boolean { + function isLiteral( + expression: TSESTree.Expression, + ): expression is TSESTree.Literal { return expression.type === AST_NODE_TYPES.Literal; } - function isTemplateLiteral(expression: TSESTree.Expression): boolean { + function isTemplateLiteral( + expression: TSESTree.Expression, + ): expression is TSESTree.TemplateLiteral { return expression.type === AST_NODE_TYPES.TemplateLiteral; } @@ -113,62 +127,150 @@ export default createRule<[], MessageId>({ return; } - const fixableExpressions = node.expressions.filter( - expression => - isLiteral(expression) || - isTemplateLiteral(expression) || - isUndefinedIdentifier(expression) || - isInfinityIdentifier(expression) || - isNaNIdentifier(expression), - ); + const fixableExpressions = node.expressions + .filter( + expression => + isLiteral(expression) || + isTemplateLiteral(expression) || + isUndefinedIdentifier(expression) || + isInfinityIdentifier(expression) || + isNaNIdentifier(expression), + ) + .reverse(); + + let nextCharacterIsOpeningCurlyBrace = false; + + for (const expression of fixableExpressions) { + const fixers: ((fixer: TSESLint.RuleFixer) => TSESLint.RuleFix[])[] = + []; + const index = node.expressions.indexOf(expression); + const prevQuasi = node.quasis[index]; + const nextQuasi = node.quasis[index + 1]; + + if (nextQuasi.value.raw.length !== 0) { + nextCharacterIsOpeningCurlyBrace = + nextQuasi.value.raw.startsWith('{'); + } + + if (isLiteral(expression)) { + let escapedValue = ( + typeof expression.value === 'string' + ? // The value is already a string, so we're removing quotes: + // "'va`lue'" -> "va`lue" + expression.raw.slice(1, -1) + : // The value may be one of number | bigint | boolean | RegExp | null. + // In regular expressions, we escape every backslash + String(expression.value).replace(/\\/g, '\\\\') + ) + // The string or RegExp may contain ` or ${. + // We want both of these to be escaped in the final template expression. + // + // A pair of backslashes means "escaped backslash", so backslashes + // from this pair won't escape ` or ${. Therefore, to escape these + // sequences in the resulting template expression, we need to escape + // all sequences that are preceded by an even number of backslashes. + // + // This RegExp does the following transformations: + // \` -> \` + // \\` -> \\\` + // \${ -> \${ + // \\${ -> \\\${ + .replace( + new RegExp( + String(evenNumOfBackslashesRegExp.source) + '(`|\\${)', + 'g', + ), + '\\$1', + ); + + // `...${'...$'}{...` + // ^^^^ + if ( + nextCharacterIsOpeningCurlyBrace && + endsWithUnescapedDollarSign(escapedValue) + ) { + escapedValue = escapedValue.replaceAll(/\$$/g, '\\$'); + } + + if (escapedValue.length !== 0) { + nextCharacterIsOpeningCurlyBrace = escapedValue.startsWith('{'); + } + + fixers.push(fixer => [fixer.replaceText(expression, escapedValue)]); + } else if (isTemplateLiteral(expression)) { + // Since we iterate from the last expression to the first, + // a subsequent expression can tell the current expression + // that it starts with {. + // + // `... ${`... $`}${'{...'} ...` + // ^ ^ subsequent expression starts with { + // current expression ends with a dollar sign, + // so '$' + '{' === '${' (bad news for us). + // Let's escape the dollar sign at the end. + if ( + nextCharacterIsOpeningCurlyBrace && + endsWithUnescapedDollarSign( + expression.quasis[expression.quasis.length - 1].value.raw, + ) + ) { + fixers.push(fixer => [ + fixer.replaceTextRange( + [expression.range[1] - 2, expression.range[1] - 2], + '\\', + ), + ]); + } + if ( + expression.quasis.length === 1 && + expression.quasis[0].value.raw.length !== 0 + ) { + nextCharacterIsOpeningCurlyBrace = + expression.quasis[0].value.raw.startsWith('{'); + } + + // Remove the beginning and trailing backtick characters. + fixers.push(fixer => [ + fixer.removeRange([expression.range[0], expression.range[0] + 1]), + fixer.removeRange([expression.range[1] - 1, expression.range[1]]), + ]); + } else { + nextCharacterIsOpeningCurlyBrace = false; + } + + // `... $${'{...'} ...` + // ^^^^^ + if ( + nextCharacterIsOpeningCurlyBrace && + endsWithUnescapedDollarSign(prevQuasi.value.raw) + ) { + fixers.push(fixer => [ + fixer.replaceTextRange( + [prevQuasi.range[1] - 3, prevQuasi.range[1] - 2], + '\\$', + ), + ]); + } - fixableExpressions.forEach(expression => { context.report({ node: expression, messageId: 'noUnnecessaryTemplateExpression', fix(fixer): TSESLint.RuleFix[] { - const index = node.expressions.indexOf(expression); - const prevQuasi = node.quasis[index]; - const nextQuasi = node.quasis[index + 1]; - - // Remove the quasis' parts that are related to the current expression. - const fixes = [ + return [ + // Remove the quasis' parts that are related to the current expression. fixer.removeRange([ prevQuasi.range[1] - 2, expression.range[0], ]), - fixer.removeRange([ expression.range[1], nextQuasi.range[0] + 1, ]), - ]; - const stringValue = getStaticStringValue(expression); - - if (stringValue != null) { - const escapedValue = stringValue.replace(/([`$\\])/g, '\\$1'); - - fixes.push(fixer.replaceText(expression, escapedValue)); - } else if (isTemplateLiteral(expression)) { - // Note that some template literals get handled in the previous branch too. - // Remove the beginning and trailing backtick characters. - fixes.push( - fixer.removeRange([ - expression.range[0], - expression.range[0] + 1, - ]), - fixer.removeRange([ - expression.range[1] - 1, - expression.range[1], - ]), - ); - } - - return fixes; + ...fixers.flatMap(cb => cb(fixer)), + ]; }, }); - }); + } }, }; }, diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts index fff88fdc3bf3..e7137ada9073 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts @@ -1,3 +1,4 @@ +import type { InvalidTestCase } from '@typescript-eslint/rule-tester'; import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; import rule from '../../src/rules/no-unnecessary-template-expression'; @@ -13,6 +14,888 @@ const ruleTester = new RuleTester({ }, }); +const invalidCases: readonly InvalidTestCase< + 'noUnnecessaryTemplateExpression', + [] +>[] = [ + { + code: '`${1}`;', + output: '`1`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 5, + }, + ], + }, + { + code: '`${1n}`;', + output: '`1`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 6, + }, + ], + }, + { + code: '`${0o25}`;', + output: '`21`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 8, + }, + ], + }, + { + code: '`${0b1010} ${0b1111}`;', + output: '`10 15`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 10, + }, + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 14, + endColumn: 20, + }, + ], + }, + { + code: '`${0x25}`;', + output: '`37`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 8, + }, + ], + }, + { + code: '`${/a/}`;', + output: '`/a/`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 7, + }, + ], + }, + { + code: '`${/a/gim}`;', + output: '`/a/gim`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 10, + }, + ], + }, + + { + code: noFormat`\`\${ 1 }\`;`, + output: '`1`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: noFormat`\`\${ 'a' }\`;`, + output: `'a';`, + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: noFormat`\`\${ "a" }\`;`, + output: `"a";`, + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: noFormat`\`\${ 'a' + 'b' }\`;`, + output: `'a' + 'b';`, + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: '`${true}`;', + output: '`true`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 8, + }, + ], + }, + + { + code: noFormat`\`\${ true }\`;`, + output: '`true`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: '`${null}`;', + output: '`null`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 8, + }, + ], + }, + + { + code: noFormat`\`\${ null }\`;`, + output: '`null`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: '`${undefined}`;', + output: '`undefined`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 13, + }, + ], + }, + + { + code: noFormat`\`\${ undefined }\`;`, + output: '`undefined`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: '`${Infinity}`;', + output: '`Infinity`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 12, + }, + ], + }, + + { + code: '`${NaN}`;', + output: '`NaN`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 7, + }, + ], + }, + + { + code: "`${'a'} ${'b'}`;", + output: '`a b`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 7, + }, + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 11, + endColumn: 14, + }, + ], + }, + + { + code: noFormat`\`\${ 'a' } \${ 'b' }\`;`, + output: '`a b`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: "`use${'less'}`;", + output: '`useless`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + }, + ], + }, + + { + code: '`use${`less`}`;', + output: '`useless`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + }, + ], + }, + { + code: noFormat` +\`u\${ + // hopefully this comment is not needed. + 'se' + +}\${ + \`le\${ \`ss\` }\` +}\`; + `, + output: [ + ` +\`use\${ + \`less\` +}\`; + `, + ` +\`useless\`; + `, + ], + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 4, + }, + { + messageId: 'noUnnecessaryTemplateExpression', + line: 7, + column: 3, + endLine: 7, + }, + { + messageId: 'noUnnecessaryTemplateExpression', + line: 7, + column: 10, + endLine: 7, + }, + ], + }, + { + code: noFormat` +\`use\${ + \`less\` +}\`; + `, + output: ` +\`useless\`; + `, + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 3, + column: 3, + endColumn: 9, + }, + ], + }, + + { + code: "`${'1 + 1 ='} ${2}`;", + output: '`1 + 1 = 2`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 13, + }, + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 17, + endColumn: 18, + }, + ], + }, + + { + code: "`${'a'} ${true}`;", + output: '`a true`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 7, + }, + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 11, + endColumn: 15, + }, + ], + }, + + { + code: "`${String(Symbol.for('test'))}`;", + output: "String(Symbol.for('test'));", + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + line: 1, + column: 4, + endColumn: 30, + }, + ], + }, + + { + code: "`${'`'}`;", + output: "'`';", + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: "`back${'`'}tick`;", + output: '`back\\`tick`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: "`dollar${'${`this is test`}'}sign`;", + output: '`dollar\\${\\`this is test\\`}sign`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: '`complex${\'`${"`${test}`"}`\'}case`;', + output: '`complex\\`\\${"\\`\\${test}\\`"}\\`case`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: "`some ${'\\\\${test}'} string`;", + output: '`some \\\\\\${test} string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: "`some ${'\\\\`'} string`;", + output: '`some \\\\\\` string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + + { + code: '`some ${/`/} string`;', + output: '`some /\\`/ string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + { + code: '`some ${/\\`/} string`;', + output: '`some /\\\\\\`/ string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + { + code: '`some ${/\\\\`/} string`;', + output: '`some /\\\\\\\\\\`/ string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + { + code: '`some ${/\\\\\\`/} string`;', + output: '`some /\\\\\\\\\\\\\\`/ string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + { + code: '`some ${/${}/} string`;', + output: '`some /\\${}/ string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + { + code: '`some ${/$ {}/} string`;', + output: '`some /$ {}/ string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + { + code: '`some ${/\\\\/} string`;', + output: '`some /\\\\\\\\/ string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + { + code: '`some ${/\\\\\\b/} string`;', + output: '`some /\\\\\\\\\\\\b/ string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + { + code: '`some ${/\\\\\\\\/} string`;', + output: '`some /\\\\\\\\\\\\\\\\/ string`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + }, + { + code: "` ${''} `;", + output: '` `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: noFormat`\` \${""} \`;`, + output: '` `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: '` ${``} `;', + output: '` `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: noFormat`\` \${'\\\`'} \`;`, + output: '` \\` `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'\\\\`'} `;", + output: '` \\\\\\` `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'$'}{} `;", + output: '` \\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: noFormat`\` \${'\\$'}{} \`;`, + output: '` \\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'\\\\$'}{} `;", + output: '` \\\\\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'\\\\$ '}{} `;", + output: '` \\\\$ {} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: noFormat`\` \${'\\\\\\$'}{} \`;`, + output: '` \\\\\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` \\\\${'\\\\$'}{} `;", + output: '` \\\\\\\\\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` $${'{$'}{} `;", + output: '` \\${\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` $${'${$'}{} `;", + output: '` $\\${\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'foo$'}{} `;", + output: '` foo\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: '` ${`$`} `;', + output: '` $ `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: '` ${`$`}{} `;', + output: '` \\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: '` ${`$`} {} `;', + output: '` $ {} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: '` ${`$`}${undefined}{} `;', + output: ['` $${undefined}{} `;', '` $undefined{} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: '` ${`foo$`}{} `;', + output: '` foo\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'$'}${''}{} `;", + output: ["` \\$${''}{} `;", '` \\${} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` ${'$'}${``}{} `;", + output: ['` \\$${``}{} `;', '` \\${} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` ${'foo$'}${''}${``}{} `;", + output: ["` foo\\$${''}{} `;", '` foo\\${} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` $${'{}'} `;", + output: '` \\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` $${undefined}${'{}'} `;", + output: ["` $undefined${'{}'} `;", '` $undefined{} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` $${''}${undefined}${'{}'} `;", + output: ['` $${undefined}{} `;', '` $undefined{} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` \\$${'{}'} `;", + output: '` \\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` $${'foo'}${'{'} `;", + output: ["` $foo${'{'} `;", '` $foo{ `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` $${'{ foo'}${'{'} `;", + output: ["` \\${ foo${'{'} `;", '` \\${ foo{ `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` \\\\$${'{}'} `;", + output: '` \\\\\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` \\\\\\$${'{}'} `;", + output: '` \\\\\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` foo$${'{}'} `;", + output: '` foo\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` $${''}${'{}'} `;", + output: ["` \\$${'{}'} `;", '` \\${} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` $${''} `;", + output: '` $ `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: '` $${`{}`} `;', + output: '` \\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: '` $${``}${`{}`} `;', + output: ['` \\$${`{}`} `;', '` \\${} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: '` $${``}${`foo{}`} `;', + output: ['` $${`foo{}`} `;', '` $foo{} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` $${`${''}${`${``}`}`}${`{a}`} `;", + output: [ + "` \\$${''}${`${``}`}${`{a}`} `;", + '` \\$${``}{a} `;', + '` \\${a} `;', + ], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` $${''}${`{}`} `;", + output: ['` \\$${`{}`} `;', '` \\${} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` $${``}${'{}'} `;", + output: ["` \\$${'{}'} `;", '` \\${} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` $${''}${``}${'{}'} `;", + output: ['` \\$${``}{} `;', '` \\${} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` ${'$'} `;", + output: '` $ `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'$'}${'{}'} `;", + output: ["` \\$${'{}'} `;", '` \\${} `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: "` ${'$'}${''}${'{'} `;", + output: ["` \\$${''}{ `;", '` \\${ `;'], + errors: [ + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + { messageId: 'noUnnecessaryTemplateExpression' }, + ], + }, + { + code: '` ${`\n\\$`}{} `;', + output: '` \n\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: '` ${`\n\\\\$`}{} `;', + output: '` \n\\\\\\${} `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + + { + code: "`${'\\u00E5'}`;", + output: "'\\u00E5';", + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "`${'\\n'}`;", + output: "'\\n';", + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'\\u00E5'} `;", + output: '` \\u00E5 `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'\\n'} `;", + output: '` \\n `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: noFormat`\` \${"\\n"} \`;`, + output: '` \\n `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: '` ${`\\n`} `;', + output: '` \\n `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: noFormat`\` \${ 'A\\u0307\\u0323' } \`;`, + output: '` A\\u0307\\u0323 `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ'} `;", + output: '` ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, + { + code: "` ${'\\ud83d\\udc68'} `;", + output: '` \\ud83d\\udc68 `;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + }, +]; + +describe('fixer should not change runtime value', () => { + for (const { code, output } of invalidCases) { + if (!output) { + continue; + } + + test(code, () => { + expect(eval(code)).toEqual( + eval(Array.isArray(output) ? output.at(-1)! : output), + ); + }); + } +}); + ruleTester.run('no-unnecessary-template-expression', rule, { valid: [ "const string = 'a';", @@ -138,210 +1021,27 @@ ruleTester.run('no-unnecessary-template-expression', rule, { ], invalid: [ + ...invalidCases, { - code: '`${1}`;', - output: '`1`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 5, - }, - ], - }, - { - code: '`${1n}`;', - output: '`1`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 6, - }, - ], - }, - { - code: '`${/a/}`;', - output: '`/a/`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 7, - }, - ], - }, - - { - code: noFormat`\`\${ 1 }\`;`, - output: '`1`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: noFormat`\`\${ 'a' }\`;`, - output: `'a';`, - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: noFormat`\`\${ "a" }\`;`, - output: `"a";`, - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: noFormat`\`\${ 'a' + 'b' }\`;`, - output: `'a' + 'b';`, - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: '`${true}`;', - output: '`true`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 8, - }, - ], - }, - - { - code: noFormat`\`\${ true }\`;`, - output: '`true`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: '`${null}`;', - output: '`null`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 8, - }, - ], - }, - - { - code: noFormat`\`\${ null }\`;`, - output: '`null`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: '`${undefined}`;', - output: '`undefined`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 13, - }, - ], - }, - - { - code: noFormat`\`\${ undefined }\`;`, - output: '`undefined`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: '`${Infinity}`;', - output: '`Infinity`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 12, - }, - ], - }, - - { - code: '`${NaN}`;', - output: '`NaN`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 7, - }, - ], - }, - - { - code: "`${'a'} ${'b'}`;", - output: '`a b`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 7, - }, - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 11, - endColumn: 14, - }, - ], - }, - - { - code: noFormat`\`\${ 'a' } \${ 'b' }\`;`, - output: '`a b`;', + code: ` + function func(arg: T) { + \`\${arg}\`; + } + `, + output: ` + function func(arg: T) { + arg; + } + `, errors: [ { messageId: 'noUnnecessaryTemplateExpression', - }, - { - messageId: 'noUnnecessaryTemplateExpression', + line: 3, + column: 14, + endColumn: 17, }, ], }, - { code: ` declare const b: 'b'; @@ -360,29 +1060,6 @@ ruleTester.run('no-unnecessary-template-expression', rule, { }, ], }, - - { - code: "`use${'less'}`;", - output: '`useless`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - }, - ], - }, - - { - code: '`use${`less`}`;', - output: '`useless`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - }, - ], - }, - { code: ` declare const nested: string, interpolation: string; @@ -398,103 +1075,21 @@ declare const nested: string, interpolation: string; }, ], }, - - { - code: noFormat` -\`u\${ - // hopefully this comment is not needed. - 'se' - -}\${ - \`le\${ \`ss\` }\` -}\`; - `, - output: [ - ` -\`use\${ - \`less\` -}\`; - `, - ` -\`useless\`; - `, - ], - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 4, - }, - { - messageId: 'noUnnecessaryTemplateExpression', - line: 7, - column: 3, - endLine: 7, - }, - { - messageId: 'noUnnecessaryTemplateExpression', - line: 7, - column: 10, - endLine: 7, - }, - ], - }, { code: noFormat` -\`use\${ - \`less\` -}\`; + declare const string: 'a'; + \`\${ string }\`; `, output: ` -\`useless\`; + declare const string: 'a'; + string; `, errors: [ { messageId: 'noUnnecessaryTemplateExpression', - line: 3, - column: 3, - endColumn: 9, }, ], }, - - { - code: "`${'1 + 1 ='} ${2}`;", - output: '`1 + 1 = 2`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 13, - }, - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 17, - endColumn: 18, - }, - ], - }, - - { - code: "`${'a'} ${true}`;", - output: '`a true`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 7, - }, - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 11, - endColumn: 15, - }, - ], - }, - { code: ` declare const string: 'a'; @@ -513,36 +1108,6 @@ declare const nested: string, interpolation: string; }, ], }, - - { - code: noFormat` - declare const string: 'a'; - \`\${ string }\`; - `, - output: ` - declare const string: 'a'; - string; - `, - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: "`${String(Symbol.for('test'))}`;", - output: "String(Symbol.for('test'));", - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 1, - column: 4, - endColumn: 30, - }, - ], - }, - { code: ` declare const intersection: string & { _brand: 'test-brand' }; @@ -561,86 +1126,5 @@ declare const nested: string, interpolation: string; }, ], }, - - { - code: ` - function func(arg: T) { - \`\${arg}\`; - } - `, - output: ` - function func(arg: T) { - arg; - } - `, - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - line: 3, - column: 14, - endColumn: 17, - }, - ], - }, - - { - code: "`${'`'}`;", - output: "'`';", - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: "`back${'`'}tick`;", - output: '`back\\`tick`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: "`dollar${'${`this is test`}'}sign`;", - output: '`dollar\\${\\`this is test\\`}sign`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: '`complex${\'`${"`${test}`"}`\'}case`;', - output: '`complex\\`\\${"\\`\\${test}\\`"}\\`case`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: "`some ${'\\\\${test}'} string`;", - output: '`some \\\\\\${test} string`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, - - { - code: "`some ${'\\\\`'} string`;", - output: '`some \\\\\\` string`;', - errors: [ - { - messageId: 'noUnnecessaryTemplateExpression', - }, - ], - }, ], });