diff --git a/.cspell.json b/.cspell.json index 080101b25f08..1bf064b538c6 100644 --- a/.cspell.json +++ b/.cspell.json @@ -106,6 +106,7 @@ "preact", "Premade", "prettier's", + "quasis", "Quickstart", "recurse", "redeclaration", diff --git a/packages/eslint-plugin/src/rules/no-useless-template-literals.ts b/packages/eslint-plugin/src/rules/no-useless-template-literals.ts index 48c714edb90a..d5a8a602b6b4 100644 --- a/packages/eslint-plugin/src/rules/no-useless-template-literals.ts +++ b/packages/eslint-plugin/src/rules/no-useless-template-literals.ts @@ -1,4 +1,4 @@ -import type { TSESTree } from '@typescript-eslint/utils'; +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as ts from 'typescript'; @@ -15,7 +15,8 @@ type MessageId = 'noUselessTemplateLiteral'; export default createRule<[], MessageId>({ name: 'no-useless-template-literals', meta: { - type: 'problem', + fixable: 'code', + type: 'suggestion', docs: { description: 'Disallow unnecessary template literals', recommended: 'strict', @@ -68,6 +69,22 @@ export default createRule<[], MessageId>({ context.report({ node: node.expressions[0], messageId: 'noUselessTemplateLiteral', + fix(fixer): TSESLint.RuleFix[] { + const [prevQuasi, nextQuasi] = node.quasis; + + // Remove the quasis and backticks. + return [ + fixer.removeRange([ + prevQuasi.range[1] - 3, + node.expressions[0].range[0], + ]), + + fixer.removeRange([ + node.expressions[0].range[1], + nextQuasi.range[0] + 2, + ]), + ]; + }, }); return; @@ -83,6 +100,40 @@ export default createRule<[], MessageId>({ context.report({ node: expression, messageId: 'noUselessTemplateLiteral', + 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 = [ + fixer.removeRange([ + prevQuasi.range[1] - 2, + expression.range[0], + ]), + + fixer.removeRange([ + expression.range[1], + nextQuasi.range[0] + 1, + ]), + ]; + + // Remove quotes for string literals (i.e. `'a'` will become `a`). + const isStringLiteral = + isUnderlyingTypeString(expression) && + expression.type === AST_NODE_TYPES.Literal; + + if (isStringLiteral) { + const escapedValue = expression.value.replace( + /([`$\\])/g, + '\\$1', + ); + + fixes.push(fixer.replaceText(expression, escapedValue)); + } + + return fixes; + }, }); }); }, diff --git a/packages/eslint-plugin/tests/rules/no-useless-template-literals.test.ts b/packages/eslint-plugin/tests/rules/no-useless-template-literals.test.ts index 54ac89c2c797..674b4ccefe77 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-template-literals.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-template-literals.test.ts @@ -137,6 +137,7 @@ ruleTester.run('no-useless-template-literals', rule, { invalid: [ { code: '`${1}`;', + output: '`1`;', errors: [ { messageId: 'noUselessTemplateLiteral', @@ -146,19 +147,50 @@ ruleTester.run('no-useless-template-literals', rule, { }, ], }, + { - code: '`${1n}`;', + code: noFormat`\`\${ 1 }\`;`, + output: '`1`;', + errors: [ + { + messageId: 'noUselessTemplateLiteral', + }, + ], + }, + + { + code: noFormat`\`\${ 'a' }\`;`, + output: `'a';`, errors: [ { messageId: 'noUselessTemplateLiteral', - line: 1, - column: 4, - endColumn: 6, }, ], }, + + { + code: noFormat`\`\${ "a" }\`;`, + output: `"a";`, + errors: [ + { + messageId: 'noUselessTemplateLiteral', + }, + ], + }, + + { + code: noFormat`\`\${ 'a' + 'b' }\`;`, + output: `'a' + 'b';`, + errors: [ + { + messageId: 'noUselessTemplateLiteral', + }, + ], + }, + { code: '`${true}`;', + output: '`true`;', errors: [ { messageId: 'noUselessTemplateLiteral', @@ -168,8 +200,20 @@ ruleTester.run('no-useless-template-literals', rule, { }, ], }, + + { + code: noFormat`\`\${ true }\`;`, + output: '`true`;', + errors: [ + { + messageId: 'noUselessTemplateLiteral', + }, + ], + }, + { code: '`${null}`;', + output: '`null`;', errors: [ { messageId: 'noUselessTemplateLiteral', @@ -179,8 +223,20 @@ ruleTester.run('no-useless-template-literals', rule, { }, ], }, + + { + code: noFormat`\`\${ null }\`;`, + output: '`null`;', + errors: [ + { + messageId: 'noUselessTemplateLiteral', + }, + ], + }, + { code: '`${undefined}`;', + output: '`undefined`;', errors: [ { messageId: 'noUselessTemplateLiteral', @@ -190,8 +246,20 @@ ruleTester.run('no-useless-template-literals', rule, { }, ], }, + { - code: "`${'a'}${'b'}`;", + code: noFormat`\`\${ undefined }\`;`, + output: '`undefined`;', + errors: [ + { + messageId: 'noUselessTemplateLiteral', + }, + ], + }, + + { + code: "`${'a'} ${'b'}`;", + output: '`a b`;', errors: [ { messageId: 'noUselessTemplateLiteral', @@ -202,8 +270,21 @@ ruleTester.run('no-useless-template-literals', rule, { { messageId: 'noUselessTemplateLiteral', line: 1, - column: 10, - endColumn: 13, + column: 11, + endColumn: 14, + }, + ], + }, + + { + code: noFormat`\`\${ 'a' } \${ 'b' }\`;`, + output: '`a b`;', + errors: [ + { + messageId: 'noUselessTemplateLiteral', + }, + { + messageId: 'noUselessTemplateLiteral', }, ], }, @@ -213,6 +294,10 @@ ruleTester.run('no-useless-template-literals', rule, { declare const b: 'b'; \`a\${b}\${'c'}\`; `, + output: ` + declare const b: 'b'; + \`a\${b}c\`; + `, errors: [ { messageId: 'noUselessTemplateLiteral', @@ -225,6 +310,7 @@ ruleTester.run('no-useless-template-literals', rule, { { code: "`a${'b'}`;", + output: '`ab`;', errors: [ { messageId: 'noUselessTemplateLiteral', @@ -236,13 +322,14 @@ ruleTester.run('no-useless-template-literals', rule, { }, { - code: "`${'1 + 1 = '}${2}`;", + code: "`${'1 + 1 ='} ${2}`;", + output: '`1 + 1 = 2`;', errors: [ { messageId: 'noUselessTemplateLiteral', line: 1, column: 4, - endColumn: 14, + endColumn: 13, }, { messageId: 'noUselessTemplateLiteral', @@ -254,7 +341,8 @@ ruleTester.run('no-useless-template-literals', rule, { }, { - code: "`${'a'}${true}`;", + code: "`${'a'} ${true}`;", + output: '`a true`;', errors: [ { messageId: 'noUselessTemplateLiteral', @@ -265,8 +353,8 @@ ruleTester.run('no-useless-template-literals', rule, { { messageId: 'noUselessTemplateLiteral', line: 1, - column: 10, - endColumn: 14, + column: 11, + endColumn: 15, }, ], }, @@ -276,6 +364,10 @@ ruleTester.run('no-useless-template-literals', rule, { declare const string: 'a'; \`\${string}\`; `, + output: ` + declare const string: 'a'; + string; + `, errors: [ { messageId: 'noUselessTemplateLiteral', @@ -286,8 +378,25 @@ ruleTester.run('no-useless-template-literals', rule, { ], }, + { + code: noFormat` + declare const string: 'a'; + \`\${ string }\`; + `, + output: ` + declare const string: 'a'; + string; + `, + errors: [ + { + messageId: 'noUselessTemplateLiteral', + }, + ], + }, + { code: "`${String(Symbol.for('test'))}`;", + output: "String(Symbol.for('test'));", errors: [ { messageId: 'noUselessTemplateLiteral', @@ -303,6 +412,10 @@ ruleTester.run('no-useless-template-literals', rule, { declare const intersection: string & { _brand: 'test-brand' }; \`\${intersection}\`; `, + output: ` + declare const intersection: string & { _brand: 'test-brand' }; + intersection; + `, errors: [ { messageId: 'noUselessTemplateLiteral', @@ -319,6 +432,11 @@ ruleTester.run('no-useless-template-literals', rule, { \`\${arg}\`; } `, + output: ` + function func(arg: T) { + arg; + } + `, errors: [ { messageId: 'noUselessTemplateLiteral', @@ -328,5 +446,65 @@ ruleTester.run('no-useless-template-literals', rule, { }, ], }, + + { + code: "`${'`'}`;", + output: "'`';", + errors: [ + { + messageId: 'noUselessTemplateLiteral', + }, + ], + }, + + { + code: "`back${'`'}tick`;", + output: '`back\\`tick`;', + errors: [ + { + messageId: 'noUselessTemplateLiteral', + }, + ], + }, + + { + code: "`dollar${'${`this is test`}'}sign`;", + output: '`dollar\\${\\`this is test\\`}sign`;', + errors: [ + { + messageId: 'noUselessTemplateLiteral', + }, + ], + }, + + { + code: '`complex${\'`${"`${test}`"}`\'}case`;', + output: '`complex\\`\\${"\\`\\${test}\\`"}\\`case`;', + errors: [ + { + messageId: 'noUselessTemplateLiteral', + }, + ], + }, + + { + code: "`some ${'\\\\${test}'} string`;", + output: '`some \\\\\\${test} string`;', + errors: [ + { + messageId: 'noUselessTemplateLiteral', + }, + ], + }, + + { + code: "`some ${'\\\\`'} string`;", + output: '`some \\\\\\` string`;', + errors: [ + { + messageId: 'noUselessTemplateLiteral', + }, + ], + }, ], });