From 74b88288bde826e8c06f9d9a2e36dd71ddcdebda Mon Sep 17 00:00:00 2001 From: Tamashoo Date: Tue, 18 Nov 2025 22:52:36 +0900 Subject: [PATCH 1/2] fix: consistent-type-exports handle shadowed import correctly --- .../src/rules/consistent-type-exports.ts | 32 +++++++++++++++++++ .../fixtures/consistent-type-exports/index.ts | 1 + .../rules/consistent-type-exports.test.ts | 5 +++ 3 files changed, 38 insertions(+) diff --git a/packages/eslint-plugin/src/rules/consistent-type-exports.ts b/packages/eslint-plugin/src/rules/consistent-type-exports.ts index f70d59618514..ed8653032ad0 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-exports.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-exports.ts @@ -1,5 +1,6 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import { DefinitionType } from '@typescript-eslint/scope-manager'; import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils'; import * as ts from 'typescript'; @@ -80,6 +81,30 @@ export default createRule({ const services = getParserServices(context); const checker = services.program.getTypeChecker(); + /** + * Check if the export specifier is shadowed by a local value definition. + * + * @returns True if shadowed by a local value, false otherwise. + */ + function isShadowedByLocalValue( + specifier: TSESTree.ExportSpecifier, + ): boolean { + const scope = context.sourceCode.getScope(specifier); + const localName = + specifier.local.type === AST_NODE_TYPES.Identifier + ? specifier.local.name + : specifier.local.value; + const variable = scope.set.get(localName); + return Boolean( + variable?.defs.some( + def => + def.type === DefinitionType.Variable || + def.type === DefinitionType.FunctionName || + def.type === DefinitionType.ClassName, + ), + ); + } + /** * Helper for identifying if a symbol resolves to a * JavaScript value or a TypeScript type. @@ -89,7 +114,13 @@ export default createRule({ */ function isSymbolTypeBased( symbol: ts.Symbol | undefined, + specifier: TSESTree.ExportSpecifier, ): boolean | undefined { + // Check if any imported type is shadowed by a local value definition + if (isShadowedByLocalValue(specifier)) { + return false; + } + while (symbol && symbol.flags & ts.SymbolFlags.Alias) { symbol = checker.getAliasedSymbol(symbol); if ( @@ -221,6 +252,7 @@ export default createRule({ const isTypeBased = isSymbolTypeBased( services.getSymbolAtLocation(specifier.exported), + specifier, ); if (isTypeBased === true) { diff --git a/packages/eslint-plugin/tests/fixtures/consistent-type-exports/index.ts b/packages/eslint-plugin/tests/fixtures/consistent-type-exports/index.ts index cd6777fd1b68..8c204574f044 100644 --- a/packages/eslint-plugin/tests/fixtures/consistent-type-exports/index.ts +++ b/packages/eslint-plugin/tests/fixtures/consistent-type-exports/index.ts @@ -1,5 +1,6 @@ export type Type1 = 1; export type Type2 = 1; +export type Type3 = { foo: string }; export const value1 = 2; export const value2 = 2; diff --git a/packages/eslint-plugin/tests/rules/consistent-type-exports.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-exports.test.ts index 6226405f4ebb..f145f22d3e45 100644 --- a/packages/eslint-plugin/tests/rules/consistent-type-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-type-exports.test.ts @@ -62,6 +62,11 @@ import * as Foo from './consistent-type-exports'; type Foo = 1; export { Foo } `, + ` +import { Type3 } from './consistent-type-exports'; +const Type3 = { foo: 'bar' }; +export { Type3 }; + `, ], invalid: [ { From 493c1b2b73f757ac57dec8668ed554a398062d1d Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Fri, 28 Nov 2025 12:25:15 -0500 Subject: [PATCH 2/2] add re-export tests --- .../fixtures/consistent-type-exports/index.ts | 1 - .../consistent-type-exports/reexport-1.ts | 1 + .../consistent-type-exports/reexport-2-named.ts | 3 +++ .../reexport-2-namespace.ts | 5 +++++ .../tests/rules/consistent-type-exports.test.ts | 15 ++++++++++++--- 5 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 packages/eslint-plugin/tests/fixtures/consistent-type-exports/reexport-1.ts create mode 100644 packages/eslint-plugin/tests/fixtures/consistent-type-exports/reexport-2-named.ts create mode 100644 packages/eslint-plugin/tests/fixtures/consistent-type-exports/reexport-2-namespace.ts diff --git a/packages/eslint-plugin/tests/fixtures/consistent-type-exports/index.ts b/packages/eslint-plugin/tests/fixtures/consistent-type-exports/index.ts index 6e8f8d85e5f6..af2a70f7d321 100644 --- a/packages/eslint-plugin/tests/fixtures/consistent-type-exports/index.ts +++ b/packages/eslint-plugin/tests/fixtures/consistent-type-exports/index.ts @@ -1,6 +1,5 @@ export type Type1 = 1; export type Type2 = 1; -export type Type3 = { foo: string }; export const value1 = 2; export const value2 = 2; diff --git a/packages/eslint-plugin/tests/fixtures/consistent-type-exports/reexport-1.ts b/packages/eslint-plugin/tests/fixtures/consistent-type-exports/reexport-1.ts new file mode 100644 index 000000000000..f29cc6f5296e --- /dev/null +++ b/packages/eslint-plugin/tests/fixtures/consistent-type-exports/reexport-1.ts @@ -0,0 +1 @@ +export type A = 1; diff --git a/packages/eslint-plugin/tests/fixtures/consistent-type-exports/reexport-2-named.ts b/packages/eslint-plugin/tests/fixtures/consistent-type-exports/reexport-2-named.ts new file mode 100644 index 000000000000..4486d9a3ab41 --- /dev/null +++ b/packages/eslint-plugin/tests/fixtures/consistent-type-exports/reexport-2-named.ts @@ -0,0 +1,3 @@ +import { A } from './reexport-1'; +const A = 1; +export { A }; diff --git a/packages/eslint-plugin/tests/fixtures/consistent-type-exports/reexport-2-namespace.ts b/packages/eslint-plugin/tests/fixtures/consistent-type-exports/reexport-2-namespace.ts new file mode 100644 index 000000000000..e422b217435c --- /dev/null +++ b/packages/eslint-plugin/tests/fixtures/consistent-type-exports/reexport-2-namespace.ts @@ -0,0 +1,5 @@ +import { A } from './reexport-1'; +namespace A { + export const b = 2; +} +export { A }; diff --git a/packages/eslint-plugin/tests/rules/consistent-type-exports.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-exports.test.ts index 069064b1779e..de7aa6dff488 100644 --- a/packages/eslint-plugin/tests/rules/consistent-type-exports.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-type-exports.test.ts @@ -68,9 +68,18 @@ const Type1 = 1; export { Type1 }; `, ` -import { Type3 } from './consistent-type-exports'; -const Type3 = { foo: 'bar' }; -export { Type3 }; +export { A } from './consistent-type-exports/reexport-2-named'; + `, + ` +import { A } from './consistent-type-exports/reexport-2-named'; +export { A }; + `, + ` +export { A } from './consistent-type-exports/reexport-2-namespace'; + `, + ` +import { A } from './consistent-type-exports/reexport-2-namespace'; +export { A }; `, ], invalid: [