diff --git a/packages/eslint-plugin/docs/rules/restrict-template-expressions.md b/packages/eslint-plugin/docs/rules/restrict-template-expressions.md index 816a0b0f9d6..4bb2a062ad9 100644 --- a/packages/eslint-plugin/docs/rules/restrict-template-expressions.md +++ b/packages/eslint-plugin/docs/rules/restrict-template-expressions.md @@ -40,6 +40,7 @@ type Options = { const defaults = { allowNumber: false, allowBoolean: false, + allowAny: false, allowNullable: false, }; ``` diff --git a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts index 90dd363f820..6ba846ed598 100644 --- a/packages/eslint-plugin/src/rules/restrict-template-expressions.ts +++ b/packages/eslint-plugin/src/rules/restrict-template-expressions.ts @@ -27,7 +27,7 @@ export default util.createRule({ requiresTypeChecking: true, }, messages: { - invalidType: 'Invalid type of template literal expression.', + invalidType: 'Invalid type "{{type}}" of template literal expression.', }, schema: [ { @@ -90,46 +90,44 @@ export default util.createRule({ } for (const expression of node.expressions) { + const expressionType = util.getConstrainedTypeAtLocation( + typeChecker, + service.esTreeNodeToTSNodeMap.get(expression), + ); + if ( - !isUnderlyingExpressionTypeConfirmingTo( - expression, + !isInnerUnionOrIntersectionConformingTo( + expressionType, isUnderlyingTypePrimitive, ) ) { context.report({ node: expression, messageId: 'invalidType', + data: { type: typeChecker.typeToString(expressionType) }, }); } } }, }; - function isUnderlyingExpressionTypeConfirmingTo( - expression: TSESTree.Expression, + function isInnerUnionOrIntersectionConformingTo( + type: ts.Type, predicate: (underlyingType: ts.Type) => boolean, ): boolean { - return rec(getExpressionNodeType(expression)); + return rec(type); - function rec(type: ts.Type): boolean { - if (type.isUnion()) { - return type.types.every(rec); + function rec(innerType: ts.Type): boolean { + if (innerType.isUnion()) { + return innerType.types.every(rec); } - if (type.isIntersection()) { - return type.types.some(rec); + if (innerType.isIntersection()) { + return innerType.types.some(rec); } - return predicate(type); + return predicate(innerType); } } - - /** - * Helper function to extract the TS type of an TSESTree expression. - */ - function getExpressionNodeType(node: TSESTree.Expression): ts.Type { - const tsNode = service.esTreeNodeToTSNodeMap.get(node); - return util.getConstrainedTypeAtLocation(typeChecker, tsNode); - } }, }); 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 2f5900f3d2a..9b991053dcb 100644 --- a/packages/eslint-plugin/tests/rules/restrict-template-expressions.test.ts +++ b/packages/eslint-plugin/tests/rules/restrict-template-expressions.test.ts @@ -214,33 +214,68 @@ ruleTester.run('restrict-template-expressions', rule, { code: ` const msg = \`arg = \${123}\`; `, - errors: [{ messageId: 'invalidType', line: 2, column: 30 }], + errors: [ + { + messageId: 'invalidType', + data: { type: '123' }, + line: 2, + column: 30, + }, + ], }, { code: ` const msg = \`arg = \${false}\`; `, - errors: [{ messageId: 'invalidType', line: 2, column: 30 }], + errors: [ + { + messageId: 'invalidType', + data: { type: 'false' }, + line: 2, + column: 30, + }, + ], }, { code: ` const msg = \`arg = \${null}\`; `, - errors: [{ messageId: 'invalidType', line: 2, column: 30 }], + errors: [ + { + messageId: 'invalidType', + data: { type: 'null' }, + line: 2, + column: 30, + }, + ], }, { code: ` declare const arg: number; const msg = \`arg = \${arg}\`; `, - errors: [{ messageId: 'invalidType', line: 3, column: 30 }], + errors: [ + { + messageId: 'invalidType', + data: { type: 'number' }, + line: 3, + column: 30, + }, + ], }, { code: ` declare const arg: boolean; const msg = \`arg = \${arg}\`; `, - errors: [{ messageId: 'invalidType', line: 3, column: 30 }], + errors: [ + { + messageId: 'invalidType', + data: { type: 'boolean' }, + line: 3, + column: 30, + }, + ], }, { options: [{ allowNumber: true, allowBoolean: true, allowNullable: true }], @@ -248,14 +283,23 @@ ruleTester.run('restrict-template-expressions', rule, { const arg = {}; const msg = \`arg = \${arg}\`; `, - errors: [{ messageId: 'invalidType', line: 3, column: 30 }], + errors: [ + { messageId: 'invalidType', data: { type: '{}' }, line: 3, column: 30 }, + ], }, { code: ` declare const arg: { a: string } & { b: string }; const msg = \`arg = \${arg}\`; `, - errors: [{ messageId: 'invalidType', line: 3, column: 30 }], + errors: [ + { + messageId: 'invalidType', + data: { type: '{ a: string; } & { b: string; }' }, + line: 3, + column: 30, + }, + ], }, { options: [{ allowNumber: true, allowBoolean: true, allowNullable: true }], @@ -264,7 +308,25 @@ ruleTester.run('restrict-template-expressions', rule, { return \`arg = \${arg}\`; } `, - errors: [{ messageId: 'invalidType', line: 3, column: 27 }], + errors: [ + { messageId: 'invalidType', data: { type: '{}' }, line: 3, column: 27 }, + ], + }, + { + options: [{ allowNumber: true, allowBoolean: true, allowNullable: true }], + code: ` + function test(arg: T) { + return \`arg = \${arg}\`; + } + `, + errors: [ + { + messageId: 'invalidType', + data: { type: 'any' }, + line: 3, + column: 27, + }, + ], }, { options: [{ allowNumber: true, allowBoolean: true, allowNullable: true }], @@ -273,7 +335,14 @@ ruleTester.run('restrict-template-expressions', rule, { return \`arg = \${arg}\`; } `, - errors: [{ messageId: 'invalidType', line: 3, column: 27 }], + errors: [ + { + messageId: 'invalidType', + data: { type: 'any' }, + line: 3, + column: 27, + }, + ], }, ], });