diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index 3b9cbda2ba04..f2bfa26bb1a8 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -10,6 +10,8 @@ import { } from '@typescript-eslint/scope-manager'; import { AST_NODE_TYPES, TSESLint } from '@typescript-eslint/utils'; +import type { MakeRequired } from '../util'; + import { collectVariables, createRule, @@ -58,6 +60,11 @@ type VariableType = | 'parameter' | 'variable'; +type ModuleDeclarationWithBody = MakeRequired< + TSESTree.TSModuleDeclaration, + 'body' +>; + export default createRule({ name: 'no-unused-vars', meta: { @@ -144,7 +151,10 @@ export default createRule({ }, defaultOptions: [{}], create(context, [firstOption]) { - const MODULE_DECL_CACHE = new Map(); + const MODULE_DECL_CACHE = new Map< + ModuleDeclarationWithBody | TSESTree.Program, + boolean + >(); const options = ((): TranslatedOptions => { const options: TranslatedOptions = { @@ -557,40 +567,71 @@ export default createRule({ } return { - // declaration file handling - [ambientDeclarationSelector(AST_NODE_TYPES.Program, true)]( + // top-level declaration file handling + [ambientDeclarationSelector(AST_NODE_TYPES.Program)]( node: DeclarationSelectorNode, ): void { if (!isDefinitionFile(context.filename)) { return; } + + const moduleDecl = nullThrows( + node.parent, + NullThrowsReasons.MissingParent, + ) as TSESTree.Program; + + if (checkForOverridingExportStatements(moduleDecl)) { + return; + } + markDeclarationChildAsUsed(node); }, // children of a namespace that is a child of a declared namespace are auto-exported [ambientDeclarationSelector( 'TSModuleDeclaration[declare = true] > TSModuleBlock TSModuleDeclaration > TSModuleBlock', - false, )](node: DeclarationSelectorNode): void { + const moduleDecl = nullThrows( + node.parent.parent, + NullThrowsReasons.MissingParent, + ) as ModuleDeclarationWithBody; + + if (checkForOverridingExportStatements(moduleDecl)) { + return; + } + markDeclarationChildAsUsed(node); }, // declared namespace handling [ambientDeclarationSelector( 'TSModuleDeclaration[declare = true] > TSModuleBlock', - false, )](node: DeclarationSelectorNode): void { const moduleDecl = nullThrows( node.parent.parent, NullThrowsReasons.MissingParent, - ) as TSESTree.TSModuleDeclaration; + ) as ModuleDeclarationWithBody; - // declared ambient modules with an `export =` statement will only export that one thing - // all other statements are not automatically exported in this case - if ( - moduleDecl.id.type === AST_NODE_TYPES.Literal && - checkModuleDeclForExportEquals(moduleDecl) - ) { + if (checkForOverridingExportStatements(moduleDecl)) { + return; + } + + markDeclarationChildAsUsed(node); + }, + + // namespace handling in definition files + [ambientDeclarationSelector('TSModuleDeclaration > TSModuleBlock')]( + node: DeclarationSelectorNode, + ): void { + if (!isDefinitionFile(context.filename)) { + return; + } + const moduleDecl = nullThrows( + node.parent.parent, + NullThrowsReasons.MissingParent, + ) as ModuleDeclarationWithBody; + + if (checkForOverridingExportStatements(moduleDecl)) { return; } @@ -670,21 +711,19 @@ export default createRule({ }, }; - function checkModuleDeclForExportEquals( - node: TSESTree.TSModuleDeclaration, + function checkForOverridingExportStatements( + node: ModuleDeclarationWithBody | TSESTree.Program, ): boolean { const cached = MODULE_DECL_CACHE.get(node); if (cached != null) { return cached; } - if (node.body) { - for (const statement of node.body.body) { - if (statement.type === AST_NODE_TYPES.TSExportAssignment) { - MODULE_DECL_CACHE.set(node, true); - return true; - } - } + const body = getStatementsOfNode(node); + + if (hasOverridingExportStatement(body)) { + MODULE_DECL_CACHE.set(node, true); + return true; } MODULE_DECL_CACHE.set(node, false); @@ -700,10 +739,7 @@ export default createRule({ | TSESTree.TSModuleDeclaration | TSESTree.TSTypeAliasDeclaration | TSESTree.VariableDeclaration; - function ambientDeclarationSelector( - parent: string, - childDeclare: boolean, - ): string { + function ambientDeclarationSelector(parent: string): string { return [ // Types are ambiently exported `${parent} > :matches(${[ @@ -717,7 +753,7 @@ export default createRule({ AST_NODE_TYPES.TSEnumDeclaration, AST_NODE_TYPES.TSModuleDeclaration, AST_NODE_TYPES.VariableDeclaration, - ].join(', ')})${childDeclare ? '[declare = true]' : ''}`, + ].join(', ')})`, ].join(', '); } function markDeclarationChildAsUsed(node: DeclarationSelectorNode): void { @@ -774,6 +810,40 @@ export default createRule({ }, }); +function hasOverridingExportStatement( + body: TSESTree.ProgramStatement[], +): boolean { + for (const statement of body) { + if ( + (statement.type === AST_NODE_TYPES.ExportNamedDeclaration && + statement.declaration == null) || + statement.type === AST_NODE_TYPES.ExportAllDeclaration || + statement.type === AST_NODE_TYPES.TSExportAssignment + ) { + return true; + } + + if ( + statement.type === AST_NODE_TYPES.ExportDefaultDeclaration && + statement.declaration.type === AST_NODE_TYPES.Identifier + ) { + return true; + } + } + + return false; +} + +function getStatementsOfNode( + block: ModuleDeclarationWithBody | TSESTree.Program, +): TSESTree.ProgramStatement[] { + if (block.type === AST_NODE_TYPES.Program) { + return block.body; + } + + return block.body.body; +} + /* ###### TODO ###### 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 3461829abd44..5ebc8d9a406b 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 @@ -1168,6 +1168,572 @@ export class Foo { }, ], }, + { + code: ` +export namespace Foo { + const foo: 1234; + export {}; +} + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'foo', + }, + line: 3, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +const foo: 1234; +export {}; + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'foo', + }, + line: 2, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +declare module 'foo' { + const foo: 1234; + export {}; +} + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'foo', + }, + line: 3, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +export namespace Foo { + const foo: 1234; + const bar: 4567; + + export { bar }; +} + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'foo', + }, + line: 3, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +const foo: 1234; +const bar: 4567; + +export { bar }; + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'foo', + }, + line: 2, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +declare module 'foo' { + const foo: 1234; + const bar: 4567; + + export { bar }; +} + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'foo', + }, + line: 3, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +export namespace Foo { + const foo: 1234; + const bar: 4567; + export const bazz: 4567; + + export { bar }; +} + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'foo', + }, + line: 3, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +const foo: 1234; +const bar: 4567; +export const bazz: 4567; + +export { bar }; + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'foo', + }, + line: 2, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +declare module 'foo' { + const foo: 1234; + const bar: 4567; + export const bazz: 4567; + + export { bar }; +} + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'foo', + }, + line: 3, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +export namespace Foo { + const foo: string; + const bar: number; + + export default bar; +} + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'foo', + }, + line: 3, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +const foo: string; +const bar: number; + +export default bar; + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'foo', + }, + line: 2, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +declare module 'foo' { + const foo: string; + const bar: number; + + export default bar; +} + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'foo', + }, + line: 3, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +export namespace Foo { + const foo: string; + const bar: number; + export const bazz: number; + + export default bar; +} + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'foo', + }, + line: 3, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +const foo: string; +const bar: number; +export const bazz: number; + +export default bar; + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'foo', + }, + line: 2, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +declare module 'foo' { + const foo: string; + const bar: number; + export const bazz: number; + + export default bar; +} + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'foo', + }, + line: 3, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +export namespace Foo { + const foo: string; + export const bar: number; + + export * from '...'; +} + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'foo', + }, + line: 3, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +const foo: string; +export const bar: number; + +export * from '...'; + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'foo', + }, + line: 2, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +declare module 'foo' { + const foo: string; + export const bar: number; + + export * from '...'; +} + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'foo', + }, + line: 3, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +namespace Foo { + type Foo = 1; + type Bar = 1; + + export = Bar; +} + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'Foo', + }, + line: 3, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +type Foo = 1; +type Bar = 1; + +export = Bar; + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'Foo', + }, + line: 2, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +declare module 'foo' { + type Foo = 1; + type Bar = 1; + + export = Bar; +} + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'Foo', + }, + line: 3, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +declare module 'foo' { + type Test = 1; + export {}; +} + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'Test', + }, + line: 3, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, + { + code: ` +export declare namespace Foo { + namespace Bar { + namespace Baz { + namespace Bam { + const x = 1; + } + + export {}; + } + } +} + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'Bam', + }, + line: 5, + messageId: 'unusedVar', + }, + ], + }, + { + code: ` +declare module 'foo' { + namespace Bar { + namespace Baz { + namespace Bam { + const x = 1; + } + + export {}; + } + } +} + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'Bam', + }, + line: 5, + messageId: 'unusedVar', + }, + ], + }, + { + code: ` +declare enum Foo {} +export {}; + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'Foo', + }, + line: 2, + messageId: 'unusedVar', + }, + ], + }, + { + code: ` +class Foo {} +declare class Bar {} + +export {}; + `, + errors: [ + { + data: { + action: 'defined', + additional: '', + varName: 'Foo', + }, + line: 2, + messageId: 'unusedVar', + }, + { + data: { + action: 'defined', + additional: '', + varName: 'Bar', + }, + line: 3, + messageId: 'unusedVar', + }, + ], + filename: 'foo.d.ts', + }, ], valid: [ @@ -2337,5 +2903,139 @@ export class Foo { } } `, + { + code: ` +export namespace Foo { + const foo: 1234; +} + `, + filename: 'foo.d.ts', + }, + { + code: ` +declare module 'foo' { + const foo: 1234; +} + `, + filename: 'foo.d.ts', + }, + { + code: ` +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', + }, + { + code: ` +export import Bar = Something.Bar; +const foo: 1234; + `, + filename: 'foo.d.ts', + }, + { + 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', + }, + { + 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', + }, + { + code: ` +export import Bar = Something.Bar; +const foo: 1234; +export const bar: string; +export namespace NS { + const baz: 1234; +} + `, + filename: 'foo.d.ts', + }, + { + code: ` +export namespace Foo { + const foo: 1234; + export const bar: string; + export namespace NS { + const baz: 1234; + } +} + `, + filename: 'foo.d.ts', + }, + { + code: ` +export namespace Foo { + type Foo = 1; + type Bar = 1; + + export default function foo(): Bar; +} + `, + filename: 'foo.d.ts', + }, + { + code: ` +declare module 'foo' { + type Foo = 1; + type Bar = 1; + + export default function foo(): Bar; +} + `, + filename: 'foo.d.ts', + }, + { + code: ` +type Foo = 1; +type Bar = 1; + +export default function foo(): Bar; + `, + filename: 'foo.d.ts', + }, + { + code: ` +class Foo {} +declare class Bar {} + `, + filename: 'foo.d.ts', + }, ], });