diff --git a/packages/eslint-plugin/docs/rules/no-throw-literal.mdx b/packages/eslint-plugin/docs/rules/no-throw-literal.mdx index d5919b3bdc63..9bd9481ffa15 100644 --- a/packages/eslint-plugin/docs/rules/no-throw-literal.mdx +++ b/packages/eslint-plugin/docs/rules/no-throw-literal.mdx @@ -16,10 +16,10 @@ This rule restricts what can be thrown as an exception. :::warning This rule is being renamed to [`only-throw-error`](./only-throw-error.mdx). -When it was first created, it only prevented literals from being thrown (hence the name), but it has now been expanded to only allow expressions which have a possibility of being an `Error` object. -With the `allowThrowingAny` and `allowThrowingUnknown` options, it can be configured to only allow throwing values which are guaranteed to be an instance of `Error`. +The current name, `no-throw-literal`, will be removed in a future major version of typescript-eslint. -The current name `no-throw-literal` will be removed in a future major version of typescript-eslint. +When it was first created, this rule only prevented literals from being thrown (hence the name), but it has now been expanded to only allow expressions which have a possibility of being an `Error` object. +With the `allowThrowingAny` and `allowThrowingUnknown` options, it can be configured to only allow throwing values which are guaranteed to be an instance of `Error`. ::: {/* Intentionally Omitted: When Not To Use It */} diff --git a/packages/eslint-plugin/docs/rules/no-useless-template-expression.mdx b/packages/eslint-plugin/docs/rules/no-useless-template-expression.mdx new file mode 100644 index 000000000000..2b6a28802b22 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-useless-template-expression.mdx @@ -0,0 +1,87 @@ +--- +description: 'Disallow unnecessary template expressions.' +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/no-useless-template-expression** for documentation. + +This rule reports template literals that contain substitution expressions (also variously referred to as embedded expressions or string interpolations) that are unnecessary and can be simplified. + +:::info[Migration from `no-useless-template-literals`] + +This rule was formerly known as [`no-useless-template-literals`](./no-useless-template-literals.mdx). +We encourage users to migrate to the new name, `no-useless-template-expression`, as the old name will be removed in a future major version of typescript-eslint. + +The new name is a drop-in replacement with identical functionality. + +::: + +## Examples + + + + +```ts +// Static values can be incorporated into the surrounding template. + +const ab1 = `${'a'}${'b'}`; +const ab2 = `a${'b'}`; + +const stringWithNumber = `${'1 + 1 = '}${2}`; + +const stringWithBoolean = `${'true is '}${true}`; + +// Some simple expressions that are already strings +// can be rewritten without a template at all. + +const text = 'a'; +const wrappedText = `${text}`; + +declare const intersectionWithString: string & { _brand: 'test-brand' }; +const wrappedIntersection = `${intersectionWithString}`; +``` + + + + +```ts +// Static values can be incorporated into the surrounding template. + +const ab1 = `ab`; +const ab2 = `ab`; + +const stringWithNumber = `1 + 1 = 2`; + +const stringWithBoolean = `true is true`; + +// Some simple expressions that are already strings +// can be rewritten without a template at all. + +const text = 'a'; +const wrappedText = text; + +declare const intersectionWithString: string & { _brand: 'test-brand' }; +const wrappedIntersection = intersectionWithString; +``` + + + + +:::info +This rule does not aim to flag template literals without substitution expressions that could have been written as an ordinary string. +That is to say, this rule will not help you turn `` `this` `` into `"this"`. +If you are looking for such a rule, you can configure the [`@stylistic/ts/quotes`](https://eslint.style/rules/ts/quotes) rule to do this. +::: + +## When Not To Use It + +When you want to allow string expressions inside template literals. + +## Related To + +- [`restrict-template-expressions`](./restrict-template-expressions.mdx) +- [`@stylistic/ts/quotes`](https://eslint.style/rules/ts/quotes) diff --git a/packages/eslint-plugin/docs/rules/no-useless-template-literals.mdx b/packages/eslint-plugin/docs/rules/no-useless-template-literals.mdx index b229a4b1ff67..f02ec5ada9ec 100644 --- a/packages/eslint-plugin/docs/rules/no-useless-template-literals.mdx +++ b/packages/eslint-plugin/docs/rules/no-useless-template-literals.mdx @@ -1,5 +1,5 @@ --- -description: 'Disallow unnecessary template literals.' +description: 'Disallow unnecessary template expressions.' --- import Tabs from '@theme/Tabs'; @@ -9,53 +9,15 @@ import TabItem from '@theme/TabItem'; > > See **https://typescript-eslint.io/rules/no-useless-template-literals** for documentation. -This rule reports template literals that can be simplified to a normal string literal. +This rule reports template literals that contain substitution expressions (also variously referred to as embedded expressions or string interpolations) that are unnecessary and can be simplified. -## Examples +:::warning +This rule is being renamed to [`no-useless-template-expression`](./no-useless-template-expression.mdx). +The current name, `no-useless-template-literals`, will be removed in a future major version of typescript-eslint. - - +After the creation of this rule, it was realized that the name `no-useless-template-literals` could be misleading, seeing as this rule only targets template literals with substitution expressions. +In particular, it does _not_ aim to flag useless template literals that look like `` `this` `` and could be simplified to `"this"`. +If you are looking for such a rule, you can configure the [`@stylistic/ts/quotes`](https://eslint.style/rules/ts/quotes) rule to do this. +::: -```ts -const ab1 = `${'a'}${'b'}`; -const ab2 = `a${'b'}`; - -const stringWithNumber = `${'1 + 1 = '}${2}`; - -const stringWithBoolean = `${'true is '}${true}`; - -const text = 'a'; -const wrappedText = `${text}`; - -declare const intersectionWithString: string & { _brand: 'test-brand' }; -const wrappedIntersection = `${intersectionWithString}`; -``` - - - - -```ts -const ab1 = 'ab'; -const ab2 = 'ab'; - -const stringWithNumber = `1 + 1 = 2`; - -const stringWithBoolean = `true is true`; - -const text = 'a'; -const wrappedText = text; - -declare const intersectionWithString: string & { _brand: 'test-brand' }; -const wrappedIntersection = intersectionWithString; -``` - - - - -## When Not To Use It - -When you want to allow string expressions inside template literals. - -## Related To - -- [`restrict-template-expressions`](./restrict-template-expressions.mdx) +{/* Intentionally Omitted: When Not To Use It */} diff --git a/packages/eslint-plugin/docs/rules/only-throw-error.mdx b/packages/eslint-plugin/docs/rules/only-throw-error.mdx index cd27f8cc4387..0070d648a888 100644 --- a/packages/eslint-plugin/docs/rules/only-throw-error.mdx +++ b/packages/eslint-plugin/docs/rules/only-throw-error.mdx @@ -14,6 +14,15 @@ The fundamental benefit of `Error` objects is that they automatically keep track This rule restricts what can be thrown as an exception. +:::info[Migration from `no-throw-literal`] + +This rule was formerly known as [`no-throw-literal`](./no-throw-literal.mdx). +We encourage users to migrate to the new name, `only-throw-error`, as the old name will be removed in a future major version of typescript-eslint. + +The new name is a drop-in replacement with identical functionality. + +::: + ## Examples This rule is aimed at maintaining consistency when throwing exception by disallowing to throw literals and other expressions which cannot possibly be an `Error` object. diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 1e7304e45a36..ce40957c831d 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -113,7 +113,7 @@ export = { 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'error', '@typescript-eslint/no-useless-empty-export': 'error', - '@typescript-eslint/no-useless-template-literals': 'error', + '@typescript-eslint/no-useless-template-expression': 'error', '@typescript-eslint/no-var-requires': 'error', '@typescript-eslint/non-nullable-type-assertion-style': 'error', 'no-throw-literal': 'off', diff --git a/packages/eslint-plugin/src/configs/disable-type-checked.ts b/packages/eslint-plugin/src/configs/disable-type-checked.ts index fd3a8c2d7cbd..6400ad4f43ee 100644 --- a/packages/eslint-plugin/src/configs/disable-type-checked.ts +++ b/packages/eslint-plugin/src/configs/disable-type-checked.ts @@ -39,6 +39,7 @@ export = { '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-unsafe-return': 'off', '@typescript-eslint/no-unsafe-unary-minus': 'off', + '@typescript-eslint/no-useless-template-expression': 'off', '@typescript-eslint/no-useless-template-literals': 'off', '@typescript-eslint/non-nullable-type-assertion-style': 'off', '@typescript-eslint/only-throw-error': 'off', diff --git a/packages/eslint-plugin/src/configs/strict-type-checked-only.ts b/packages/eslint-plugin/src/configs/strict-type-checked-only.ts index 12709933dfb7..0815a6007e31 100644 --- a/packages/eslint-plugin/src/configs/strict-type-checked-only.ts +++ b/packages/eslint-plugin/src/configs/strict-type-checked-only.ts @@ -33,7 +33,7 @@ export = { '@typescript-eslint/no-unsafe-enum-comparison': 'error', '@typescript-eslint/no-unsafe-member-access': 'error', '@typescript-eslint/no-unsafe-return': 'error', - '@typescript-eslint/no-useless-template-literals': 'error', + '@typescript-eslint/no-useless-template-expression': 'error', 'no-throw-literal': 'off', '@typescript-eslint/only-throw-error': 'error', '@typescript-eslint/prefer-includes': 'error', diff --git a/packages/eslint-plugin/src/configs/strict-type-checked.ts b/packages/eslint-plugin/src/configs/strict-type-checked.ts index 26d8d9698812..2396c00ced5e 100644 --- a/packages/eslint-plugin/src/configs/strict-type-checked.ts +++ b/packages/eslint-plugin/src/configs/strict-type-checked.ts @@ -60,7 +60,7 @@ export = { '@typescript-eslint/no-unused-vars': 'error', 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'error', - '@typescript-eslint/no-useless-template-literals': 'error', + '@typescript-eslint/no-useless-template-expression': 'error', '@typescript-eslint/no-var-requires': 'error', 'no-throw-literal': 'off', '@typescript-eslint/only-throw-error': 'error', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 64c6a872ed69..9befde4df7bf 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -97,6 +97,7 @@ import noUnusedVars from './no-unused-vars'; import noUseBeforeDefine from './no-use-before-define'; import noUselessConstructor from './no-useless-constructor'; import noUselessEmptyExport from './no-useless-empty-export'; +import noUselessTemplateExpression from './no-useless-template-expression'; import noUselessTemplateLiterals from './no-useless-template-literals'; import noVarRequires from './no-var-requires'; import nonNullableTypeAssertionStyle from './non-nullable-type-assertion-style'; @@ -242,6 +243,7 @@ export default { 'no-use-before-define': noUseBeforeDefine, 'no-useless-constructor': noUselessConstructor, 'no-useless-empty-export': noUselessEmptyExport, + 'no-useless-template-expression': noUselessTemplateExpression, 'no-useless-template-literals': noUselessTemplateLiterals, 'no-var-requires': noVarRequires, 'non-nullable-type-assertion-style': nonNullableTypeAssertionStyle, diff --git a/packages/eslint-plugin/src/rules/no-useless-template-expression.ts b/packages/eslint-plugin/src/rules/no-useless-template-expression.ts new file mode 100644 index 000000000000..e7afc261e2fe --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-useless-template-expression.ts @@ -0,0 +1,175 @@ +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import * as ts from 'typescript'; + +import { + createRule, + getConstrainedTypeAtLocation, + getParserServices, + getStaticStringValue, + isTypeFlagSet, + isUndefinedIdentifier, +} from '../util'; + +type MessageId = 'noUselessTemplateExpression'; + +export default createRule<[], MessageId>({ + name: 'no-useless-template-expression', + meta: { + fixable: 'code', + type: 'suggestion', + docs: { + description: 'Disallow unnecessary template expressions', + recommended: 'strict', + requiresTypeChecking: true, + }, + messages: { + noUselessTemplateExpression: + 'Template literal expression is unnecessary and can be simplified.', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + const services = getParserServices(context); + + function isUnderlyingTypeString( + expression: TSESTree.Expression, + ): expression is TSESTree.StringLiteral | TSESTree.Identifier { + const type = getConstrainedTypeAtLocation(services, expression); + + const isString = (t: ts.Type): boolean => { + return isTypeFlagSet(t, ts.TypeFlags.StringLike); + }; + + if (type.isUnion()) { + return type.types.every(isString); + } + + if (type.isIntersection()) { + return type.types.some(isString); + } + + return isString(type); + } + + function isLiteral(expression: TSESTree.Expression): boolean { + return expression.type === AST_NODE_TYPES.Literal; + } + + function isTemplateLiteral(expression: TSESTree.Expression): boolean { + return expression.type === AST_NODE_TYPES.TemplateLiteral; + } + + function isInfinityIdentifier(expression: TSESTree.Expression): boolean { + return ( + expression.type === AST_NODE_TYPES.Identifier && + expression.name === 'Infinity' + ); + } + + function isNaNIdentifier(expression: TSESTree.Expression): boolean { + return ( + expression.type === AST_NODE_TYPES.Identifier && + expression.name === 'NaN' + ); + } + + return { + TemplateLiteral(node: TSESTree.TemplateLiteral): void { + if (node.parent.type === AST_NODE_TYPES.TaggedTemplateExpression) { + return; + } + + const hasSingleStringVariable = + node.quasis.length === 2 && + node.quasis[0].value.raw === '' && + node.quasis[1].value.raw === '' && + node.expressions.length === 1 && + isUnderlyingTypeString(node.expressions[0]); + + if (hasSingleStringVariable) { + context.report({ + node: node.expressions[0], + messageId: 'noUselessTemplateExpression', + 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; + } + + const fixableExpressions = node.expressions.filter( + expression => + isLiteral(expression) || + isTemplateLiteral(expression) || + isUndefinedIdentifier(expression) || + isInfinityIdentifier(expression) || + isNaNIdentifier(expression), + ); + + fixableExpressions.forEach(expression => { + context.report({ + node: expression, + messageId: 'noUselessTemplateExpression', + 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, + ]), + ]; + + 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; + }, + }); + }); + }, + }; + }, +}); 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 4610d406465a..9156c6e4053e 100644 --- a/packages/eslint-plugin/src/rules/no-useless-template-literals.ts +++ b/packages/eslint-plugin/src/rules/no-useless-template-literals.ts @@ -11,7 +11,7 @@ import { isUndefinedIdentifier, } from '../util'; -type MessageId = 'noUselessTemplateLiteral'; +type MessageId = 'noUselessTemplateExpression'; export default createRule<[], MessageId>({ name: 'no-useless-template-literals', @@ -19,15 +19,16 @@ export default createRule<[], MessageId>({ fixable: 'code', type: 'suggestion', docs: { - description: 'Disallow unnecessary template literals', - recommended: 'strict', + description: 'Disallow unnecessary template expressions', requiresTypeChecking: true, }, messages: { - noUselessTemplateLiteral: + noUselessTemplateExpression: 'Template literal expression is unnecessary and can be simplified.', }, schema: [], + deprecated: true, + replacedBy: ['@typescript-eslint/no-useless-template-expression'], }, defaultOptions: [], create(context) { @@ -91,7 +92,7 @@ export default createRule<[], MessageId>({ if (hasSingleStringVariable) { context.report({ node: node.expressions[0], - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', fix(fixer): TSESLint.RuleFix[] { const [prevQuasi, nextQuasi] = node.quasis; @@ -125,7 +126,7 @@ export default createRule<[], MessageId>({ fixableExpressions.forEach(expression => { context.report({ node: expression, - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', fix(fixer): TSESLint.RuleFix[] { const index = node.expressions.indexOf(expression); const prevQuasi = node.quasis[index]; diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-useless-template-expression.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-useless-template-expression.shot new file mode 100644 index 000000000000..d5813404d862 --- /dev/null +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-useless-template-expression.shot @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Validating rule docs no-useless-template-expression.mdx code examples ESLint output 1`] = ` +"Incorrect + +// Static values can be incorporated into the surrounding template. + +const ab1 = \`\${'a'}\${'b'}\`; + ~~~ Template literal expression is unnecessary and can be simplified. + ~~~ Template literal expression is unnecessary and can be simplified. +const ab2 = \`a\${'b'}\`; + ~~~ Template literal expression is unnecessary and can be simplified. + +const stringWithNumber = \`\${'1 + 1 = '}\${2}\`; + ~~~~~~~~~~ Template literal expression is unnecessary and can be simplified. + ~ Template literal expression is unnecessary and can be simplified. + +const stringWithBoolean = \`\${'true is '}\${true}\`; + ~~~~~~~~~~ Template literal expression is unnecessary and can be simplified. + ~~~~ Template literal expression is unnecessary and can be simplified. + +// Some simple expressions that are already strings +// can be rewritten without a template at all. + +const text = 'a'; +const wrappedText = \`\${text}\`; + ~~~~ Template literal expression is unnecessary and can be simplified. + +declare const intersectionWithString: string & { _brand: 'test-brand' }; +const wrappedIntersection = \`\${intersectionWithString}\`; + ~~~~~~~~~~~~~~~~~~~~~~ Template literal expression is unnecessary and can be simplified. +" +`; + +exports[`Validating rule docs no-useless-template-expression.mdx code examples ESLint output 2`] = ` +"Correct + +// Static values can be incorporated into the surrounding template. + +const ab1 = \`ab\`; +const ab2 = \`ab\`; + +const stringWithNumber = \`1 + 1 = 2\`; + +const stringWithBoolean = \`true is true\`; + +// Some simple expressions that are already strings +// can be rewritten without a template at all. + +const text = 'a'; +const wrappedText = text; + +declare const intersectionWithString: string & { _brand: 'test-brand' }; +const wrappedIntersection = intersectionWithString; +" +`; diff --git a/packages/eslint-plugin/tests/rules/no-useless-template-expression.test.ts b/packages/eslint-plugin/tests/rules/no-useless-template-expression.test.ts new file mode 100644 index 000000000000..628057ac7d17 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-useless-template-expression.test.ts @@ -0,0 +1,641 @@ +import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/no-useless-template-expression'; +import { getFixturesRootDir } from '../RuleTester'; + +const rootPath = getFixturesRootDir(); + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, +}); + +ruleTester.run('no-useless-template-expression', rule, { + valid: [ + "const string = 'a';", + 'const string = `a`;', + ` + declare const string: 'a'; + \`\${string}b\`; + `, + + ` + declare const number: 1; + \`\${number}b\`; + `, + + ` + declare const boolean: true; + \`\${boolean}b\`; + `, + + ` + declare const nullish: null; + \`\${nullish}-undefined\`; + `, + + ` + declare const undefinedish: undefined; + \`\${undefinedish}\`; + `, + + ` + declare const left: 'a'; + declare const right: 'b'; + \`\${left}\${right}\`; + `, + + ` + declare const left: 'a'; + declare const right: 'c'; + \`\${left}b\${right}\`; + `, + + ` + declare const left: 'a'; + declare const center: 'b'; + declare const right: 'c'; + \`\${left}\${center}\${right}\`; + `, + + '`1 + 1 = ${1 + 1}`;', + + '`true && false = ${true && false}`;', + + "tag`${'a'}${'b'}`;", + + '`${function () {}}`;', + + '`${() => {}}`;', + + '`${(...args: any[]) => args}`;', + + ` + declare const number: 1; + \`\${number}\`; + `, + + ` + declare const boolean: true; + \`\${boolean}\`; + `, + + ` + declare const nullish: null; + \`\${nullish}\`; + `, + + ` + declare const union: string | number; + \`\${union}\`; + `, + + ` + declare const unknown: unknown; + \`\${unknown}\`; + `, + + ` + declare const never: never; + \`\${never}\`; + `, + + ` + declare const any: any; + \`\${any}\`; + `, + + ` + function func(arg: T) { + \`\${arg}\`; + } + `, + + ` + \`with + + new line\`; + `, + + ` + declare const a: 'a'; + + \`\${a} with + + new line\`; + `, + + noFormat` + \`with windows \r new line\`; + `, + + ` +\`not a useless \${String.raw\`nested interpolation \${a}\`}\`; + `, + ], + + invalid: [ + { + code: '`${1}`;', + output: '`1`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 1, + column: 4, + endColumn: 5, + }, + ], + }, + { + code: '`${1n}`;', + output: '`1`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 1, + column: 4, + endColumn: 6, + }, + ], + }, + { + code: '`${/a/}`;', + output: '`/a/`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 1, + column: 4, + endColumn: 7, + }, + ], + }, + + { + code: noFormat`\`\${ 1 }\`;`, + output: '`1`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + }, + ], + }, + + { + code: noFormat`\`\${ 'a' }\`;`, + output: `'a';`, + errors: [ + { + messageId: 'noUselessTemplateExpression', + }, + ], + }, + + { + code: noFormat`\`\${ "a" }\`;`, + output: `"a";`, + errors: [ + { + messageId: 'noUselessTemplateExpression', + }, + ], + }, + + { + code: noFormat`\`\${ 'a' + 'b' }\`;`, + output: `'a' + 'b';`, + errors: [ + { + messageId: 'noUselessTemplateExpression', + }, + ], + }, + + { + code: '`${true}`;', + output: '`true`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 1, + column: 4, + endColumn: 8, + }, + ], + }, + + { + code: noFormat`\`\${ true }\`;`, + output: '`true`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + }, + ], + }, + + { + code: '`${null}`;', + output: '`null`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 1, + column: 4, + endColumn: 8, + }, + ], + }, + + { + code: noFormat`\`\${ null }\`;`, + output: '`null`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + }, + ], + }, + + { + code: '`${undefined}`;', + output: '`undefined`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 1, + column: 4, + endColumn: 13, + }, + ], + }, + + { + code: noFormat`\`\${ undefined }\`;`, + output: '`undefined`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + }, + ], + }, + + { + code: '`${Infinity}`;', + output: '`Infinity`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 1, + column: 4, + endColumn: 12, + }, + ], + }, + + { + code: '`${NaN}`;', + output: '`NaN`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 1, + column: 4, + endColumn: 7, + }, + ], + }, + + { + code: "`${'a'} ${'b'}`;", + output: '`a b`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 1, + column: 4, + endColumn: 7, + }, + { + messageId: 'noUselessTemplateExpression', + line: 1, + column: 11, + endColumn: 14, + }, + ], + }, + + { + code: noFormat`\`\${ 'a' } \${ 'b' }\`;`, + output: '`a b`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + }, + { + messageId: 'noUselessTemplateExpression', + }, + ], + }, + + { + code: ` + declare const b: 'b'; + \`a\${b}\${'c'}\`; + `, + output: ` + declare const b: 'b'; + \`a\${b}c\`; + `, + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 3, + column: 17, + endColumn: 20, + }, + ], + }, + + { + code: "`use${'less'}`;", + output: '`useless`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 1, + }, + ], + }, + + { + code: '`use${`less`}`;', + output: '`useless`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 1, + }, + ], + }, + + { + code: ` +declare const nested: string, interpolation: string; +\`use\${\`less\${nested}\${interpolation}\`}\`; + `, + output: ` +declare const nested: string, interpolation: string; +\`useless\${nested}\${interpolation}\`; + `, + errors: [ + { + messageId: 'noUselessTemplateExpression', + }, + ], + }, + + { + code: noFormat` +\`u\${ + // hopefully this comment is not needed. + 'se' + +}\${ + \`le\${ \`ss\` }\` +}\`; + `, + output: ` +\`use\${ + \`less\` +}\`; + `, + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 4, + }, + { + messageId: 'noUselessTemplateExpression', + line: 7, + column: 3, + endLine: 7, + }, + { + messageId: 'noUselessTemplateExpression', + line: 7, + column: 10, + endLine: 7, + }, + ], + }, + { + code: noFormat` +\`use\${ + \`less\` +}\`; + `, + output: ` +\`useless\`; + `, + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 3, + column: 3, + endColumn: 9, + }, + ], + }, + + { + code: "`${'1 + 1 ='} ${2}`;", + output: '`1 + 1 = 2`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 1, + column: 4, + endColumn: 13, + }, + { + messageId: 'noUselessTemplateExpression', + line: 1, + column: 17, + endColumn: 18, + }, + ], + }, + + { + code: "`${'a'} ${true}`;", + output: '`a true`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 1, + column: 4, + endColumn: 7, + }, + { + messageId: 'noUselessTemplateExpression', + line: 1, + column: 11, + endColumn: 15, + }, + ], + }, + + { + code: ` + declare const string: 'a'; + \`\${string}\`; + `, + output: ` + declare const string: 'a'; + string; + `, + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 3, + column: 12, + endColumn: 18, + }, + ], + }, + + { + code: noFormat` + declare const string: 'a'; + \`\${ string }\`; + `, + output: ` + declare const string: 'a'; + string; + `, + errors: [ + { + messageId: 'noUselessTemplateExpression', + }, + ], + }, + + { + code: "`${String(Symbol.for('test'))}`;", + output: "String(Symbol.for('test'));", + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 1, + column: 4, + endColumn: 30, + }, + ], + }, + + { + code: ` + declare const intersection: string & { _brand: 'test-brand' }; + \`\${intersection}\`; + `, + output: ` + declare const intersection: string & { _brand: 'test-brand' }; + intersection; + `, + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 3, + column: 12, + endColumn: 24, + }, + ], + }, + + { + code: ` + function func(arg: T) { + \`\${arg}\`; + } + `, + output: ` + function func(arg: T) { + arg; + } + `, + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 3, + column: 14, + endColumn: 17, + }, + ], + }, + + { + code: "`${'`'}`;", + output: "'`';", + errors: [ + { + messageId: 'noUselessTemplateExpression', + }, + ], + }, + + { + code: "`back${'`'}tick`;", + output: '`back\\`tick`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + }, + ], + }, + + { + code: "`dollar${'${`this is test`}'}sign`;", + output: '`dollar\\${\\`this is test\\`}sign`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + }, + ], + }, + + { + code: '`complex${\'`${"`${test}`"}`\'}case`;', + output: '`complex\\`\\${"\\`\\${test}\\`"}\\`case`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + }, + ], + }, + + { + code: "`some ${'\\\\${test}'} string`;", + output: '`some \\\\\\${test} string`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + }, + ], + }, + + { + code: "`some ${'\\\\`'} string`;", + output: '`some \\\\\\` string`;', + errors: [ + { + messageId: 'noUselessTemplateExpression', + }, + ], + }, + ], +}); 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 d443c4ff729d..06eacb1e31e6 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 @@ -143,7 +143,7 @@ ruleTester.run('no-useless-template-literals', rule, { output: '`1`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 1, column: 4, endColumn: 5, @@ -155,7 +155,7 @@ ruleTester.run('no-useless-template-literals', rule, { output: '`1`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 1, column: 4, endColumn: 6, @@ -167,7 +167,7 @@ ruleTester.run('no-useless-template-literals', rule, { output: '`/a/`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 1, column: 4, endColumn: 7, @@ -180,7 +180,7 @@ ruleTester.run('no-useless-template-literals', rule, { output: '`1`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', }, ], }, @@ -190,7 +190,7 @@ ruleTester.run('no-useless-template-literals', rule, { output: `'a';`, errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', }, ], }, @@ -200,7 +200,7 @@ ruleTester.run('no-useless-template-literals', rule, { output: `"a";`, errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', }, ], }, @@ -210,7 +210,7 @@ ruleTester.run('no-useless-template-literals', rule, { output: `'a' + 'b';`, errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', }, ], }, @@ -220,7 +220,7 @@ ruleTester.run('no-useless-template-literals', rule, { output: '`true`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 1, column: 4, endColumn: 8, @@ -233,7 +233,7 @@ ruleTester.run('no-useless-template-literals', rule, { output: '`true`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', }, ], }, @@ -243,7 +243,7 @@ ruleTester.run('no-useless-template-literals', rule, { output: '`null`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 1, column: 4, endColumn: 8, @@ -256,7 +256,7 @@ ruleTester.run('no-useless-template-literals', rule, { output: '`null`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', }, ], }, @@ -266,7 +266,7 @@ ruleTester.run('no-useless-template-literals', rule, { output: '`undefined`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 1, column: 4, endColumn: 13, @@ -279,7 +279,7 @@ ruleTester.run('no-useless-template-literals', rule, { output: '`undefined`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', }, ], }, @@ -289,7 +289,7 @@ ruleTester.run('no-useless-template-literals', rule, { output: '`Infinity`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 1, column: 4, endColumn: 12, @@ -302,7 +302,7 @@ ruleTester.run('no-useless-template-literals', rule, { output: '`NaN`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 1, column: 4, endColumn: 7, @@ -315,13 +315,13 @@ ruleTester.run('no-useless-template-literals', rule, { output: '`a b`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 1, column: 4, endColumn: 7, }, { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 1, column: 11, endColumn: 14, @@ -334,10 +334,10 @@ ruleTester.run('no-useless-template-literals', rule, { output: '`a b`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', }, { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', }, ], }, @@ -353,7 +353,7 @@ ruleTester.run('no-useless-template-literals', rule, { `, errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 3, column: 17, endColumn: 20, @@ -366,7 +366,7 @@ ruleTester.run('no-useless-template-literals', rule, { output: '`useless`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 1, }, ], @@ -377,7 +377,7 @@ ruleTester.run('no-useless-template-literals', rule, { output: '`useless`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 1, }, ], @@ -394,7 +394,7 @@ declare const nested: string, interpolation: string; `, errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', }, ], }, @@ -416,17 +416,17 @@ declare const nested: string, interpolation: string; `, errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 4, }, { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 7, column: 3, endLine: 7, }, { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 7, column: 10, endLine: 7, @@ -444,7 +444,7 @@ declare const nested: string, interpolation: string; `, errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 3, column: 3, endColumn: 9, @@ -457,13 +457,13 @@ declare const nested: string, interpolation: string; output: '`1 + 1 = 2`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 1, column: 4, endColumn: 13, }, { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 1, column: 17, endColumn: 18, @@ -476,13 +476,13 @@ declare const nested: string, interpolation: string; output: '`a true`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 1, column: 4, endColumn: 7, }, { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 1, column: 11, endColumn: 15, @@ -501,7 +501,7 @@ declare const nested: string, interpolation: string; `, errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 3, column: 12, endColumn: 18, @@ -520,7 +520,7 @@ declare const nested: string, interpolation: string; `, errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', }, ], }, @@ -530,7 +530,7 @@ declare const nested: string, interpolation: string; output: "String(Symbol.for('test'));", errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 1, column: 4, endColumn: 30, @@ -549,7 +549,7 @@ declare const nested: string, interpolation: string; `, errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 3, column: 12, endColumn: 24, @@ -570,7 +570,7 @@ declare const nested: string, interpolation: string; `, errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', line: 3, column: 14, endColumn: 17, @@ -583,7 +583,7 @@ declare const nested: string, interpolation: string; output: "'`';", errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', }, ], }, @@ -593,7 +593,7 @@ declare const nested: string, interpolation: string; output: '`back\\`tick`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', }, ], }, @@ -603,7 +603,7 @@ declare const nested: string, interpolation: string; output: '`dollar\\${\\`this is test\\`}sign`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', }, ], }, @@ -613,7 +613,7 @@ declare const nested: string, interpolation: string; output: '`complex\\`\\${"\\`\\${test}\\`"}\\`case`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', }, ], }, @@ -623,7 +623,7 @@ declare const nested: string, interpolation: string; output: '`some \\\\\\${test} string`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', }, ], }, @@ -633,7 +633,7 @@ declare const nested: string, interpolation: string; output: '`some \\\\\\` string`;', errors: [ { - messageId: 'noUselessTemplateLiteral', + messageId: 'noUselessTemplateExpression', }, ], }, diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-useless-template-expression.shot b/packages/eslint-plugin/tests/schema-snapshots/no-useless-template-expression.shot new file mode 100644 index 000000000000..f34596f9a1a4 --- /dev/null +++ b/packages/eslint-plugin/tests/schema-snapshots/no-useless-template-expression.shot @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rule schemas should be convertible to TS types for documentation purposes no-useless-template-expression 1`] = ` +" +# SCHEMA: + +[] + + +# TYPES: + +/** No options declared */ +type Options = [];" +`; diff --git a/packages/typescript-eslint/src/configs/all.ts b/packages/typescript-eslint/src/configs/all.ts index efe0d16fceb8..71d701e3d1ca 100644 --- a/packages/typescript-eslint/src/configs/all.ts +++ b/packages/typescript-eslint/src/configs/all.ts @@ -122,7 +122,7 @@ export default ( 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'error', '@typescript-eslint/no-useless-empty-export': 'error', - '@typescript-eslint/no-useless-template-literals': 'error', + '@typescript-eslint/no-useless-template-expression': 'error', '@typescript-eslint/no-var-requires': 'error', '@typescript-eslint/non-nullable-type-assertion-style': 'error', 'no-throw-literal': 'off', diff --git a/packages/typescript-eslint/src/configs/disable-type-checked.ts b/packages/typescript-eslint/src/configs/disable-type-checked.ts index 8d2f29220aba..9be9ab93dfd3 100644 --- a/packages/typescript-eslint/src/configs/disable-type-checked.ts +++ b/packages/typescript-eslint/src/configs/disable-type-checked.ts @@ -42,6 +42,7 @@ export default ( '@typescript-eslint/no-unsafe-member-access': 'off', '@typescript-eslint/no-unsafe-return': 'off', '@typescript-eslint/no-unsafe-unary-minus': 'off', + '@typescript-eslint/no-useless-template-expression': 'off', '@typescript-eslint/no-useless-template-literals': 'off', '@typescript-eslint/non-nullable-type-assertion-style': 'off', '@typescript-eslint/only-throw-error': 'off', diff --git a/packages/typescript-eslint/src/configs/strict-type-checked-only.ts b/packages/typescript-eslint/src/configs/strict-type-checked-only.ts index f17b5280ca49..a2dbdaab1059 100644 --- a/packages/typescript-eslint/src/configs/strict-type-checked-only.ts +++ b/packages/typescript-eslint/src/configs/strict-type-checked-only.ts @@ -42,7 +42,7 @@ export default ( '@typescript-eslint/no-unsafe-enum-comparison': 'error', '@typescript-eslint/no-unsafe-member-access': 'error', '@typescript-eslint/no-unsafe-return': 'error', - '@typescript-eslint/no-useless-template-literals': 'error', + '@typescript-eslint/no-useless-template-expression': 'error', 'no-throw-literal': 'off', '@typescript-eslint/only-throw-error': 'error', '@typescript-eslint/prefer-includes': 'error', diff --git a/packages/typescript-eslint/src/configs/strict-type-checked.ts b/packages/typescript-eslint/src/configs/strict-type-checked.ts index ad62ee749e25..20db0da63d1b 100644 --- a/packages/typescript-eslint/src/configs/strict-type-checked.ts +++ b/packages/typescript-eslint/src/configs/strict-type-checked.ts @@ -69,7 +69,7 @@ export default ( '@typescript-eslint/no-unused-vars': 'error', 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'error', - '@typescript-eslint/no-useless-template-literals': 'error', + '@typescript-eslint/no-useless-template-expression': 'error', '@typescript-eslint/no-var-requires': 'error', 'no-throw-literal': 'off', '@typescript-eslint/only-throw-error': 'error',