diff --git a/packages/eslint-plugin/docs/rules/no-unnecessary-template-expression.mdx b/packages/eslint-plugin/docs/rules/no-unnecessary-template-expression.mdx index 1fe39d412dd1..c720652ebf7a 100644 --- a/packages/eslint-plugin/docs/rules/no-unnecessary-template-expression.mdx +++ b/packages/eslint-plugin/docs/rules/no-unnecessary-template-expression.mdx @@ -28,6 +28,8 @@ The new name is a drop-in replacement with identical functionality. const ab1 = `${'a'}${'b'}`; const ab2 = `a${'b'}`; +type AB1 = `${'A'}${'B'}`; +type AB2 = `A${'B'}`; const stringWithNumber = `${'1 + 1 = '}${2}`; @@ -38,9 +40,13 @@ const stringWithBoolean = `${'true is '}${true}`; const text = 'a'; const wrappedText = `${text}`; +type Text = 'A'; +type WrappedText = `${Text}`; declare const intersectionWithString: string & { _brand: 'test-brand' }; const wrappedIntersection = `${intersectionWithString}`; +type IntersectionWithString = string & { _brand: 'test-brand' }; +type WrappedIntersection = `${IntersectionWithString}`; ``` @@ -51,6 +57,15 @@ const wrappedIntersection = `${intersectionWithString}`; const ab1 = `ab`; const ab2 = `ab`; +type AB = `AB`; + +// Transforming enum members into string unions using template literals is allowed. +enum ABC { + A = 'A', + B = 'B', + C = 'C', +} +type ABCUnion = `${ABC}`; const stringWithNumber = `1 + 1 = 2`; @@ -61,9 +76,13 @@ const stringWithBoolean = `true is true`; const text = 'a'; const wrappedText = text; +type Text = 'A'; +type WrappedText = Text; declare const intersectionWithString: string & { _brand: 'test-brand' }; const wrappedIntersection = intersectionWithString; +type IntersectionWithString = string & { _brand: 'test-brand' }; +type WrappedIntersection = IntersectionWithString; ``` diff --git a/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts b/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts index fde1ea99e780..308855686d79 100644 --- a/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts +++ b/packages/eslint-plugin/src/rules/no-unnecessary-template-expression.ts @@ -1,6 +1,6 @@ -import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import type { TSESLint } from '@typescript-eslint/utils'; -import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as ts from 'typescript'; import { @@ -8,6 +8,7 @@ import { getConstraintInfo, getMovedNodeCode, getParserServices, + isNodeOfType, isTypeFlagSet, isUndefinedIdentifier, nullThrows, @@ -17,6 +18,16 @@ import { rangeToLoc } from '../util/rangeToLoc'; export type MessageId = 'noUnnecessaryTemplateExpression'; +type TemplateLiteralTypeOrValue = + | TSESTree.TemplateLiteral + | TSESTree.TSTemplateLiteralType; + +interface InterpolationInfo { + interpolation: TSESTree.Expression | TSESTree.TypeNode; + prevQuasi: TSESTree.TemplateElement; + nextQuasi: TSESTree.TemplateElement; +} + const evenNumOfBackslashesRegExp = /(?({ defaultOptions: [], create(context) { const services = getParserServices(context); + const checker = services.program.getTypeChecker(); - function isUnderlyingTypeString( - expression: TSESTree.Expression, - ): expression is TSESTree.Identifier | TSESTree.StringLiteral { - const checker = services.program.getTypeChecker(); - const { constraintType } = getConstraintInfo( - checker, - services.getTypeAtLocation(expression), - ); - - if (constraintType == null) { - return false; - } - - const isString = (t: ts.Type): boolean => { - return isTypeFlagSet(t, ts.TypeFlags.StringLike); - }; + function isStringLike(type: ts.Type): boolean { + return isTypeFlagSet(type, ts.TypeFlags.StringLike); + } - if (constraintType.isUnion()) { - return constraintType.types.every(isString); + function isUnderlyingTypeString(type: ts.Type): boolean { + if (type.isUnion()) { + return type.types.every(isStringLike); } - if (constraintType.isIntersection()) { - return constraintType.types.some(isString); + if (type.isIntersection()) { + return type.types.some(isStringLike); } - return isString(constraintType); + return isStringLike(type); } - function isLiteral( - expression: TSESTree.Expression, - ): expression is TSESTree.Literal { - return expression.type === AST_NODE_TYPES.Literal; + /** + * Checks for whole enum types, i.e. `MyEnum`, and not their values, i.e. `MyEnum.A` + */ + function isEnumType(type: ts.Type): boolean { + const symbol = type.getSymbol(); + + return !!( + symbol?.valueDeclaration && + ts.isEnumDeclaration(symbol.valueDeclaration) + ); } + const isLiteral = isNodeOfType(TSESTree.AST_NODE_TYPES.Literal); + function isTemplateLiteral( - expression: TSESTree.Expression, - ): expression is TSESTree.TemplateLiteral { - return expression.type === AST_NODE_TYPES.TemplateLiteral; + node: TSESTree.Node, + ): node is TSESTree.TemplateLiteral { + return node.type === AST_NODE_TYPES.TemplateLiteral; } - function isInfinityIdentifier(expression: TSESTree.Expression): boolean { + function isInfinityIdentifier(node: TSESTree.Node): boolean { return ( - expression.type === AST_NODE_TYPES.Identifier && - expression.name === 'Infinity' + node.type === AST_NODE_TYPES.Identifier && node.name === 'Infinity' ); } - function isNaNIdentifier(expression: TSESTree.Expression): boolean { + function isNaNIdentifier(node: TSESTree.Node): boolean { + return node.type === AST_NODE_TYPES.Identifier && node.name === 'NaN'; + } + + function isFixableIdentifier(node: TSESTree.Node): boolean { return ( - expression.type === AST_NODE_TYPES.Identifier && - expression.name === 'NaN' + isUndefinedIdentifier(node) || + isInfinityIdentifier(node) || + isNaNIdentifier(node) ); } @@ -118,220 +130,348 @@ export default createRule<[], MessageId>({ return context.sourceCode.commentsExistBetween(startToken, endToken); } - return { - TemplateLiteral(node: TSESTree.TemplateLiteral): void { - if (node.parent.type === AST_NODE_TYPES.TaggedTemplateExpression) { - return; - } + function isTrivialInterpolation( + node: TSESTree.TemplateLiteral | TSESTree.TSTemplateLiteralType, + ) { + return ( + node.quasis.length === 2 && + node.quasis[0].value.raw === '' && + node.quasis[1].value.raw === '' + ); + } - const hasSingleStringVariable = - node.quasis.length === 2 && - node.quasis[0].value.raw === '' && - node.quasis[1].value.raw === '' && - node.expressions.length === 1 && - isUnderlyingTypeString(node.expressions[0]); + function getInterpolations( + node: TemplateLiteralTypeOrValue, + ): TSESTree.Expression[] | TSESTree.TypeNode[] { + if (node.type === AST_NODE_TYPES.TemplateLiteral) { + return node.expressions; + } + return node.types; + } - if (hasSingleStringVariable) { - if (hasCommentsBetweenQuasi(node.quasis[0], node.quasis[1])) { - return; - } + function getInterpolationInfos( + node: TemplateLiteralTypeOrValue, + ): InterpolationInfo[] { + return getInterpolations(node).map((interpolation, index) => ({ + interpolation, + nextQuasi: node.quasis[index + 1], + prevQuasi: node.quasis[index], + })); + } - context.report({ - loc: rangeToLoc(context.sourceCode, [ - node.expressions[0].range[0] - 2, - node.expressions[0].range[1] + 1, - ]), - messageId: 'noUnnecessaryTemplateExpression', - fix(fixer): TSESLint.RuleFix | null { - const wrappingCode = getMovedNodeCode({ - destinationNode: node, - nodeToMove: node.expressions[0], - sourceCode: context.sourceCode, - }); - - return fixer.replaceText(node, wrappingCode); - }, + function getLiteral( + node: TSESTree.Expression | TSESTree.TypeNode, + ): TSESTree.Literal | null { + const maybeLiteral = + node.type === AST_NODE_TYPES.TSLiteralType ? node.literal : node; + return isLiteral(maybeLiteral) ? maybeLiteral : null; + } + + function getTemplateLiteral( + node: TSESTree.Expression | TSESTree.TypeNode, + ): TSESTree.TemplateLiteral | null { + const maybeTemplateLiteral = + node.type === AST_NODE_TYPES.TSLiteralType ? node.literal : node; + return isTemplateLiteral(maybeTemplateLiteral) + ? maybeTemplateLiteral + : null; + } + + function reportSingleInterpolation(node: TemplateLiteralTypeOrValue): void { + const interpolations = getInterpolations(node); + context.report({ + loc: rangeToLoc(context.sourceCode, [ + interpolations[0].range[0] - 2, + interpolations[0].range[1] + 1, + ]), + messageId: 'noUnnecessaryTemplateExpression', + fix(fixer): TSESLint.RuleFix | null { + const wrappingCode = getMovedNodeCode({ + destinationNode: node, + nodeToMove: interpolations[0], + sourceCode: context.sourceCode, }); - return; + return fixer.replaceText(node, wrappingCode); + }, + }); + } + + function isUnncessaryValueInterpolation({ + interpolation, + nextQuasi, + prevQuasi, + }: InterpolationInfo): boolean { + if (hasCommentsBetweenQuasi(prevQuasi, nextQuasi)) { + return false; + } + + if (isFixableIdentifier(interpolation)) { + return true; + } + + if (isLiteral(interpolation)) { + // allow trailing whitespace literal + if (startsWithNewLine(nextQuasi.value.raw)) { + return !( + typeof interpolation.value === 'string' && + isWhitespace(interpolation.value) + ); } + return true; + } - const fixableExpressionsReversed = node.expressions - .map((expression, index) => ({ - expression, - nextQuasi: node.quasis[index + 1], - prevQuasi: node.quasis[index], - })) - .filter(({ expression, nextQuasi, prevQuasi }) => { - if ( - isUndefinedIdentifier(expression) || - isInfinityIdentifier(expression) || - isNaNIdentifier(expression) - ) { - return true; - } - - // allow expressions that include comments - if (hasCommentsBetweenQuasi(prevQuasi, nextQuasi)) { - return false; - } - - if (isLiteral(expression)) { - // allow trailing whitespace literal - if (startsWithNewLine(nextQuasi.value.raw)) { - return !( - typeof expression.value === 'string' && - isWhitespace(expression.value) - ); - } - return true; - } - - if (isTemplateLiteral(expression)) { - // allow trailing whitespace literal - if (startsWithNewLine(nextQuasi.value.raw)) { - return !( - expression.quasis.length === 1 && - isWhitespace(expression.quasis[0].value.raw) - ); - } - return true; - } - - return false; - }) - .reverse(); - - let nextCharacterIsOpeningCurlyBrace = false; - - for (const { - expression, - nextQuasi, - prevQuasi, - } of fixableExpressionsReversed) { - const fixers: ((fixer: TSESLint.RuleFixer) => TSESLint.RuleFix[])[] = - []; - - if (nextQuasi.value.raw !== '') { - nextCharacterIsOpeningCurlyBrace = - nextQuasi.value.raw.startsWith('{'); - } + if (isTemplateLiteral(interpolation)) { + // allow trailing whitespace literal + if (startsWithNewLine(nextQuasi.value.raw)) { + return !( + interpolation.quasis.length === 1 && + isWhitespace(interpolation.quasis[0].value.raw) + ); + } + return true; + } - if (isLiteral(expression)) { - let escapedValue = ( - typeof expression.value === 'string' - ? // The value is already a string, so we're removing quotes: - // "'va`lue'" -> "va`lue" - expression.raw.slice(1, -1) - : // The value may be one of number | bigint | boolean | RegExp | null. - // In regular expressions, we escape every backslash - String(expression.value).replaceAll('\\', '\\\\') - ) - // The string or RegExp may contain ` or ${. - // We want both of these to be escaped in the final template expression. - // - // A pair of backslashes means "escaped backslash", so backslashes - // from this pair won't escape ` or ${. Therefore, to escape these - // sequences in the resulting template expression, we need to escape - // all sequences that are preceded by an even number of backslashes. - // - // This RegExp does the following transformations: - // \` -> \` - // \\` -> \\\` - // \${ -> \${ - // \\${ -> \\\${ - .replaceAll( - new RegExp( - `${String(evenNumOfBackslashesRegExp.source)}(\`|\\\${)`, - 'g', - ), - '\\$1', - ); - - // `...${'...$'}{...` - // ^^^^ - if ( - nextCharacterIsOpeningCurlyBrace && - endsWithUnescapedDollarSign(escapedValue) - ) { - escapedValue = escapedValue.replaceAll(/\$$/g, '\\$'); - } - - if (escapedValue.length !== 0) { - nextCharacterIsOpeningCurlyBrace = escapedValue.startsWith('{'); - } - - fixers.push(fixer => [fixer.replaceText(expression, escapedValue)]); - } else if (isTemplateLiteral(expression)) { - // Since we iterate from the last expression to the first, - // a subsequent expression can tell the current expression - // that it starts with {. + return false; + } + + function isUnncessaryTypeInterpolation({ + interpolation, + nextQuasi, + prevQuasi, + }: InterpolationInfo): boolean { + if (hasCommentsBetweenQuasi(prevQuasi, nextQuasi)) { + return false; + } + + const literal = getLiteral(interpolation); + if (literal) { + // allow trailing whitespace literal + if (startsWithNewLine(nextQuasi.value.raw)) { + return !( + typeof literal.value === 'string' && isWhitespace(literal.value) + ); + } + return true; + } + + if ( + interpolation.type === AST_NODE_TYPES.TSNullKeyword || + interpolation.type === AST_NODE_TYPES.TSUndefinedKeyword + ) { + return true; + } + + const templateLiteral = getTemplateLiteral(interpolation); + if (templateLiteral) { + // allow trailing whitespace literal + if (startsWithNewLine(nextQuasi.value.raw)) { + return !( + templateLiteral.quasis.length === 1 && + isWhitespace(templateLiteral.quasis[0].value.raw) + ); + } + return true; + } + + return false; + } + + function getReportDescriptors( + infos: InterpolationInfo[], + ): TSESLint.ReportDescriptor[] { + let nextCharacterIsOpeningCurlyBrace = false; + const reportDescriptors: TSESLint.ReportDescriptor[] = []; + const reversedInfos = [...infos].reverse(); + for (const { interpolation, nextQuasi, prevQuasi } of reversedInfos) { + const fixers: ((fixer: TSESLint.RuleFixer) => TSESLint.RuleFix[])[] = + []; + + if (nextQuasi.value.raw !== '') { + nextCharacterIsOpeningCurlyBrace = + nextQuasi.value.raw.startsWith('{'); + } + + const literal = getLiteral(interpolation); + const templateLiteral = getTemplateLiteral(interpolation); + if (literal) { + let escapedValue = ( + typeof literal.value === 'string' + ? // The value is already a string, so we're removing quotes: + // "'va`lue'" -> "va`lue" + literal.raw.slice(1, -1) + : // The value may be one of number | bigint | boolean | RegExp | null. + // In regular expressions, we escape every backslash + String(literal.value).replaceAll('\\', '\\\\') + ) + // The string or RegExp may contain ` or ${. + // We want both of these to be escaped in the final template expression. // - // `... ${`... $`}${'{...'} ...` - // ^ ^ subsequent expression starts with { - // current expression ends with a dollar sign, - // so '$' + '{' === '${' (bad news for us). - // Let's escape the dollar sign at the end. - if ( - nextCharacterIsOpeningCurlyBrace && - endsWithUnescapedDollarSign( - expression.quasis[expression.quasis.length - 1].value.raw, - ) - ) { - fixers.push(fixer => [ - fixer.replaceTextRange( - [expression.range[1] - 2, expression.range[1] - 2], - '\\', - ), - ]); - } - if ( - expression.quasis.length === 1 && - expression.quasis[0].value.raw.length !== 0 - ) { - nextCharacterIsOpeningCurlyBrace = - expression.quasis[0].value.raw.startsWith('{'); - } - - // Remove the beginning and trailing backtick characters. - fixers.push(fixer => [ - fixer.removeRange([expression.range[0], expression.range[0] + 1]), - fixer.removeRange([expression.range[1] - 1, expression.range[1]]), - ]); - } else { - nextCharacterIsOpeningCurlyBrace = false; + // A pair of backslashes means "escaped backslash", so backslashes + // from this pair won't escape ` or ${. Therefore, to escape these + // sequences in the resulting template expression, we need to escape + // all sequences that are preceded by an even number of backslashes. + // + // This RegExp does the following transformations: + // \` -> \` + // \\` -> \\\` + // \${ -> \${ + // \\${ -> \\\${ + .replaceAll( + new RegExp( + `${String(evenNumOfBackslashesRegExp.source)}(\`|\\\${)`, + 'g', + ), + '\\$1', + ); + + // `...${'...$'}{...` + // ^^^^ + if ( + nextCharacterIsOpeningCurlyBrace && + endsWithUnescapedDollarSign(escapedValue) + ) { + escapedValue = escapedValue.replaceAll(/\$$/g, '\\$'); } - // `... $${'{...'} ...` - // ^^^^^ + if (escapedValue.length !== 0) { + nextCharacterIsOpeningCurlyBrace = escapedValue.startsWith('{'); + } + + fixers.push(fixer => [fixer.replaceText(literal, escapedValue)]); + } else if (templateLiteral) { + // Since we iterate from the last expression to the first, + // a subsequent expression can tell the current expression + // that it starts with {. + // + // `... ${`... $`}${'{...'} ...` + // ^ ^ subsequent expression starts with { + // current expression ends with a dollar sign, + // so '$' + '{' === '${' (bad news for us). + // Let's escape the dollar sign at the end. if ( nextCharacterIsOpeningCurlyBrace && - endsWithUnescapedDollarSign(prevQuasi.value.raw) + endsWithUnescapedDollarSign( + templateLiteral.quasis[templateLiteral.quasis.length - 1].value + .raw, + ) ) { fixers.push(fixer => [ fixer.replaceTextRange( - [prevQuasi.range[1] - 3, prevQuasi.range[1] - 2], - '\\$', + [templateLiteral.range[1] - 2, templateLiteral.range[1] - 2], + '\\', ), ]); } + if ( + templateLiteral.quasis.length === 1 && + templateLiteral.quasis[0].value.raw.length !== 0 + ) { + nextCharacterIsOpeningCurlyBrace = + templateLiteral.quasis[0].value.raw.startsWith('{'); + } - const warnLocStart = prevQuasi.range[1] - 2; - const warnLocEnd = nextQuasi.range[0] + 1; - - context.report({ - loc: rangeToLoc(context.sourceCode, [warnLocStart, warnLocEnd]), - messageId: 'noUnnecessaryTemplateExpression', - fix(fixer): TSESLint.RuleFix[] { - return [ - // Remove the quasis' parts that are related to the current expression. - fixer.removeRange([warnLocStart, expression.range[0]]), - fixer.removeRange([expression.range[1], warnLocEnd]), - - ...fixers.flatMap(cb => cb(fixer)), - ]; - }, - }); + // Remove the beginning and trailing backtick characters. + fixers.push(fixer => [ + fixer.removeRange([ + templateLiteral.range[0], + templateLiteral.range[0] + 1, + ]), + fixer.removeRange([ + templateLiteral.range[1] - 1, + templateLiteral.range[1], + ]), + ]); + } else { + nextCharacterIsOpeningCurlyBrace = false; + } + + // `... $${'{...'} ...` + // ^^^^^ + if ( + nextCharacterIsOpeningCurlyBrace && + endsWithUnescapedDollarSign(prevQuasi.value.raw) + ) { + fixers.push(fixer => [ + fixer.replaceTextRange( + [prevQuasi.range[1] - 3, prevQuasi.range[1] - 2], + '\\$', + ), + ]); + } + + const warnLocStart = prevQuasi.range[1] - 2; + const warnLocEnd = nextQuasi.range[0] + 1; + reportDescriptors.push({ + loc: rangeToLoc(context.sourceCode, [warnLocStart, warnLocEnd]), + messageId: 'noUnnecessaryTemplateExpression', + fix(fixer): TSESLint.RuleFix[] { + return [ + // Remove the quasis' parts that are related to the current expression. + fixer.removeRange([warnLocStart, interpolation.range[0]]), + fixer.removeRange([interpolation.range[1], warnLocEnd]), + + ...fixers.flatMap(cb => cb(fixer)), + ]; + }, + }); + } + return reportDescriptors; + } + + return { + TemplateLiteral(node: TSESTree.TemplateLiteral): void { + if (node.parent.type === AST_NODE_TYPES.TaggedTemplateExpression) { + return; + } + if ( + isTrivialInterpolation(node) && + !hasCommentsBetweenQuasi(node.quasis[0], node.quasis[1]) + ) { + const { constraintType } = getConstraintInfo( + checker, + services.getTypeAtLocation(node.expressions[0]), + ); + if (constraintType && isUnderlyingTypeString(constraintType)) { + reportSingleInterpolation(node); + return; + } + } + + const infos = getInterpolationInfos(node).filter( + isUnncessaryValueInterpolation, + ); + + for (const reportDescriptor of getReportDescriptors(infos)) { + context.report(reportDescriptor); + } + }, + TSTemplateLiteralType(node: TSESTree.TSTemplateLiteralType): void { + if ( + isTrivialInterpolation(node) && + !hasCommentsBetweenQuasi(node.quasis[0], node.quasis[1]) + ) { + const { constraintType } = getConstraintInfo( + checker, + services.getTypeAtLocation(node.types[0]), + ); + + if ( + constraintType && + isUnderlyingTypeString(constraintType) && + !isEnumType(constraintType) + ) { + reportSingleInterpolation(node); + return; + } + } + + const infos = getInterpolationInfos(node).filter( + isUnncessaryTypeInterpolation, + ); + + for (const reportDescriptor of getReportDescriptors(infos)) { + context.report(reportDescriptor); } }, }; diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-template-expression.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-template-expression.shot index 47aa863d7203..0065786d6ef9 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-template-expression.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unnecessary-template-expression.shot @@ -10,6 +10,11 @@ const ab1 = \`\${'a'}\${'b'}\`; ~~~~~~ Template literal expression is unnecessary and can be simplified. const ab2 = \`a\${'b'}\`; ~~~~~~ Template literal expression is unnecessary and can be simplified. +type AB1 = \`\${'A'}\${'B'}\`; + ~~~~~~ Template literal expression is unnecessary and can be simplified. + ~~~~~~ Template literal expression is unnecessary and can be simplified. +type 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. @@ -25,10 +30,16 @@ const stringWithBoolean = \`\${'true is '}\${true}\`; const text = 'a'; const wrappedText = \`\${text}\`; ~~~~~~~ Template literal expression is unnecessary and can be simplified. +type Text = 'A'; +type 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. +type IntersectionWithString = string & { _brand: 'test-brand' }; +type WrappedIntersection = \`\${IntersectionWithString}\`; + ~~~~~~~~~~~~~~~~~~~~~~~~~ Template literal expression is unnecessary and can be simplified. " `; @@ -39,6 +50,15 @@ exports[`Validating rule docs no-unnecessary-template-expression.mdx code exampl const ab1 = \`ab\`; const ab2 = \`ab\`; +type AB = \`AB\`; + +// Transforming enum members into string unions using template literals is allowed. +enum ABC { + A = 'A', + B = 'B', + C = 'C', +} +type ABCUnion = \`\${ABC}\`; const stringWithNumber = \`1 + 1 = 2\`; @@ -49,8 +69,12 @@ const stringWithBoolean = \`true is true\`; const text = 'a'; const wrappedText = text; +type Text = 'A'; +type WrappedText = Text; declare const intersectionWithString: string & { _brand: 'test-brand' }; const wrappedIntersection = intersectionWithString; +type IntersectionWithString = string & { _brand: 'test-brand' }; +type WrappedIntersection = IntersectionWithString; " `; diff --git a/packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts b/packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts index eb87d7f3755d..160c9114f2aa 100644 --- a/packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unnecessary-template-expression.test.ts @@ -976,6 +976,9 @@ ruleTester.run('no-unnecessary-template-expression', rule, { valid: [ "const string = 'a';", 'const string = `a`;', + 'const string = `NaN: ${/* comment */ NaN}`;', + 'const string = `undefined: ${/* comment */ undefined}`;', + 'const string = `Infinity: ${Infinity /* comment */}`;', ` declare const string: 'a'; \`\${string}b\`; @@ -1097,6 +1100,11 @@ ruleTester.run('no-unnecessary-template-expression', rule, { ` \` this code has trailing whitespace: \${' '} + \`; + `, + ` +\` +this code has trailing whitespace: \${\` \`} \`; `, noFormat` @@ -1147,6 +1155,41 @@ this code has trailing whitespace: \${' '} return \`\${input}\`; } `, + ` +type FooBarBaz = \`foo\${/* comment */ 'bar'}"baz"\`; + `, + ` +enum Foo { + A = 'A', + B = 'B', +} +type Foos = \`\${Foo}\`; + `, + ` +type Foo = 'A' | 'B'; +type Bar = \`foo\${Foo}foo\`; + `, + ` +type Foo = + \`trailing position interpolated empty string also makes whitespace clear \${''} +\`; + `, + noFormat` +type Foo = \`this code has trailing whitespace with a windows \\\r new line: \${\` \`}\r\n\`; + `, + "type Foo = `${'foo' | 'bar' | null}`;", + + ` +type StringOrNumber = string | number; +type Foo = \`\${StringOrNumber}\`; + `, + ` +enum Foo { + A = 1, + B = 2, +} +type Bar = \`\${Foo.A}\`; + `, ], invalid: [ @@ -1264,5 +1307,150 @@ declare const nested: string, interpolation: string; ], output: "true ? ('test' || '').trim() : undefined;", }, + { + code: 'type Foo = `${1}`;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + output: 'type Foo = `1`;', + }, + { + code: 'type Foo = `${null}`;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + output: 'type Foo = `null`;', + }, + { + code: 'type Foo = `${undefined}`;', + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + output: 'type Foo = `undefined`;', + }, + { + code: "type Foo = `${'foo'}`;", + errors: [{ messageId: 'noUnnecessaryTemplateExpression' }], + output: "type Foo = 'foo';", + }, + { + code: ` +type Foo = 'A' | 'B'; +type Bar = \`\${Foo}\`; + `, + errors: [ + { + column: 13, + endColumn: 19, + endLine: 3, + line: 3, + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + output: ` +type Foo = 'A' | 'B'; +type Bar = Foo; + `, + }, + { + code: ` +type Foo = 'A' | 'B'; +type Bar = \`\${\`\${Foo}\`}\`; + `, + errors: [ + { + column: 13, + endColumn: 24, + endLine: 3, + line: 3, + messageId: 'noUnnecessaryTemplateExpression', + }, + { + column: 16, + endColumn: 22, + endLine: 3, + line: 3, + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + output: [ + ` +type Foo = 'A' | 'B'; +type Bar = \`\${Foo}\`; + `, + + ` +type Foo = 'A' | 'B'; +type Bar = Foo; + `, + ], + }, + { + code: "type FooBarBaz = `foo${'bar'}baz`;", + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + output: 'type FooBarBaz = `foobarbaz`;', + }, + { + code: 'type FooBar = `foo${`bar`}`;', + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + output: 'type FooBar = `foobar`;', + }, + { + code: "type FooBar = `${'foo' | 'bar'}`;", + errors: [ + { + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + output: "type FooBar = 'foo' | 'bar';", + }, + { + code: ` +enum Foo { + A = 'A', + B = 'B', +} +type Bar = \`\${Foo.A}\`; + `, + errors: [ + { + column: 13, + endColumn: 21, + endLine: 6, + line: 6, + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + output: ` +enum Foo { + A = 'A', + B = 'B', +} +type Bar = Foo.A; + `, + }, + { + code: ` +function foo() { + const a: \`\${T}\` = 'a'; +} + `, + errors: [ + { + column: 13, + endColumn: 17, + endLine: 3, + line: 3, + messageId: 'noUnnecessaryTemplateExpression', + }, + ], + output: ` +function foo() { + const a: T = 'a'; +} + `, + }, ], }); 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