diff --git a/packages/eslint-plugin/src/util/collectUnusedVariables.ts b/packages/eslint-plugin/src/util/collectUnusedVariables.ts index 983eb1472c1..376efa900bc 100644 --- a/packages/eslint-plugin/src/util/collectUnusedVariables.ts +++ b/packages/eslint-plugin/src/util/collectUnusedVariables.ts @@ -5,6 +5,8 @@ import type { import type { TSESTree } from '@typescript-eslint/utils'; import { + DefinitionType, + ESLintScopeVariable, ImplicitLibVariable, ScopeType, Visitor, @@ -188,7 +190,7 @@ class UnusedVarsVisitor extends Visitor { // basic exported variables isExported(variable) || // variables implicitly exported via a merged declaration - isMergableExported(variable) || + isMergeableExported(variable) || // used variables isUsedVariable(variable) ) { @@ -415,6 +417,29 @@ function isSelfReference( return false; } +const exportExceptDefTypes: DefinitionType[] = [ + DefinitionType.Variable, + DefinitionType.Type, +]; +/** + * In edge cases, the existing used logic does not work. + * When the type and variable name of the variable are the same + * @ref https://github.com/typescript-eslint/typescript-eslint/issues/10658 + * @param variable the variable to check + * @returns true if it is an edge case + */ +function isSafeUnusedExportCondition(variable: ScopeVariable): boolean { + if (variable instanceof ESLintScopeVariable) { + return true; + } + if (variable.isTypeVariable && variable.isValueVariable) { + return !variable.defs + .map(d => d.type) + .every(t => exportExceptDefTypes.includes(t)); + } + return true; +} + const MERGABLE_TYPES = new Set([ AST_NODE_TYPES.ClassDeclaration, AST_NODE_TYPES.FunctionDeclaration, @@ -426,7 +451,8 @@ const MERGABLE_TYPES = new Set([ * Determine if the variable is directly exported * @param variable the variable to check */ -function isMergableExported(variable: ScopeVariable): boolean { +function isMergeableExported(variable: ScopeVariable): boolean { + const safeFlag = isSafeUnusedExportCondition(variable); // If all of the merged things are of the same type, TS will error if not all of them are exported - so we only need to find one for (const def of variable.defs) { // parameters can never be exported. @@ -441,7 +467,9 @@ function isMergableExported(variable: ScopeVariable): boolean { def.node.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration) || def.node.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration ) { - return true; + return ( + safeFlag || def.node.type !== AST_NODE_TYPES.TSTypeAliasDeclaration + ); } } @@ -454,6 +482,7 @@ function isMergableExported(variable: ScopeVariable): boolean { * @returns True if the variable is exported, false if not. */ function isExported(variable: ScopeVariable): boolean { + const safeFlag = isSafeUnusedExportCondition(variable); return variable.defs.some(definition => { let node = definition.node; @@ -465,7 +494,12 @@ function isExported(variable: ScopeVariable): boolean { } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return node.parent!.type.startsWith('Export'); + const isExportedFlag = node.parent!.type.startsWith('Export'); + + return ( + isExportedFlag && + (safeFlag || node.type !== AST_NODE_TYPES.TSTypeAliasDeclaration) + ); }); } diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts index 5f40bbcdc3c..c9a5f4a1c12 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts @@ -1715,6 +1715,24 @@ export {}; ], filename: 'foo.d.ts', }, + // https://github.com/typescript-eslint/typescript-eslint/issues/10658 + { + code: ` +const A = 0; +export type A = typeof A; + `, + errors: [ + { + data: { + action: 'assigned a value', + additional: '', + varName: 'A', + }, + line: 2, + messageId: 'usedOnlyAsType', + }, + ], + }, ], valid: [ @@ -3018,5 +3036,13 @@ declare class Bar {} `, filename: 'foo.d.ts', }, + { + code: ` +const A = 0; + +type A = typeof A; +export { A }; + `, + }, ], });