Skip to content

Commit

Permalink
fix(eslint-plugin): [no-unused-vars] properly handle ambient declarat…
Browse files Browse the repository at this point in the history
…ion exports (#2496)
  • Loading branch information
bradzacher committed Sep 6, 2020
1 parent 916e95a commit 4d3ce5f
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 26 deletions.
1 change: 1 addition & 0 deletions .cspell.json
Expand Up @@ -38,6 +38,7 @@
"words": [
"Airbnb",
"Airbnb's",
"ambiently",
"ASTs",
"autofix",
"autofixers",
Expand Down
93 changes: 84 additions & 9 deletions packages/eslint-plugin/src/rules/no-unused-vars.ts
Expand Up @@ -28,6 +28,7 @@ export default util.createRule<Options, MessageIds>({
defaultOptions: [{}],
create(context) {
const rules = baseRule.create(context);
const filename = context.getFilename();

/**
* Gets a list of TS module definitions for a specified variable.
Expand Down Expand Up @@ -207,19 +208,93 @@ export default util.createRule<Options, MessageIds>({
}
},

// 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,
Expand Down
35 changes: 18 additions & 17 deletions packages/eslint-plugin/tests/rules/no-unused-vars.test.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {}
Expand Down Expand Up @@ -788,6 +771,24 @@ export const StyledPayment = styled.div<StyledPaymentProps>\`\`;
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: [
Expand Down

0 comments on commit 4d3ce5f

Please sign in to comment.