diff --git a/packages/eslint-plugin/src/util/collectUnusedVariables.ts b/packages/eslint-plugin/src/util/collectUnusedVariables.ts index dbe749e62900..cd215124d64c 100644 --- a/packages/eslint-plugin/src/util/collectUnusedVariables.ts +++ b/packages/eslint-plugin/src/util/collectUnusedVariables.ts @@ -1,6 +1,7 @@ import { ImplicitLibVariable, ScopeType, + Variable, Visitor, } from '@typescript-eslint/scope-manager'; import type { TSESTree } from '@typescript-eslint/utils'; @@ -10,6 +11,7 @@ import { ESLintUtils, TSESLint, } from '@typescript-eslint/utils'; +import { isDefinitionFile } from './misc'; class UnusedVarsVisitor< MessageIds extends string, @@ -22,6 +24,7 @@ class UnusedVarsVisitor< readonly #scopeManager: TSESLint.Scope.ScopeManager; // readonly #unusedVariables = new Set(); + readonly #isDefinitionFile: boolean; private constructor(context: TSESLint.RuleContext) { super({ @@ -32,6 +35,8 @@ class UnusedVarsVisitor< context.sourceCode.scopeManager, 'Missing required scope manager', ); + + this.#isDefinitionFile = isDefinitionFile(context.filename); } public static collectUnusedVariables< @@ -60,7 +65,12 @@ class UnusedVarsVisitor< scope: TSESLint.Scope.Scope, unusedVariables = new Set(), ): ReadonlySet { - for (const variable of scope.variables) { + const implicitlyExported = allVariablesImplicitlyExported( + scope, + this.#isDefinitionFile, + ); + + for (const variable of implicitlyExported ? [] : scope.variables) { if ( // skip function expression names, scope.functionExpressionScope || @@ -438,6 +448,47 @@ function isExported(variable: TSESLint.Scope.Variable): boolean { }); } +function allVariablesImplicitlyExported( + scope: TSESLint.Scope.Scope, + isDefinitionFile: boolean, +): boolean { + // TODO: does this also happen in ambient module declarations? + if ( + !isDefinitionFile || + !( + scope.type === ScopeType.tsModule || + scope.type === ScopeType.module || + scope.type === ScopeType.global + ) + ) { + return false; + } + + // TODO: test modules, globals + // TODO: look for `export {}` + + function isExportImportEquals(variable: Variable): boolean { + for (const def of variable.defs) { + if ( + def.type === TSESLint.Scope.DefinitionType.ImportBinding && + def.node.type === AST_NODE_TYPES.TSImportEqualsDeclaration && + def.node.parent.type === AST_NODE_TYPES.ExportNamedDeclaration + ) { + return true; + } + } + return false; + } + + for (const variable of scope.variables) { + if (isExported(variable) && !isExportImportEquals(variable)) { + return false; + } + } + + return true; +} + const LOGICAL_ASSIGNMENT_OPERATORS = new Set(['&&=', '||=', '??=']); /** 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 04c218b7631e..8827b82f30e2 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 @@ -1123,6 +1123,72 @@ export namespace Bar { export import TheFoo = Foo; } `, + ` +const foo = 1; +export = foo; + `, + ` +const Foo = 1; +interface Foo { + bar: string; +} +export = Foo; + `, + ` +interface Foo { + bar: string; +} +export = Foo; + `, + ` +type Foo = 1; +export = Foo; + `, + ` +type Foo = 1; +export = {} as Foo; + `, + ` +declare module 'foo' { + type Foo = 1; + export = Foo; +} + `, + ` +namespace Foo { + export const foo = 1; +} +export namespace Bar { + export import TheFoo = Foo; +} + `, + // https://github.com/typescript-eslint/typescript-eslint/issues/2867 + { + code: ` +export namespace Foo { + const foo: 1234; +} + `, + filename: 'foo.d.ts', + }, + { + code: ` +export namespace Foo { + export import Bar = Something.Bar; + const foo: 1234; +} + `, + filename: 'foo.d.ts', + }, + { + code: ` +declare module 'foo' { + export import Bar = Something.Bar; + const foo: 1234; +} + `, + filename: 'foo.d.ts', + }, ], invalid: [ @@ -1950,5 +2016,80 @@ export namespace Bar { }, ], }, + // https://github.com/typescript-eslint/typescript-eslint/issues/2867 + { + code: ` +export namespace Foo { + const foo: 1234; + export const bar: string; + export namespace NS { + const baz: 1234; + } +} + `, + filename: 'foo.d.ts', + errors: [ + { + messageId: 'unusedVar', + line: 3, + column: 9, + data: { + varName: 'foo', + action: 'defined', + additional: '', + }, + }, + ], + }, + { + code: ` +export namespace Foo { + export import Bar = Something.Bar; + const foo: 1234; + export const bar: string; + export namespace NS { + const baz: 1234; + } +} + `, + filename: 'foo.d.ts', + errors: [ + { + messageId: 'unusedVar', + line: 4, + column: 9, + data: { + varName: 'foo', + action: 'defined', + additional: '', + }, + }, + ], + }, + { + code: ` +declare module 'foo' { + export import Bar = Something.Bar; + const foo: 1234; + export const bar: string; + export namespace NS { + const baz: 1234; + } +} + `, + filename: 'foo.d.ts', + errors: [ + { + messageId: 'unusedVar', + line: 4, + column: 9, + data: { + varName: 'foo', + action: 'defined', + additional: '', + }, + }, + ], + }, ], });