From 12182f56d10e05d427e13753c425a1bcc7cf40b1 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Mon, 1 Apr 2024 19:24:34 -0600 Subject: [PATCH 1/7] Rename no-useless-template-literals to no-useless-template-expression --- .../rules/no-useless-template-expression.mdx | 78 +++ .../rules/no-useless-template-literals.mdx | 58 +- packages/eslint-plugin/src/configs/all.ts | 2 +- .../src/configs/disable-type-checked.ts | 1 + .../src/configs/strict-type-checked-only.ts | 2 +- .../src/configs/strict-type-checked.ts | 2 +- packages/eslint-plugin/src/rules/index.ts | 2 + .../rules/no-useless-template-expression.ts | 194 ++++++ .../src/rules/no-useless-template-literals.ts | 195 +----- packages/eslint-plugin/tests/docs.test.ts | 4 +- .../no-useless-template-expression.test.ts | 652 ++++++++++++++++++ .../no-useless-template-literals.test.ts | 634 +---------------- .../no-useless-template-expression.shot | 14 + packages/typescript-eslint/src/configs/all.ts | 2 +- .../src/configs/disable-type-checked.ts | 1 + .../src/configs/strict-type-checked-only.ts | 2 +- .../src/configs/strict-type-checked.ts | 2 +- 17 files changed, 988 insertions(+), 857 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/no-useless-template-expression.mdx create mode 100644 packages/eslint-plugin/src/rules/no-useless-template-expression.ts create mode 100644 packages/eslint-plugin/tests/rules/no-useless-template-expression.test.ts create mode 100644 packages/eslint-plugin/tests/schema-snapshots/no-useless-template-expression.shot 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..972d47709739 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-useless-template-expression.mdx @@ -0,0 +1,78 @@ +--- +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. + +## 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 [`quotes`](./quotes.mdx) 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) +- [`quotes`](./quotes.mdx) 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..c0f82b01c148 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). +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 [`quotes`](./quotes.mdx) rule to do this. - - +The current name, `no-useless-template-literals`, will be removed in a future major version of typescript-eslint. +::: -```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/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..01e37f3fa213 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-useless-template-expression.ts @@ -0,0 +1,194 @@ +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'; + +export type NoUselessTemplateExpressionMessageId = + 'noUselessTemplateExpression'; + +export type NoUselessTemplateExpressionRuleObject = Parameters< + typeof createRule<[], NoUselessTemplateExpressionMessageId> +>[0]; + +export function makeNoUselessTemplateExpressionRuleObject(): NoUselessTemplateExpressionRuleObject { + // For some reason, TS isn't totally happy if you return the object directly, + // but it doesn't mind if you assign it to a variable first. + const ruleObject: NoUselessTemplateExpressionRuleObject = { + 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; + }, + }); + }); + }, + }; + }, + }; + + return ruleObject; +} + +// The rule object factory is to support code sharing between the current rule, +// and the deprecated version of this rule under its previous name +// no-useless-template-literals. +// See https://github.com/typescript-eslint/typescript-eslint/issues/8544 +export default createRule<[], NoUselessTemplateExpressionMessageId>( + makeNoUselessTemplateExpressionRuleObject(), +); 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..72a61d4c8025 100644 --- a/packages/eslint-plugin/src/rules/no-useless-template-literals.ts +++ b/packages/eslint-plugin/src/rules/no-useless-template-literals.ts @@ -1,175 +1,20 @@ -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 = 'noUselessTemplateLiteral'; - -export default createRule<[], MessageId>({ - name: 'no-useless-template-literals', - meta: { - fixable: 'code', - type: 'suggestion', - docs: { - description: 'Disallow unnecessary template literals', - recommended: 'strict', - requiresTypeChecking: true, - }, - messages: { - noUselessTemplateLiteral: - '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: '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; - } - - const fixableExpressions = node.expressions.filter( - expression => - isLiteral(expression) || - isTemplateLiteral(expression) || - isUndefinedIdentifier(expression) || - isInfinityIdentifier(expression) || - isNaNIdentifier(expression), - ); - - fixableExpressions.forEach(expression => { - 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, - ]), - ]; - - 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; - }, - }); - }); - }, - }; - }, -}); +import { createRule } from '../util'; +import type { NoUselessTemplateExpressionMessageId } from './no-useless-template-expression'; +import { makeNoUselessTemplateExpressionRuleObject } from './no-useless-template-expression'; + +// This rule was renamed to `no-useless-template-expression`. +// This module's purpose is just to import the code from the new implementation +// and adjust its metadata to account for the renaming. +// See https://github.com/typescript-eslint/typescript-eslint/issues/8544 + +const ruleObject = makeNoUselessTemplateExpressionRuleObject(); +// @ts-expect-error: easier than figuring out how to make this mutable. +ruleObject.name = 'no-useless-template-literals'; +ruleObject.meta.replacedBy = [ + '@typescript-eslint/no-useless-template-expression', +]; +ruleObject.meta.deprecated = true; +// not recommended anymore; the new rule is recommended instead. +delete ruleObject.meta.docs.recommended; + +export default createRule<[], NoUselessTemplateExpressionMessageId>(ruleObject); diff --git a/packages/eslint-plugin/tests/docs.test.ts b/packages/eslint-plugin/tests/docs.test.ts index b3000aa3a721..5439e4bc6755 100644 --- a/packages/eslint-plugin/tests/docs.test.ts +++ b/packages/eslint-plugin/tests/docs.test.ts @@ -215,6 +215,8 @@ describe('Validating rule metadata', () => { const rulesThatRequireTypeInformationInAWayThatsHardToDetect = new Set([ // the core rule file doesn't use type information, instead it's used in `src/rules/naming-convention-utils/validator.ts` 'naming-convention', + // rule moved to no-useless-template-expression. + 'no-useless-template-literals', ]); function requiresFullTypeInformation(content: string): boolean { return /getParserServices(\(\s*[^,\s)]+)\s*(,\s*false\s*)?\)/.test(content); @@ -224,7 +226,7 @@ describe('Validating rule metadata', () => { describe(ruleName, () => { it('`name` field in rule must match the filename', () => { // validate if rule name is same as url - // there is no way to access this field but its used only in generation of docs url + // there is no way to access this field but it's used only in generation of docs url expect(rule.meta.docs?.url).toBe( `https://typescript-eslint.io/rules/${ruleName}`, ); 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..647c6b621131 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-useless-template-expression.test.ts @@ -0,0 +1,652 @@ +import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; + +import type { NoUselessTemplateExpressionMessageId } from '../../src/rules/no-useless-template-expression'; +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, + getNoUselessTemplateExpressionTestCases(), +); + +export function getNoUselessTemplateExpressionTestCases(): Parameters< + typeof ruleTester.run +>[2] { + return { + 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..5bc461968d39 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 @@ -1,7 +1,8 @@ -import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; +import { RuleTester } from '@typescript-eslint/rule-tester'; import rule from '../../src/rules/no-useless-template-literals'; import { getFixturesRootDir } from '../RuleTester'; +import { getNoUselessTemplateExpressionTestCases } from './no-useless-template-expression.test'; const rootPath = getFixturesRootDir(); @@ -13,629 +14,8 @@ const ruleTester = new RuleTester({ }, }); -ruleTester.run('no-useless-template-literals', 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: 'noUselessTemplateLiteral', - line: 1, - column: 4, - endColumn: 5, - }, - ], - }, - { - code: '`${1n}`;', - output: '`1`;', - errors: [ - { - messageId: 'noUselessTemplateLiteral', - line: 1, - column: 4, - endColumn: 6, - }, - ], - }, - { - code: '`${/a/}`;', - output: '`/a/`;', - errors: [ - { - messageId: 'noUselessTemplateLiteral', - line: 1, - column: 4, - endColumn: 7, - }, - ], - }, - - { - code: noFormat`\`\${ 1 }\`;`, - output: '`1`;', - errors: [ - { - messageId: 'noUselessTemplateLiteral', - }, - ], - }, - - { - code: noFormat`\`\${ 'a' }\`;`, - output: `'a';`, - errors: [ - { - messageId: 'noUselessTemplateLiteral', - }, - ], - }, - - { - 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', - line: 1, - column: 4, - endColumn: 8, - }, - ], - }, - - { - code: noFormat`\`\${ true }\`;`, - output: '`true`;', - errors: [ - { - messageId: 'noUselessTemplateLiteral', - }, - ], - }, - - { - code: '`${null}`;', - output: '`null`;', - errors: [ - { - messageId: 'noUselessTemplateLiteral', - line: 1, - column: 4, - endColumn: 8, - }, - ], - }, - - { - code: noFormat`\`\${ null }\`;`, - output: '`null`;', - errors: [ - { - messageId: 'noUselessTemplateLiteral', - }, - ], - }, - - { - code: '`${undefined}`;', - output: '`undefined`;', - errors: [ - { - messageId: 'noUselessTemplateLiteral', - line: 1, - column: 4, - endColumn: 13, - }, - ], - }, - - { - code: noFormat`\`\${ undefined }\`;`, - output: '`undefined`;', - errors: [ - { - messageId: 'noUselessTemplateLiteral', - }, - ], - }, - - { - code: '`${Infinity}`;', - output: '`Infinity`;', - errors: [ - { - messageId: 'noUselessTemplateLiteral', - line: 1, - column: 4, - endColumn: 12, - }, - ], - }, - - { - code: '`${NaN}`;', - output: '`NaN`;', - errors: [ - { - messageId: 'noUselessTemplateLiteral', - line: 1, - column: 4, - endColumn: 7, - }, - ], - }, - - { - code: "`${'a'} ${'b'}`;", - output: '`a b`;', - errors: [ - { - messageId: 'noUselessTemplateLiteral', - line: 1, - column: 4, - endColumn: 7, - }, - { - messageId: 'noUselessTemplateLiteral', - line: 1, - column: 11, - endColumn: 14, - }, - ], - }, - - { - code: noFormat`\`\${ 'a' } \${ 'b' }\`;`, - output: '`a b`;', - errors: [ - { - messageId: 'noUselessTemplateLiteral', - }, - { - messageId: 'noUselessTemplateLiteral', - }, - ], - }, - - { - code: ` - declare const b: 'b'; - \`a\${b}\${'c'}\`; - `, - output: ` - declare const b: 'b'; - \`a\${b}c\`; - `, - errors: [ - { - messageId: 'noUselessTemplateLiteral', - line: 3, - column: 17, - endColumn: 20, - }, - ], - }, - - { - code: "`use${'less'}`;", - output: '`useless`;', - errors: [ - { - messageId: 'noUselessTemplateLiteral', - line: 1, - }, - ], - }, - - { - code: '`use${`less`}`;', - output: '`useless`;', - errors: [ - { - messageId: 'noUselessTemplateLiteral', - 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: 'noUselessTemplateLiteral', - }, - ], - }, - - { - code: noFormat` -\`u\${ - // hopefully this comment is not needed. - 'se' - -}\${ - \`le\${ \`ss\` }\` -}\`; - `, - output: ` -\`use\${ - \`less\` -}\`; - `, - errors: [ - { - messageId: 'noUselessTemplateLiteral', - line: 4, - }, - { - messageId: 'noUselessTemplateLiteral', - line: 7, - column: 3, - endLine: 7, - }, - { - messageId: 'noUselessTemplateLiteral', - line: 7, - column: 10, - endLine: 7, - }, - ], - }, - { - code: noFormat` -\`use\${ - \`less\` -}\`; - `, - output: ` -\`useless\`; - `, - errors: [ - { - messageId: 'noUselessTemplateLiteral', - line: 3, - column: 3, - endColumn: 9, - }, - ], - }, - - { - code: "`${'1 + 1 ='} ${2}`;", - output: '`1 + 1 = 2`;', - errors: [ - { - messageId: 'noUselessTemplateLiteral', - line: 1, - column: 4, - endColumn: 13, - }, - { - messageId: 'noUselessTemplateLiteral', - line: 1, - column: 17, - endColumn: 18, - }, - ], - }, - - { - code: "`${'a'} ${true}`;", - output: '`a true`;', - errors: [ - { - messageId: 'noUselessTemplateLiteral', - line: 1, - column: 4, - endColumn: 7, - }, - { - messageId: 'noUselessTemplateLiteral', - line: 1, - column: 11, - endColumn: 15, - }, - ], - }, - - { - code: ` - declare const string: 'a'; - \`\${string}\`; - `, - output: ` - declare const string: 'a'; - string; - `, - errors: [ - { - messageId: 'noUselessTemplateLiteral', - line: 3, - column: 12, - endColumn: 18, - }, - ], - }, - - { - 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', - 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: 'noUselessTemplateLiteral', - line: 3, - column: 12, - endColumn: 24, - }, - ], - }, - - { - code: ` - function func(arg: T) { - \`\${arg}\`; - } - `, - output: ` - function func(arg: T) { - arg; - } - `, - errors: [ - { - messageId: 'noUselessTemplateLiteral', - line: 3, - column: 14, - endColumn: 17, - }, - ], - }, - - { - 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', - }, - ], - }, - ], -}); +ruleTester.run( + 'no-useless-template-literals', + rule, + getNoUselessTemplateExpressionTestCases(), +); 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 893a6364f56b..d6a77338e630 100644 --- a/packages/typescript-eslint/src/configs/all.ts +++ b/packages/typescript-eslint/src/configs/all.ts @@ -121,7 +121,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 30187dd7f20a..34360db7cfbe 100644 --- a/packages/typescript-eslint/src/configs/disable-type-checked.ts +++ b/packages/typescript-eslint/src/configs/disable-type-checked.ts @@ -41,6 +41,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 ad8fb26fbfb6..da4d2ffc85f1 100644 --- a/packages/typescript-eslint/src/configs/strict-type-checked-only.ts +++ b/packages/typescript-eslint/src/configs/strict-type-checked-only.ts @@ -41,7 +41,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 ba6e92ae3565..8e24b4a8cb0b 100644 --- a/packages/typescript-eslint/src/configs/strict-type-checked.ts +++ b/packages/typescript-eslint/src/configs/strict-type-checked.ts @@ -68,7 +68,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', From 0bd126fb44810b731eb8f336019c40a07964a0e1 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger <53019676+kirkwaiblinger@users.noreply.github.com> Date: Sun, 14 Apr 2024 14:08:53 -0600 Subject: [PATCH 2/7] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- .../eslint-plugin/docs/rules/no-useless-template-literals.mdx | 2 +- .../eslint-plugin/src/rules/no-useless-template-expression.ts | 1 + .../eslint-plugin/src/rules/no-useless-template-literals.ts | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) 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 c0f82b01c148..feac2c62e632 100644 --- a/packages/eslint-plugin/docs/rules/no-useless-template-literals.mdx +++ b/packages/eslint-plugin/docs/rules/no-useless-template-literals.mdx @@ -15,7 +15,7 @@ This rule reports template literals that contain substitution expressions (also This rule is being renamed to [`no-useless-template-expression`](./no-useless-template-expression.mdx). 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 [`quotes`](./quotes.mdx) rule to do 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. The current name, `no-useless-template-literals`, will be removed in a future major version of typescript-eslint. ::: diff --git a/packages/eslint-plugin/src/rules/no-useless-template-expression.ts b/packages/eslint-plugin/src/rules/no-useless-template-expression.ts index 01e37f3fa213..85a9b090f1a0 100644 --- a/packages/eslint-plugin/src/rules/no-useless-template-expression.ts +++ b/packages/eslint-plugin/src/rules/no-useless-template-expression.ts @@ -189,6 +189,7 @@ export function makeNoUselessTemplateExpressionRuleObject(): NoUselessTemplateEx // and the deprecated version of this rule under its previous name // no-useless-template-literals. // See https://github.com/typescript-eslint/typescript-eslint/issues/8544 +TODO: Remove in v8 export default createRule<[], NoUselessTemplateExpressionMessageId>( makeNoUselessTemplateExpressionRuleObject(), ); 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 72a61d4c8025..39beb745d912 100644 --- a/packages/eslint-plugin/src/rules/no-useless-template-literals.ts +++ b/packages/eslint-plugin/src/rules/no-useless-template-literals.ts @@ -8,7 +8,7 @@ import { makeNoUselessTemplateExpressionRuleObject } from './no-useless-template // See https://github.com/typescript-eslint/typescript-eslint/issues/8544 const ruleObject = makeNoUselessTemplateExpressionRuleObject(); -// @ts-expect-error: easier than figuring out how to make this mutable. +// @ts-expect-error: easier than asserting this as mutable or deep object spreads. ruleObject.name = 'no-useless-template-literals'; ruleObject.meta.replacedBy = [ '@typescript-eslint/no-useless-template-expression', From 6420c69e50abeb294e26e6ffcd4a8e9fdd11517c Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Sun, 14 Apr 2024 14:23:22 -0600 Subject: [PATCH 3/7] fix syntax --- .../eslint-plugin/src/rules/no-useless-template-expression.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-template-expression.ts b/packages/eslint-plugin/src/rules/no-useless-template-expression.ts index 85a9b090f1a0..c90a93b7af8d 100644 --- a/packages/eslint-plugin/src/rules/no-useless-template-expression.ts +++ b/packages/eslint-plugin/src/rules/no-useless-template-expression.ts @@ -189,7 +189,7 @@ export function makeNoUselessTemplateExpressionRuleObject(): NoUselessTemplateEx // and the deprecated version of this rule under its previous name // no-useless-template-literals. // See https://github.com/typescript-eslint/typescript-eslint/issues/8544 -TODO: Remove in v8 +// TODO: Remove in v8 export default createRule<[], NoUselessTemplateExpressionMessageId>( makeNoUselessTemplateExpressionRuleObject(), ); From 9f56e21745020255d6723abd644049c3173b06a9 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Sun, 14 Apr 2024 14:40:35 -0600 Subject: [PATCH 4/7] follow up on confusing typed function issue --- .../src/rules/no-useless-template-expression.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-template-expression.ts b/packages/eslint-plugin/src/rules/no-useless-template-expression.ts index c90a93b7af8d..5c26c8650f67 100644 --- a/packages/eslint-plugin/src/rules/no-useless-template-expression.ts +++ b/packages/eslint-plugin/src/rules/no-useless-template-expression.ts @@ -19,9 +19,7 @@ export type NoUselessTemplateExpressionRuleObject = Parameters< >[0]; export function makeNoUselessTemplateExpressionRuleObject(): NoUselessTemplateExpressionRuleObject { - // For some reason, TS isn't totally happy if you return the object directly, - // but it doesn't mind if you assign it to a variable first. - const ruleObject: NoUselessTemplateExpressionRuleObject = { + return { name: 'no-useless-template-expression', meta: { fixable: 'code', @@ -38,6 +36,8 @@ export function makeNoUselessTemplateExpressionRuleObject(): NoUselessTemplateEx schema: [], }, defaultOptions: [], + // linter bug; see https://github.com/typescript-eslint/typescript-eslint/issues/8918 + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type create(context) { const services = getParserServices(context); @@ -181,8 +181,6 @@ export function makeNoUselessTemplateExpressionRuleObject(): NoUselessTemplateEx }; }, }; - - return ruleObject; } // The rule object factory is to support code sharing between the current rule, From 3c4ab99b8cae28f4aa7628a48c3053e9b4f33ed8 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Sun, 14 Apr 2024 14:59:19 -0600 Subject: [PATCH 5/7] copypasta approach --- .../rules/no-useless-template-expression.ts | 308 +++--- .../src/rules/no-useless-template-literals.ts | 196 +++- packages/eslint-plugin/tests/docs.test.ts | 2 - .../no-useless-template-expression.test.ts | 967 +++++++++--------- .../no-useless-template-literals.test.ts | 634 +++++++++++- 5 files changed, 1426 insertions(+), 681 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-useless-template-expression.ts b/packages/eslint-plugin/src/rules/no-useless-template-expression.ts index 5c26c8650f67..e7afc261e2fe 100644 --- a/packages/eslint-plugin/src/rules/no-useless-template-expression.ts +++ b/packages/eslint-plugin/src/rules/no-useless-template-expression.ts @@ -11,183 +11,165 @@ import { isUndefinedIdentifier, } from '../util'; -export type NoUselessTemplateExpressionMessageId = - 'noUselessTemplateExpression'; - -export type NoUselessTemplateExpressionRuleObject = Parameters< - typeof createRule<[], NoUselessTemplateExpressionMessageId> ->[0]; - -export function makeNoUselessTemplateExpressionRuleObject(): NoUselessTemplateExpressionRuleObject { - return { - 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: [], +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, }, - defaultOptions: [], - // linter bug; see https://github.com/typescript-eslint/typescript-eslint/issues/8918 - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - 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); - } + 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); + }; - return isString(type); + if (type.isUnion()) { + return type.types.every(isString); } - function isLiteral(expression: TSESTree.Expression): boolean { - return expression.type === AST_NODE_TYPES.Literal; + if (type.isIntersection()) { + return type.types.some(isString); } - function isTemplateLiteral(expression: TSESTree.Expression): boolean { - return expression.type === AST_NODE_TYPES.TemplateLiteral; - } + 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; + } - function isInfinityIdentifier(expression: TSESTree.Expression): boolean { - return ( - expression.type === AST_NODE_TYPES.Identifier && - expression.name === 'Infinity' - ); - } + 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, + ]), + ]; + }, + }); - function isNaNIdentifier(expression: TSESTree.Expression): boolean { - return ( - expression.type === AST_NODE_TYPES.Identifier && - expression.name === 'NaN' - ); - } + return; + } - 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], - ]), + 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([ - 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], + expression.range[0] + 1, ]), - fixer.removeRange([ + expression.range[1] - 1, 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; - }, - }); + ); + } + + return fixes; + }, }); - }, - }; - }, - }; -} - -// The rule object factory is to support code sharing between the current rule, -// and the deprecated version of this rule under its previous name -// no-useless-template-literals. -// See https://github.com/typescript-eslint/typescript-eslint/issues/8544 -// TODO: Remove in v8 -export default createRule<[], NoUselessTemplateExpressionMessageId>( - makeNoUselessTemplateExpressionRuleObject(), -); + }); + }, + }; + }, +}); 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 39beb745d912..9156c6e4053e 100644 --- a/packages/eslint-plugin/src/rules/no-useless-template-literals.ts +++ b/packages/eslint-plugin/src/rules/no-useless-template-literals.ts @@ -1,20 +1,176 @@ -import { createRule } from '../util'; -import type { NoUselessTemplateExpressionMessageId } from './no-useless-template-expression'; -import { makeNoUselessTemplateExpressionRuleObject } from './no-useless-template-expression'; - -// This rule was renamed to `no-useless-template-expression`. -// This module's purpose is just to import the code from the new implementation -// and adjust its metadata to account for the renaming. -// See https://github.com/typescript-eslint/typescript-eslint/issues/8544 - -const ruleObject = makeNoUselessTemplateExpressionRuleObject(); -// @ts-expect-error: easier than asserting this as mutable or deep object spreads. -ruleObject.name = 'no-useless-template-literals'; -ruleObject.meta.replacedBy = [ - '@typescript-eslint/no-useless-template-expression', -]; -ruleObject.meta.deprecated = true; -// not recommended anymore; the new rule is recommended instead. -delete ruleObject.meta.docs.recommended; - -export default createRule<[], NoUselessTemplateExpressionMessageId>(ruleObject); +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-literals', + meta: { + fixable: 'code', + type: 'suggestion', + docs: { + description: 'Disallow unnecessary template expressions', + requiresTypeChecking: true, + }, + messages: { + noUselessTemplateExpression: + 'Template literal expression is unnecessary and can be simplified.', + }, + schema: [], + deprecated: true, + replacedBy: ['@typescript-eslint/no-useless-template-expression'], + }, + 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/tests/docs.test.ts b/packages/eslint-plugin/tests/docs.test.ts index a79b9e938aea..c3f08de4bbda 100644 --- a/packages/eslint-plugin/tests/docs.test.ts +++ b/packages/eslint-plugin/tests/docs.test.ts @@ -439,8 +439,6 @@ describe('Validating rule metadata', () => { const rulesThatRequireTypeInformationInAWayThatsHardToDetect = new Set([ // the core rule file doesn't use type information, instead it's used in `src/rules/naming-convention-utils/validator.ts` 'naming-convention', - // rule moved to no-useless-template-expression. - 'no-useless-template-literals', ]); function requiresFullTypeInformation(content: string): boolean { return /getParserServices(\(\s*[^,\s)]+)\s*(,\s*false\s*)?\)/.test(content); 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 index 647c6b621131..628057ac7d17 100644 --- a/packages/eslint-plugin/tests/rules/no-useless-template-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-useless-template-expression.test.ts @@ -1,6 +1,5 @@ import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; -import type { NoUselessTemplateExpressionMessageId } from '../../src/rules/no-useless-template-expression'; import rule from '../../src/rules/no-useless-template-expression'; import { getFixturesRootDir } from '../RuleTester'; @@ -14,123 +13,114 @@ const ruleTester = new RuleTester({ }, }); -ruleTester.run( - 'no-useless-template-expression', - rule, - getNoUselessTemplateExpressionTestCases(), -); - -export function getNoUselessTemplateExpressionTestCases(): Parameters< - typeof ruleTester.run ->[2] { - return { - valid: [ - "const string = 'a';", - 'const string = `a`;', - ` +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}`;', + '`1 + 1 = ${1 + 1}`;', - '`true && false = ${true && false}`;', + '`true && false = ${true && false}`;', - "tag`${'a'}${'b'}`;", + "tag`${'a'}${'b'}`;", - '`${function () {}}`;', + '`${function () {}}`;', - '`${() => {}}`;', + '`${() => {}}`;', - '`${(...args: any[]) => args}`;', + '`${(...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 @@ -138,279 +128,279 @@ export function getNoUselessTemplateExpressionTestCases(): Parameters< new line\`; `, - noFormat` + 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: ` + ], + + 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: ` + 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: ` + 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: ` + output: ` declare const nested: string, interpolation: string; \`useless\${nested}\${interpolation}\`; `, - errors: [ - { - messageId: 'noUselessTemplateExpression', - }, - ], - }, - - { - code: noFormat` + errors: [ + { + messageId: 'noUselessTemplateExpression', + }, + ], + }, + + { + code: noFormat` \`u\${ // hopefully this comment is not needed. 'se' @@ -419,234 +409,233 @@ declare const nested: string, interpolation: string; \`le\${ \`ss\` }\` }\`; `, - output: ` + 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` + 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: ` + 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: ` + 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: ` + output: ` declare const string: 'a'; string; `, - errors: [ - { - messageId: 'noUselessTemplateExpression', - line: 3, - column: 12, - endColumn: 18, - }, - ], - }, - - { - code: noFormat` + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 3, + column: 12, + endColumn: 18, + }, + ], + }, + + { + code: noFormat` declare const string: 'a'; \`\${ string }\`; `, - output: ` + 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: ` + 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: ` + output: ` declare const intersection: string & { _brand: 'test-brand' }; intersection; `, - errors: [ - { - messageId: 'noUselessTemplateExpression', - line: 3, - column: 12, - endColumn: 24, - }, - ], - }, - - { - code: ` + errors: [ + { + messageId: 'noUselessTemplateExpression', + line: 3, + column: 12, + endColumn: 24, + }, + ], + }, + + { + code: ` function func(arg: T) { \`\${arg}\`; } `, - output: ` + 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', - }, - ], - }, - ], - }; -} + 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 5bc461968d39..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 @@ -1,8 +1,7 @@ -import { RuleTester } from '@typescript-eslint/rule-tester'; +import { noFormat, RuleTester } from '@typescript-eslint/rule-tester'; import rule from '../../src/rules/no-useless-template-literals'; import { getFixturesRootDir } from '../RuleTester'; -import { getNoUselessTemplateExpressionTestCases } from './no-useless-template-expression.test'; const rootPath = getFixturesRootDir(); @@ -14,8 +13,629 @@ const ruleTester = new RuleTester({ }, }); -ruleTester.run( - 'no-useless-template-literals', - rule, - getNoUselessTemplateExpressionTestCases(), -); +ruleTester.run('no-useless-template-literals', 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', + }, + ], + }, + ], +}); From 5431911b7413f6ddd15dca8da0f78d7845109d99 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Mon, 15 Apr 2024 11:07:02 -0600 Subject: [PATCH 6/7] pr feedback and other changes --- .../docs/rules/no-throw-literal.mdx | 6 +- .../rules/no-useless-template-expression.mdx | 13 ++++- .../rules/no-useless-template-literals.mdx | 4 +- .../docs/rules/only-throw-error.mdx | 9 +++ .../no-useless-template-expression.shot | 56 +++++++++++++++++++ 5 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-useless-template-expression.shot 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 index 972d47709739..2b6a28802b22 100644 --- a/packages/eslint-plugin/docs/rules/no-useless-template-expression.mdx +++ b/packages/eslint-plugin/docs/rules/no-useless-template-expression.mdx @@ -11,6 +11,15 @@ import TabItem from '@theme/TabItem'; 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 @@ -65,7 +74,7 @@ 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 [`quotes`](./quotes.mdx) rule to do 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 @@ -75,4 +84,4 @@ When you want to allow string expressions inside template literals. ## Related To - [`restrict-template-expressions`](./restrict-template-expressions.mdx) -- [`quotes`](./quotes.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 feac2c62e632..f02ec5ada9ec 100644 --- a/packages/eslint-plugin/docs/rules/no-useless-template-literals.mdx +++ b/packages/eslint-plugin/docs/rules/no-useless-template-literals.mdx @@ -13,11 +13,11 @@ This rule reports template literals that contain substitution expressions (also :::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. - -The current name, `no-useless-template-literals`, will be removed in a future major version of typescript-eslint. ::: {/* 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/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; +" +`; From e34749914006dad631fb18a6c1ba7af56cbe0677 Mon Sep 17 00:00:00 2001 From: Kirk Waiblinger Date: Sun, 5 May 2024 18:01:39 -0600 Subject: [PATCH 7/7] revert miscellaneous spelling fix --- packages/eslint-plugin/tests/docs.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/tests/docs.test.ts b/packages/eslint-plugin/tests/docs.test.ts index c3f08de4bbda..049c0d168e4f 100644 --- a/packages/eslint-plugin/tests/docs.test.ts +++ b/packages/eslint-plugin/tests/docs.test.ts @@ -448,7 +448,7 @@ describe('Validating rule metadata', () => { describe(ruleName, () => { it('`name` field in rule must match the filename', () => { // validate if rule name is same as url - // there is no way to access this field but it's used only in generation of docs url + // there is no way to access this field but its used only in generation of docs url expect(rule.meta.docs?.url).toBe( `https://typescript-eslint.io/rules/${ruleName}`, ); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy