Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f7d55fd
test: add test case
nayounsang Jun 17, 2025
bc379fc
fix: strict checks on exports with same type and variable name
nayounsang Jun 20, 2025
bfffde3
Merge branch 'main' into type-unused
nayounsang Jun 20, 2025
2f474b3
refactor: resolve self-code-review
nayounsang Jun 20, 2025
2bfdd0b
Merge branch 'main' into type-unused
nayounsang Jul 1, 2025
52eede8
refactor: change ScopeVariable to Variable
nayounsang Jul 1, 2025
df466a4
refactor: improve perf when call isSafeUnusedExportCondition
nayounsang Jul 1, 2025
0d5801d
test: add test case
nayounsang Jul 2, 2025
ed639ea
refactor: validate logic
nayounsang Jul 2, 2025
1d36292
Merge branch 'main' into type-unused
nayounsang Jul 2, 2025
85d49d7
fix: lint test file
nayounsang Jul 2, 2025
e1e49e4
Merge branch 'main' into type-unused
nayounsang Jul 5, 2025
8ac0777
refactor: dupe logic abstract
nayounsang Jul 5, 2025
05ae42c
Merge branch 'main' into type-unused
nayounsang Jul 6, 2025
bf9a5cf
Merge branch 'main' into type-unused
nayounsang Jul 12, 2025
d2ab1bb
Merge branch 'main' into type-unused
nayounsang Aug 17, 2025
2fc0d90
fix: add case for interface
nayounsang Aug 17, 2025
8cb08d8
Merge branch 'type-unused' of https://github.com/nayounsang/typescrip…
nayounsang Aug 17, 2025
a95d35e
Merge branch 'main' into type-unused
nayounsang Aug 25, 2025
1ce2a09
test: add more test cases
nayounsang Oct 25, 2025
b529ace
test: add testcase for only value exported
nayounsang Nov 25, 2025
75e8dae
fix: implement only value exported but error report position is weird
nayounsang Nov 25, 2025
fccdd43
Merge branch 'type-unused' of https://github.com/nayounsang/typescrip…
nayounsang Nov 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 68 additions & 14 deletions packages/eslint-plugin/src/util/collectUnusedVariables.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
ScopeManager,
ScopeVariable,
Variable,
} from '@typescript-eslint/scope-manager';
import type { TSESTree } from '@typescript-eslint/utils';

Expand Down Expand Up @@ -188,7 +189,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)
) {
Expand Down Expand Up @@ -415,6 +416,23 @@ function isSelfReference(
return false;
}

/**
* @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
*/
function isMergedTypeDeclaration(
variable: Variable,
node: TSESTree.Node,
): boolean {
return (
(node.type === AST_NODE_TYPES.TSTypeAliasDeclaration ||
node.type === AST_NODE_TYPES.TSInterfaceDeclaration) &&
variable.isTypeVariable &&
variable.isValueVariable
);
}

const MERGABLE_TYPES = new Set([
AST_NODE_TYPES.ClassDeclaration,
AST_NODE_TYPES.FunctionDeclaration,
Expand All @@ -426,7 +444,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: Variable): boolean {
// 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.
Expand All @@ -441,7 +459,7 @@ function isMergableExported(variable: ScopeVariable): boolean {
def.node.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration) ||
def.node.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration
) {
return true;
return !isMergedTypeDeclaration(variable, def.node);
}
}

Expand All @@ -453,20 +471,56 @@ function isMergableExported(variable: ScopeVariable): boolean {
* @param variable eslint-scope variable object.
* @returns True if the variable is exported, false if not.
*/
function isExported(variable: ScopeVariable): boolean {
return variable.defs.some(definition => {
let node = definition.node;

function isExported(variable: Variable): boolean {
const isStartsWithExport = (node: TSESTree.Node): boolean =>
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
node.parent!.type.startsWith('Export');
const getNode = (node: TSESTree.Node): TSESTree.Node => {
if (node.type === AST_NODE_TYPES.VariableDeclarator) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
node = node.parent!;
} else if (definition.type === TSESLint.Scope.DefinitionType.Parameter) {
return false;
return node.parent;
}
return node;
};
const isMerged = variable.isTypeVariable && variable.isValueVariable;

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return node.parent!.type.startsWith('Export');
});
if (!isMerged) {
return variable.defs.some(definition => {
const node = getNode(definition.node);
if (definition.type === TSESLint.Scope.DefinitionType.Parameter) {
return false;
}

return isStartsWithExport(node);
});
}

let hasExportedValue = false;
let hasExportedType = false;
for (const definition of variable.defs) {
if (definition.type === TSESLint.Scope.DefinitionType.Parameter) {
continue;
}

const node = getNode(definition.node);
if (
node.type === AST_NODE_TYPES.TSEnumDeclaration ||
node.type === AST_NODE_TYPES.TSModuleDeclaration ||
node.type === AST_NODE_TYPES.ClassDeclaration ||
node.type === AST_NODE_TYPES.TSImportEqualsDeclaration
) {
return isStartsWithExport(node);
}

if (isMergedTypeDeclaration(variable, node)) {
if (isStartsWithExport(node)) {
hasExportedType = true;
}
} else if (isStartsWithExport(node)) {
hasExportedValue = true;
}
}

return hasExportedValue && hasExportedType;
}

const LOGICAL_ASSIGNMENT_OPERATORS = new Set(['??=', '&&=', '||=']);
Expand Down
178 changes: 166 additions & 12 deletions packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1715,6 +1715,101 @@ 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',
},
],
},
{
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',
},
],
},
{
code: `
const A = 0;
export interface A {}
`,
errors: [
{
data: {
action: 'assigned a value',
additional: '',
varName: 'A',
},
line: 2,
messageId: 'unusedVar',
},
],
},
{
code: `
interface Foo {
bar: string;
}
export const Foo = 'bar';
`,
errors: [
{
column: 14,
data: {
action: 'assigned a value',
additional: '',
varName: 'Foo',
},
line: 5,
messageId: 'unusedVar',
},
],
},
{
code: `
export const Foo = 'bar';
interface Foo {
bar: string;
}
`,
errors: [
{
column: 14,
data: {
action: 'assigned a value',
additional: '',
varName: 'Foo',
},
line: 2,
messageId: 'unusedVar',
},
],
},
],

valid: [
Expand Down Expand Up @@ -2774,18 +2869,6 @@ export class Foo {
`,
},
`
interface Foo {
bar: string;
}
export const Foo = 'bar';
`,
`
export const Foo = 'bar';
interface Foo {
bar: string;
}
`,
`
let foo = 1;
foo ??= 2;
`,
Expand Down Expand Up @@ -3018,5 +3101,76 @@ declare class Bar {}
`,
filename: 'foo.d.ts',
},
{
code: `
const A = 0;
type A = typeof A;
export { A };
`,
},
{
code: `
const A = 0;
type A = 0;
export { A };
`,
},
{
code: `
const A = 0;
interface A {}
export { A };
`,
},
{
code: `
interface A {}
const A = 0;
export { A };
`,
},
{
code: `
type A = 0;
const A = 0;
export { A };
`,
},
{
code: `
class A {}
export type B = A;
`,
},
{
code: `
export const Foo = 0;
export interface Foo {}
`,
},
{
code: `
export interface Foo {}
export const Foo = 0;
`,
},
{
code: `
export const Foo = 0;
export type Foo = typeof Foo;
`,
},
{
code: `
export const Foo = 0;
export type Foo = 0;
`,
},
{
code: `
export type Foo = 0;
export const Foo = 0;
`,
},
],
});