diff --git a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts index 069388b7eb0..e9b1069d260 100644 --- a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts +++ b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts @@ -1,12 +1,13 @@ import type { TSESTree } from '@typescript-eslint/utils'; -import type { Type, TypeChecker } from 'typescript'; +import type { InterfaceType, Type, TypeChecker } from 'typescript'; import { typeMatchesSomeSpecifier, typeOrValueSpecifiersSchema, } from '@typescript-eslint/type-utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; -import { TypeFlags } from 'typescript'; +import { isObjectFlagSet, isObjectType } from 'ts-api-utils'; +import { ObjectFlags, TypeFlags } from 'typescript'; import type { TypeOrValueSpecifier } from '../util'; @@ -130,6 +131,33 @@ export default createRule({ ({ option }) => options[option], ); + function hasBaseTypes(type: Type): type is InterfaceType { + return ( + isObjectType(type) && + isObjectFlagSet(type, ObjectFlags.Interface | ObjectFlags.Class) + ); + } + + function isAllowedTypeOrBase(type: Type, seen = new Set()): boolean { + if (seen.has(type)) { + return false; + } + + seen.add(type); + + if (typeMatchesSomeSpecifier(type, allow, program)) { + return true; + } + + if (hasBaseTypes(type)) { + return checker + .getBaseTypes(type) + .some(base => isAllowedTypeOrBase(base, seen)); + } + + return false; + } + return { TemplateLiteral(node: TSESTree.TemplateLiteral): void { // don't check tagged template literals @@ -165,7 +193,7 @@ export default createRule({ return ( isTypeFlagSet(innerType, TypeFlags.StringLike) || - typeMatchesSomeSpecifier(innerType, allow, program) || + isAllowedTypeOrBase(innerType) || enabledOptionTesters.some(({ tester }) => tester(innerType, checker, recursivelyCheckType), ) diff --git a/packages/eslint-plugin/tests/rules/restrict-template-expressions.test.ts b/packages/eslint-plugin/tests/rules/restrict-template-expressions.test.ts index aa88c8276cd..ac4fcd99f59 100644 --- a/packages/eslint-plugin/tests/rules/restrict-template-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/restrict-template-expressions.test.ts @@ -344,6 +344,52 @@ ruleTester.run('restrict-template-expressions', rule, { 'const msg = `arg = ${undefined}`;', 'const msg = `arg = ${123}`;', "const msg = `arg = ${'abc'}`;", + // https://github.com/typescript-eslint/typescript-eslint/issues/11759 + // allow should check base types + { + code: ` + class Base {} + class Derived extends Base {} + const foo = new Base(); + const bar = new Derived(); + \`\${foo}\${bar}\`; + `, + options: [{ allow: [{ from: 'file', name: 'Base' }] }], + }, + // allow should check base types - multi-level inheritance + { + code: ` + class Base {} + class Derived extends Base {} + class DerivedTwice extends Derived {} + const value = new DerivedTwice(); + \`\${value}\`; + `, + options: [{ allow: [{ from: 'file', name: 'Base' }] }], + }, + // allow should check base types - interface inheritance + { + code: ` + interface Base { + value: string; + } + interface Derived extends Base { + extra: number; + } + declare const obj: Derived; + \`\${obj}\`; + `, + options: [{ allow: [{ from: 'file', name: 'Base' }] }], + }, + // allow list with type alias without base types + { + code: ` + type Custom = { value: string }; + declare const obj: Custom; + \`\${obj}\`; + `, + options: [{ allow: [{ from: 'file', name: 'Custom' }] }], + }, ], invalid: [ @@ -614,5 +660,42 @@ ruleTester.run('restrict-template-expressions', rule, { ], options: [{ allowAny: true }], }, + // https://github.com/typescript-eslint/typescript-eslint/issues/11759 + // derived type should error when base type is not in allow list + { + code: ` + class Base {} + class Derived extends Base {} + const bar = new Derived(); + \`\${bar}\`; + `, + errors: [ + { + data: { type: 'Derived' }, + messageId: 'invalidType', + }, + ], + options: [{ allow: [] }], + }, + // derived interface should error when base type is not in allow list + { + code: ` + interface Base { + value: string; + } + interface Derived extends Base { + extra: number; + } + declare const obj: Derived; + \`\${obj}\`; + `, + errors: [ + { + data: { type: 'Derived' }, + messageId: 'invalidType', + }, + ], + options: [{ allow: [] }], + }, ], });