diff --git a/packages/eslint-plugin/src/rules/require-types-exports.ts b/packages/eslint-plugin/src/rules/require-types-exports.ts index e5cde3687c4d..3a803a25f8a9 100644 --- a/packages/eslint-plugin/src/rules/require-types-exports.ts +++ b/packages/eslint-plugin/src/rules/require-types-exports.ts @@ -1,3 +1,4 @@ +import type { Reference } from '@typescript-eslint/scope-manager'; import { DefinitionType } from '@typescript-eslint/scope-manager'; import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; @@ -12,6 +13,12 @@ type FunctionNode = | TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression; +type TypeReference = Reference & { + identifier: { + parent: TSESTree.TSTypeReference; + }; +}; + export default createRule<[], MessageIds>({ name: 'require-types-exports', meta: { @@ -28,9 +35,23 @@ export default createRule<[], MessageIds>({ }, defaultOptions: [], create(context) { + const typeReferences = new Set(); const externalizedTypes = new Set(); const reportedTypes = new Set(); + function collectTypeReferences(node: TSESTree.Program): void { + const scope = context.sourceCode.getScope(node); + + scope.references.forEach(r => { + if ( + r.resolved?.isTypeVariable && + r.identifier.parent.type === AST_NODE_TYPES.TSTypeReference + ) { + typeReferences.add(r as TypeReference); + } + }); + } + function collectImportedTypes(node: TSESTree.ImportSpecifier): void { externalizedTypes.add(node.local.name); } @@ -60,6 +81,8 @@ export default createRule<[], MessageIds>({ for (const declaration of node.declaration.declarations) { if (declaration.init?.type === AST_NODE_TYPES.ArrowFunctionExpression) { checkFunctionTypes(declaration.init); + } else { + checkVariableTypes(declaration); } } } @@ -107,6 +130,21 @@ export default createRule<[], MessageIds>({ .forEach(checkTypeNode); } + function checkVariableTypes( + node: TSESTree.LetOrConstOrVarDeclarator, + ): void { + if (node.id.type !== AST_NODE_TYPES.Identifier) { + return; + } + + typeReferences.forEach(r => { + // TODO: Probably not the best way to do it... + if (isLocationOverlapping(r.identifier.loc, node.loc)) { + checkTypeNode(r.identifier.parent); + } + }); + } + function checkTypeNode(node: TSESTree.TSTypeReference): void { const name = getTypeName(node); @@ -141,7 +179,37 @@ export default createRule<[], MessageIds>({ return ''; } + function isLocationOverlapping( + location: TSESTree.Node['loc'], + container: TSESTree.Node['loc'], + ): boolean { + if ( + location.start.line < container.start.line || + location.end.line > container.end.line + ) { + return false; + } + + if ( + location.start.line === container.start.line && + location.start.column < container.start.column + ) { + return false; + } + + if ( + location.end.line === container.end.line && + location.end.column > container.end.column + ) { + return false; + } + + return true; + } + return { + Program: collectTypeReferences, + 'ImportDeclaration ImportSpecifier, ImportSpecifier': collectImportedTypes, diff --git a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts index 63d36fd3fd16..39f66ebf2359 100644 --- a/packages/eslint-plugin/tests/rules/require-types-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/require-types-exports.test.ts @@ -303,6 +303,24 @@ ruleTester.run('require-types-exports', rule, { return value; } `, + + ` + import type { A } from './types'; + + export type T1 = number; + + export interface T2 { + key: number; + } + + export const value: { a: { b: { c: T1 } } } | [string, T2 | A] = { + a: { + b: { + c: 1, + }, + }, + }; + `, ], invalid: [ @@ -1739,5 +1757,45 @@ ruleTester.run('require-types-exports', rule, { }, ], }, + + { + code: ` + import type { A } from './types'; + + type T1 = number; + + interface T2 { + key: number; + } + + export const value: { a: { b: { c: T1 } } } | [string, T2 | A] = { + a: { + b: { + c: 1, + }, + }, + }; + `, + errors: [ + { + messageId: 'requireTypeExport', + line: 10, + column: 44, + endColumn: 46, + data: { + name: 'T1', + }, + }, + { + messageId: 'requireTypeExport', + line: 10, + column: 64, + endColumn: 66, + data: { + name: 'T2', + }, + }, + ], + }, ], });