diff --git a/.cspell.json b/.cspell.json index 45e79f88485..cbd5138affa 100644 --- a/.cspell.json +++ b/.cspell.json @@ -38,6 +38,7 @@ "words": [ "Airbnb", "Airbnb's", + "ambiently", "ASTs", "autofix", "autofixers", diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index 973df534b0e..d3dc72049ce 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -28,6 +28,7 @@ export default util.createRule({ defaultOptions: [{}], create(context) { const rules = baseRule.create(context); + const filename = context.getFilename(); /** * Gets a list of TS module definitions for a specified variable. @@ -207,19 +208,93 @@ export default util.createRule({ } }, - // TODO - this could probably be refined a bit - '*[declare=true] Identifier'(node: TSESTree.Identifier): void { - context.markVariableAsUsed(node.name); - const scope = context.getScope(); - const { variableScope } = scope; - if (variableScope !== scope) { - const superVar = variableScope.set.get(node.name); + // declaration file handling + [declarationSelector(AST_NODE_TYPES.Program, true)]( + node: DeclarationSelectorNode, + ): void { + if (!util.isDefinitionFile(filename)) { + return; + } + markDeclarationChildAsUsed(node); + }, + + // declared namespace handling + [declarationSelector( + 'TSModuleDeclaration[declare = true] > TSModuleBlock', + false, + )](node: DeclarationSelectorNode): void { + markDeclarationChildAsUsed(node); + }, + }; + + type DeclarationSelectorNode = + | TSESTree.TSInterfaceDeclaration + | TSESTree.TSTypeAliasDeclaration + | TSESTree.ClassDeclaration + | TSESTree.FunctionDeclaration + | TSESTree.TSDeclareFunction + | TSESTree.TSEnumDeclaration + | TSESTree.TSModuleDeclaration + | TSESTree.VariableDeclaration; + function declarationSelector( + parent: string, + childDeclare: boolean, + ): string { + return [ + // Types are ambiently exported + `${parent} > :matches(${[ + AST_NODE_TYPES.TSInterfaceDeclaration, + AST_NODE_TYPES.TSTypeAliasDeclaration, + ].join(', ')})`, + // Value things are ambiently exported if they are "declare"d + `${parent} > :matches(${[ + AST_NODE_TYPES.ClassDeclaration, + AST_NODE_TYPES.TSDeclareFunction, + AST_NODE_TYPES.TSEnumDeclaration, + AST_NODE_TYPES.TSModuleDeclaration, + AST_NODE_TYPES.VariableDeclaration, + ].join(', ')})${childDeclare ? '[declare=true]' : ''}`, + ].join(', '); + } + function markDeclarationChildAsUsed(node: DeclarationSelectorNode): void { + const identifiers: TSESTree.Identifier[] = []; + switch (node.type) { + case AST_NODE_TYPES.TSInterfaceDeclaration: + case AST_NODE_TYPES.TSTypeAliasDeclaration: + case AST_NODE_TYPES.ClassDeclaration: + case AST_NODE_TYPES.FunctionDeclaration: + case AST_NODE_TYPES.TSDeclareFunction: + case AST_NODE_TYPES.TSEnumDeclaration: + case AST_NODE_TYPES.TSModuleDeclaration: + if (node.id?.type === AST_NODE_TYPES.Identifier) { + identifiers.push(node.id); + } + break; + + case AST_NODE_TYPES.VariableDeclaration: + for (const declaration of node.declarations) { + visitPattern(declaration, pattern => { + identifiers.push(pattern); + }); + } + break; + } + + const scope = context.getScope(); + const { variableScope } = scope; + if (variableScope !== scope) { + for (const id of identifiers) { + const superVar = variableScope.set.get(id.name); if (superVar) { superVar.eslintUsed = true; } } - }, - }; + } else { + for (const id of identifiers) { + context.markVariableAsUsed(id.name); + } + } + } function visitPattern( node: TSESTree.Node, diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts index af6ff089d4b..5dd485d12e3 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts @@ -550,20 +550,6 @@ declare namespace Foo { var baz: string; } console.log(Foo); - `, - // https://github.com/typescript-eslint/typescript-eslint/issues/61 - ` -declare var Foo: { - new (value?: any): Object; - foo(): string; -}; - `, - // https://github.com/typescript-eslint/typescript-eslint/issues/106 - ` -declare class Foo { - constructor(value?: any): Object; - foo(): string; -} `, ` import foo from 'foo'; @@ -631,9 +617,6 @@ export default class Foo { } `, ` -declare function foo(a: number): void; - `, - ` export function foo(): void; export function foo(): void; export function foo(): void {} @@ -788,6 +771,24 @@ export const StyledPayment = styled.div\`\`; import type { foo } from './a'; export type Bar = typeof foo; `, + // https://github.com/typescript-eslint/typescript-eslint/issues/2456 + { + code: ` +interface Foo {} +type Bar = {}; +declare class Clazz {} +declare function func(); +declare enum Enum {} +declare namespace Name {} +declare const v1 = 1; +declare var v2 = 1; +declare let v3 = 1; +declare const { v4 }; +declare const { v4: v5 }; +declare const [v6]; + `, + filename: 'foo.d.ts', + }, ], invalid: [