From f7d55fd8c8fca1eed5bf6086781c932e35b7d9b8 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Tue, 17 Jun 2025 19:47:35 +0900 Subject: [PATCH 01/10] test: add test case --- .../no-unused-vars/no-unused-vars.test.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) 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 5f40bbcdc3cc..c9a5f4a1c122 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 }; + `, + }, ], }); From bc379fce5697f29911282dea9ee9194084c578f3 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Fri, 20 Jun 2025 16:29:31 +0900 Subject: [PATCH 02/10] fix: strict checks on exports with same type and variable name --- .../src/util/collectUnusedVariables.ts | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/util/collectUnusedVariables.ts b/packages/eslint-plugin/src/util/collectUnusedVariables.ts index 983eb1472c14..314dfdcd2806 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, @@ -15,6 +17,7 @@ import { ESLintUtils, TSESLint, } from '@typescript-eslint/utils'; +import { Node } from 'typescript'; import { isTypeImport } from './isTypeImport'; import { referenceContainsTypeQuery } from './referenceContainsTypeQuery'; @@ -415,6 +418,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, @@ -427,6 +453,7 @@ const MERGABLE_TYPES = new Set([ * @param variable the variable to check */ function isMergableExported(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 +468,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 +483,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 +495,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) + ); }); } From 2f474b3f902606e151b01f31e05ae16899a6946c Mon Sep 17 00:00:00 2001 From: nayounsang Date: Fri, 20 Jun 2025 21:42:33 +0900 Subject: [PATCH 03/10] refactor: resolve self-code-review --- packages/eslint-plugin/src/util/collectUnusedVariables.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/util/collectUnusedVariables.ts b/packages/eslint-plugin/src/util/collectUnusedVariables.ts index 314dfdcd2806..376efa900bc8 100644 --- a/packages/eslint-plugin/src/util/collectUnusedVariables.ts +++ b/packages/eslint-plugin/src/util/collectUnusedVariables.ts @@ -17,7 +17,6 @@ import { ESLintUtils, TSESLint, } from '@typescript-eslint/utils'; -import { Node } from 'typescript'; import { isTypeImport } from './isTypeImport'; import { referenceContainsTypeQuery } from './referenceContainsTypeQuery'; @@ -191,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) ) { @@ -452,7 +451,7 @@ 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) { From 52eede8005e79a1808633ec8bd87a43831db3bca Mon Sep 17 00:00:00 2001 From: nayounsang Date: Tue, 1 Jul 2025 17:24:46 +0900 Subject: [PATCH 04/10] refactor: change ScopeVariable to Variable --- .../eslint-plugin/src/util/collectUnusedVariables.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/util/collectUnusedVariables.ts b/packages/eslint-plugin/src/util/collectUnusedVariables.ts index 376efa900bc8..7b0b47de8c81 100644 --- a/packages/eslint-plugin/src/util/collectUnusedVariables.ts +++ b/packages/eslint-plugin/src/util/collectUnusedVariables.ts @@ -1,6 +1,7 @@ import type { ScopeManager, ScopeVariable, + Variable, } from '@typescript-eslint/scope-manager'; import type { TSESTree } from '@typescript-eslint/utils'; @@ -428,10 +429,7 @@ const exportExceptDefTypes: DefinitionType[] = [ * @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; - } +function isSafeUnusedExportCondition(variable: Variable): boolean { if (variable.isTypeVariable && variable.isValueVariable) { return !variable.defs .map(d => d.type) @@ -451,7 +449,7 @@ const MERGABLE_TYPES = new Set([ * Determine if the variable is directly exported * @param variable the variable to check */ -function isMergeableExported(variable: ScopeVariable): boolean { +function isMergeableExported(variable: Variable): 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) { @@ -481,7 +479,7 @@ function isMergeableExported(variable: ScopeVariable): boolean { * @param variable eslint-scope variable object. * @returns True if the variable is exported, false if not. */ -function isExported(variable: ScopeVariable): boolean { +function isExported(variable: Variable): boolean { const safeFlag = isSafeUnusedExportCondition(variable); return variable.defs.some(definition => { let node = definition.node; From df466a478b90bdc9b7093970e7606658c468fd75 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Tue, 1 Jul 2025 17:36:21 +0900 Subject: [PATCH 05/10] refactor: improve perf when call isSafeUnusedExportCondition --- packages/eslint-plugin/src/util/collectUnusedVariables.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/util/collectUnusedVariables.ts b/packages/eslint-plugin/src/util/collectUnusedVariables.ts index 7b0b47de8c81..62deb7f28dd7 100644 --- a/packages/eslint-plugin/src/util/collectUnusedVariables.ts +++ b/packages/eslint-plugin/src/util/collectUnusedVariables.ts @@ -450,7 +450,6 @@ const MERGABLE_TYPES = new Set([ * @param variable the variable to check */ function isMergeableExported(variable: Variable): 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. @@ -466,7 +465,8 @@ function isMergeableExported(variable: Variable): boolean { def.node.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration ) { return ( - safeFlag || def.node.type !== AST_NODE_TYPES.TSTypeAliasDeclaration + def.node.type !== AST_NODE_TYPES.TSTypeAliasDeclaration || + isSafeUnusedExportCondition(variable) ); } } @@ -480,7 +480,6 @@ function isMergeableExported(variable: Variable): boolean { * @returns True if the variable is exported, false if not. */ function isExported(variable: Variable): boolean { - const safeFlag = isSafeUnusedExportCondition(variable); return variable.defs.some(definition => { let node = definition.node; @@ -496,7 +495,8 @@ function isExported(variable: Variable): boolean { return ( isExportedFlag && - (safeFlag || node.type !== AST_NODE_TYPES.TSTypeAliasDeclaration) + (node.type !== AST_NODE_TYPES.TSTypeAliasDeclaration || + isSafeUnusedExportCondition(variable)) ); }); } From 0d5801d17acb373a7bce62194493fdb260134057 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Wed, 2 Jul 2025 17:29:50 +0900 Subject: [PATCH 06/10] test: add test case --- .../no-unused-vars/no-unused-vars.test.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) 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 c9a5f4a1c122..2fd9fdf69626 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 @@ -1733,6 +1733,26 @@ export type A = typeof A; }, ], }, + { + code: ` +function A() { } +namespace A { + export const prop = 1; +} +export type A = typeof A; + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'A', + }, + line: 2, + messageId: 'usedOnlyAsType', + }, + ], + }, ], valid: [ @@ -3039,10 +3059,15 @@ declare class Bar {} { code: ` const A = 0; - type A = typeof A; export { A }; `, }, + { + code: ` +class A { } +export type B = A; + `, + }, ], }); From ed639ea3deb69c72c6df67caff648001433c541c Mon Sep 17 00:00:00 2001 From: nayounsang Date: Wed, 2 Jul 2025 17:35:22 +0900 Subject: [PATCH 07/10] refactor: validate logic --- .../src/util/collectUnusedVariables.ts | 32 ++++++------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/packages/eslint-plugin/src/util/collectUnusedVariables.ts b/packages/eslint-plugin/src/util/collectUnusedVariables.ts index 62deb7f28dd7..34da070722ad 100644 --- a/packages/eslint-plugin/src/util/collectUnusedVariables.ts +++ b/packages/eslint-plugin/src/util/collectUnusedVariables.ts @@ -6,8 +6,6 @@ import type { import type { TSESTree } from '@typescript-eslint/utils'; import { - DefinitionType, - ESLintScopeVariable, ImplicitLibVariable, ScopeType, Visitor, @@ -418,24 +416,12 @@ 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 + * @returns true if it is `isTypeVariable` and `isValueVariable` */ -function isSafeUnusedExportCondition(variable: Variable): boolean { - if (variable.isTypeVariable && variable.isValueVariable) { - return !variable.defs - .map(d => d.type) - .every(t => exportExceptDefTypes.includes(t)); - } - return true; +function isDualPurposeVariable(variable: Variable): boolean { + return variable.isTypeVariable && variable.isValueVariable; } const MERGABLE_TYPES = new Set([ @@ -464,9 +450,9 @@ function isMergeableExported(variable: Variable): boolean { def.node.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration) || def.node.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration ) { - return ( - def.node.type !== AST_NODE_TYPES.TSTypeAliasDeclaration || - isSafeUnusedExportCondition(variable) + return !( + def.node.type === AST_NODE_TYPES.TSTypeAliasDeclaration && + isDualPurposeVariable(variable) ); } } @@ -495,8 +481,10 @@ function isExported(variable: Variable): boolean { return ( isExportedFlag && - (node.type !== AST_NODE_TYPES.TSTypeAliasDeclaration || - isSafeUnusedExportCondition(variable)) + !( + node.type === AST_NODE_TYPES.TSTypeAliasDeclaration && + isDualPurposeVariable(variable) + ) ); }); } From 85d49d7427d913f3aee57c84d5fd65b9e18a72e7 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Wed, 2 Jul 2025 18:05:09 +0900 Subject: [PATCH 08/10] fix: lint test file --- .../tests/rules/no-unused-vars/no-unused-vars.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 2fd9fdf69626..517baaefc6aa 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 @@ -1735,7 +1735,7 @@ export type A = typeof A; }, { code: ` -function A() { } +function A() {} namespace A { export const prop = 1; } @@ -3065,7 +3065,7 @@ export { A }; }, { code: ` -class A { } +class A {} export type B = A; `, }, From 8ac0777ac1c27eda0897661c64f7969dc1f80057 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Sat, 5 Jul 2025 23:46:50 +0900 Subject: [PATCH 09/10] refactor: dupe logic abstract --- .../src/util/collectUnusedVariables.ts | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/eslint-plugin/src/util/collectUnusedVariables.ts b/packages/eslint-plugin/src/util/collectUnusedVariables.ts index 34da070722ad..734eeac690de 100644 --- a/packages/eslint-plugin/src/util/collectUnusedVariables.ts +++ b/packages/eslint-plugin/src/util/collectUnusedVariables.ts @@ -418,10 +418,18 @@ function isSelfReference( /** * @param variable the variable to check - * @returns true if it is `isTypeVariable` and `isValueVariable` + * @param node the node from a some def of variable + * @returns true if variable is type/value duality and declaration is type declaration */ -function isDualPurposeVariable(variable: Variable): boolean { - return variable.isTypeVariable && variable.isValueVariable; +function isMergedTypeDeclaration( + variable: Variable, + node: TSESTree.Node, +): boolean { + return ( + node.type === AST_NODE_TYPES.TSTypeAliasDeclaration && + variable.isTypeVariable && + variable.isValueVariable + ); } const MERGABLE_TYPES = new Set([ @@ -450,10 +458,7 @@ function isMergeableExported(variable: Variable): boolean { def.node.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration) || def.node.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration ) { - return !( - def.node.type === AST_NODE_TYPES.TSTypeAliasDeclaration && - isDualPurposeVariable(variable) - ); + return !isMergedTypeDeclaration(variable, def.node); } } @@ -479,13 +484,7 @@ function isExported(variable: Variable): boolean { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const isExportedFlag = node.parent!.type.startsWith('Export'); - return ( - isExportedFlag && - !( - node.type === AST_NODE_TYPES.TSTypeAliasDeclaration && - isDualPurposeVariable(variable) - ) - ); + return isExportedFlag && !isMergedTypeDeclaration(variable, node); }); } From 2fc0d90a6d4ae6c5caeb00d1e09c778b5b521017 Mon Sep 17 00:00:00 2001 From: nayounsang Date: Sun, 17 Aug 2025 17:52:26 +0900 Subject: [PATCH 10/10] fix: add case for interface --- .../src/util/collectUnusedVariables.ts | 5 ++-- .../no-unused-vars/no-unused-vars.test.ts | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/util/collectUnusedVariables.ts b/packages/eslint-plugin/src/util/collectUnusedVariables.ts index 734eeac690de..47127e8d4406 100644 --- a/packages/eslint-plugin/src/util/collectUnusedVariables.ts +++ b/packages/eslint-plugin/src/util/collectUnusedVariables.ts @@ -419,14 +419,15 @@ function isSelfReference( /** * @param variable the variable to check * @param node the node from a some def of variable - * @returns true if variable is type/value duality and declaration is type declaration + * @returns `true` if variable is type/value duality and declaration is type declaration */ function isMergedTypeDeclaration( variable: Variable, node: TSESTree.Node, ): boolean { return ( - node.type === AST_NODE_TYPES.TSTypeAliasDeclaration && + (node.type === AST_NODE_TYPES.TSTypeAliasDeclaration || + node.type === AST_NODE_TYPES.TSInterfaceDeclaration) && variable.isTypeVariable && variable.isValueVariable ); 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 517baaefc6aa..a8beaa2d6034 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 @@ -1753,6 +1753,23 @@ export type A = typeof A; }, ], }, + { + code: ` +const A = 0; +export interface A {} + `, + errors: [ + { + data: { + action: 'assigned a value', + additional: '', + varName: 'A', + }, + line: 2, + messageId: 'unusedVar', + }, + ], + }, ], valid: [ @@ -3069,5 +3086,17 @@ class A {} export type B = A; `, }, + { + code: ` +export const Foo = 0; +export interface Foo {} + `, + }, + { + code: ` +export const Foo = 0; +export type Foo = typeof Foo; + `, + }, ], });