• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import {
2  AST_NODE_TYPES,
3  TSESLint,
4  TSESTree,
5} from '@typescript-eslint/experimental-utils';
6import { PatternVisitor } from '@typescript-eslint/scope-manager';
7import baseRule from 'eslint/lib/rules/no-unused-vars';
8import * as util from '../util';
9
10type MessageIds = util.InferMessageIdsTypeFromRule<typeof baseRule>;
11type Options = util.InferOptionsTypeFromRule<typeof baseRule>;
12
13export default util.createRule<Options, MessageIds>({
14  name: 'no-unused-vars',
15  meta: {
16    type: 'problem',
17    docs: {
18      description: 'Disallow unused variables',
19      category: 'Variables',
20      recommended: 'warn',
21      extendsBaseRule: true,
22    },
23    schema: baseRule.meta.schema,
24    messages: baseRule.meta.messages ?? {
25      unusedVar: "'{{varName}}' is {{action}} but never used{{additional}}.",
26    },
27  },
28  defaultOptions: [{}],
29  create(context) {
30    const rules = baseRule.create(context);
31    const filename = context.getFilename();
32    const MODULE_DECL_CACHE = new Map<TSESTree.TSModuleDeclaration, boolean>();
33
34    /**
35     * Gets a list of TS module definitions for a specified variable.
36     * @param variable eslint-scope variable object.
37     */
38    function getModuleNameDeclarations(
39      variable: TSESLint.Scope.Variable,
40    ): TSESTree.TSModuleDeclaration[] {
41      const moduleDeclarations: TSESTree.TSModuleDeclaration[] = [];
42
43      variable.defs.forEach(def => {
44        if (def.type === 'TSModuleName') {
45          moduleDeclarations.push(def.node);
46        }
47      });
48
49      return moduleDeclarations;
50    }
51
52    /**
53     * Determine if an identifier is referencing an enclosing name.
54     * This only applies to declarations that create their own scope (modules, functions, classes)
55     * @param ref The reference to check.
56     * @param nodes The candidate function nodes.
57     * @returns True if it's a self-reference, false if not.
58     */
59    function isBlockSelfReference(
60      ref: TSESLint.Scope.Reference,
61      nodes: TSESTree.Node[],
62    ): boolean {
63      let scope: TSESLint.Scope.Scope | null = ref.from;
64
65      while (scope) {
66        if (nodes.indexOf(scope.block) >= 0) {
67          return true;
68        }
69
70        scope = scope.upper;
71      }
72
73      return false;
74    }
75
76    function isExported(
77      variable: TSESLint.Scope.Variable,
78      target: AST_NODE_TYPES,
79    ): boolean {
80      // TS will require that all merged namespaces/interfaces are exported, so we only need to find one
81      return variable.defs.some(
82        def =>
83          def.node.type === target &&
84          (def.node.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration ||
85            def.node.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration),
86      );
87    }
88
89    return {
90      ...rules,
91      'TSCallSignatureDeclaration, TSConstructorType, TSConstructSignatureDeclaration, TSDeclareFunction, TSEmptyBodyFunctionExpression, TSFunctionType, TSMethodSignature'(
92        node:
93          | TSESTree.TSCallSignatureDeclaration
94          | TSESTree.TSConstructorType
95          | TSESTree.TSConstructSignatureDeclaration
96          | TSESTree.TSDeclareFunction
97          | TSESTree.TSEmptyBodyFunctionExpression
98          | TSESTree.TSFunctionType
99          | TSESTree.TSMethodSignature,
100      ): void {
101        // function type signature params create variables because they can be referenced within the signature,
102        // but they obviously aren't unused variables for the purposes of this rule.
103        for (const param of node.params) {
104          visitPattern(param, name => {
105            context.markVariableAsUsed(name.name);
106          });
107        }
108      },
109      TSEnumDeclaration(): void {
110        // enum members create variables because they can be referenced within the enum,
111        // but they obviously aren't unused variables for the purposes of this rule.
112        const scope = context.getScope();
113        for (const variable of scope.variables) {
114          context.markVariableAsUsed(variable.name);
115        }
116      },
117      TSMappedType(node): void {
118        // mapped types create a variable for their type name, but it's not necessary to reference it,
119        // so we shouldn't consider it as unused for the purpose of this rule.
120        context.markVariableAsUsed(node.typeParameter.name.name);
121      },
122      TSModuleDeclaration(): void {
123        const childScope = context.getScope();
124        const scope = util.nullThrows(
125          context.getScope().upper,
126          util.NullThrowsReasons.MissingToken(childScope.type, 'upper scope'),
127        );
128        for (const variable of scope.variables) {
129          const moduleNodes = getModuleNameDeclarations(variable);
130
131          if (
132            moduleNodes.length === 0 ||
133            // ignore unreferenced module definitions, as the base rule will report on them
134            variable.references.length === 0 ||
135            // ignore exported nodes
136            isExported(variable, AST_NODE_TYPES.TSModuleDeclaration)
137          ) {
138            continue;
139          }
140
141          // check if the only reference to a module's name is a self-reference in its body
142          // this won't be caught by the base rule because it doesn't understand TS modules
143          const isOnlySelfReferenced = variable.references.every(ref => {
144            return isBlockSelfReference(ref, moduleNodes);
145          });
146
147          if (isOnlySelfReferenced) {
148            context.report({
149              node: variable.identifiers[0],
150              messageId: 'unusedVar',
151              data: {
152                varName: variable.name,
153                action: 'defined',
154                additional: '',
155              },
156            });
157          }
158        }
159      },
160      [[
161        'TSParameterProperty > AssignmentPattern > Identifier.left',
162        'TSParameterProperty > Identifier.parameter',
163      ].join(', ')](node: TSESTree.Identifier): void {
164        // just assume parameter properties are used as property usage tracking is beyond the scope of this rule
165        context.markVariableAsUsed(node.name);
166      },
167      ':matches(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression) > Identifier[name="this"].params'(
168        node: TSESTree.Identifier,
169      ): void {
170        // this parameters should always be considered used as they're pseudo-parameters
171        context.markVariableAsUsed(node.name);
172      },
173      'TSInterfaceDeclaration, TSTypeAliasDeclaration'(
174        node: TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration,
175      ): void {
176        const variable = context.getScope().set.get(node.id.name);
177        if (!variable) {
178          return;
179        }
180        if (
181          variable.references.length === 0 ||
182          // ignore exported nodes
183          isExported(variable, node.type)
184        ) {
185          return;
186        }
187
188        // check if the type is only self-referenced
189        // this won't be caught by the base rule because it doesn't understand self-referencing types
190        const isOnlySelfReferenced = variable.references.every(ref => {
191          if (
192            ref.identifier.range[0] >= node.range[0] &&
193            ref.identifier.range[1] <= node.range[1]
194          ) {
195            return true;
196          }
197          return false;
198        });
199        if (isOnlySelfReferenced) {
200          context.report({
201            node: variable.identifiers[0],
202            messageId: 'unusedVar',
203            data: {
204              varName: variable.name,
205              action: 'defined',
206              additional: '',
207            },
208          });
209        }
210      },
211
212      // declaration file handling
213      [ambientDeclarationSelector(AST_NODE_TYPES.Program, true)](
214        node: DeclarationSelectorNode,
215      ): void {
216        if (!util.isDefinitionFile(filename)) {
217          return;
218        }
219        markDeclarationChildAsUsed(node);
220      },
221
222      // global augmentation can be in any file, and they do not need exports
223      'TSModuleDeclaration[declare = true][global = true]'(): void {
224        context.markVariableAsUsed('global');
225      },
226
227      // children of a namespace that is a child of a declared namespace are auto-exported
228      [ambientDeclarationSelector(
229        'TSModuleDeclaration[declare = true] > TSModuleBlock TSModuleDeclaration > TSModuleBlock',
230        false,
231      )](node: DeclarationSelectorNode): void {
232        markDeclarationChildAsUsed(node);
233      },
234
235      // declared namespace handling
236      [ambientDeclarationSelector(
237        'TSModuleDeclaration[declare = true] > TSModuleBlock',
238        false,
239      )](node: DeclarationSelectorNode): void {
240        const moduleDecl = util.nullThrows(
241          node.parent?.parent,
242          util.NullThrowsReasons.MissingParent,
243        ) as TSESTree.TSModuleDeclaration;
244
245        // declared ambient modules with an `export =` statement will only export that one thing
246        // all other statements are not automatically exported in this case
247        if (
248          moduleDecl.id.type === AST_NODE_TYPES.Literal &&
249          checkModuleDeclForExportEquals(moduleDecl)
250        ) {
251          return;
252        }
253
254        markDeclarationChildAsUsed(node);
255      },
256    };
257
258    function checkModuleDeclForExportEquals(
259      node: TSESTree.TSModuleDeclaration,
260    ): boolean {
261      const cached = MODULE_DECL_CACHE.get(node);
262      if (cached != null) {
263        return cached;
264      }
265
266      for (const statement of node.body?.body ?? []) {
267        if (statement.type === AST_NODE_TYPES.TSExportAssignment) {
268          MODULE_DECL_CACHE.set(node, true);
269          return true;
270        }
271      }
272
273      MODULE_DECL_CACHE.set(node, false);
274      return false;
275    }
276
277    type DeclarationSelectorNode =
278      | TSESTree.TSInterfaceDeclaration
279      | TSESTree.TSTypeAliasDeclaration
280      | TSESTree.ClassDeclaration
281      | TSESTree.FunctionDeclaration
282      | TSESTree.TSDeclareFunction
283      | TSESTree.TSEnumDeclaration
284      | TSESTree.TSModuleDeclaration
285      | TSESTree.VariableDeclaration;
286    function ambientDeclarationSelector(
287      parent: string,
288      childDeclare: boolean,
289    ): string {
290      return [
291        // Types are ambiently exported
292        `${parent} > :matches(${[
293          AST_NODE_TYPES.TSInterfaceDeclaration,
294          AST_NODE_TYPES.TSTypeAliasDeclaration,
295        ].join(', ')})`,
296        // Value things are ambiently exported if they are "declare"d
297        `${parent} > :matches(${[
298          AST_NODE_TYPES.ClassDeclaration,
299          AST_NODE_TYPES.TSDeclareFunction,
300          AST_NODE_TYPES.TSEnumDeclaration,
301          AST_NODE_TYPES.TSModuleDeclaration,
302          AST_NODE_TYPES.VariableDeclaration,
303        ].join(', ')})${childDeclare ? '[declare = true]' : ''}`,
304      ].join(', ');
305    }
306    function markDeclarationChildAsUsed(node: DeclarationSelectorNode): void {
307      const identifiers: TSESTree.Identifier[] = [];
308      switch (node.type) {
309        case AST_NODE_TYPES.TSInterfaceDeclaration:
310        case AST_NODE_TYPES.TSTypeAliasDeclaration:
311        case AST_NODE_TYPES.ClassDeclaration:
312        case AST_NODE_TYPES.FunctionDeclaration:
313        case AST_NODE_TYPES.TSDeclareFunction:
314        case AST_NODE_TYPES.TSEnumDeclaration:
315        case AST_NODE_TYPES.TSModuleDeclaration:
316          if (node.id?.type === AST_NODE_TYPES.Identifier) {
317            identifiers.push(node.id);
318          }
319          break;
320
321        case AST_NODE_TYPES.VariableDeclaration:
322          for (const declaration of node.declarations) {
323            visitPattern(declaration, pattern => {
324              identifiers.push(pattern);
325            });
326          }
327          break;
328      }
329
330      let scope = context.getScope();
331      const shouldUseUpperScope = [
332        AST_NODE_TYPES.TSModuleDeclaration,
333        AST_NODE_TYPES.TSDeclareFunction,
334      ].includes(node.type);
335
336      if (scope.variableScope !== scope) {
337        scope = scope.variableScope;
338      } else if (shouldUseUpperScope && scope.upper) {
339        scope = scope.upper;
340      }
341
342      for (const id of identifiers) {
343        const superVar = scope.set.get(id.name);
344        if (superVar) {
345          superVar.eslintUsed = true;
346        }
347      }
348    }
349
350    function visitPattern(
351      node: TSESTree.Node,
352      cb: (node: TSESTree.Identifier) => void,
353    ): void {
354      const visitor = new PatternVisitor({}, node, cb);
355      visitor.visit(node);
356    }
357  },
358});
359
360/*
361
362###### TODO ######
363
364Edge cases that aren't currently handled due to laziness and them being super edgy edge cases
365
366
367--- function params referenced in typeof type refs in the function declaration ---
368--- NOTE - TS gets these cases wrong
369
370function _foo(
371  arg: number // arg should be unused
372): typeof arg {
373  return 1 as any;
374}
375
376function _bar(
377  arg: number, // arg should be unused
378  _arg2: typeof arg,
379) {}
380
381
382--- function names referenced in typeof type refs in the function declaration ---
383--- NOTE - TS gets these cases right
384
385function foo( // foo should be unused
386): typeof foo {
387    return 1 as any;
388}
389
390function bar( // bar should be unused
391  _arg: typeof bar
392) {}
393
394*/
395
396/*
397
398###### TODO ######
399
400We currently extend base `no-unused-vars` implementation because it's easier and lighter-weight.
401
402Because of this, there are a few false-negatives which won't get caught.
403We could fix these if we fork the base rule; but that's a lot of code (~650 lines) to add in.
404I didn't want to do that just yet without some real-world issues, considering these are pretty rare edge-cases.
405
406These cases are mishandled because the base rule assumes that each variable has one def, but type-value shadowing
407creates a variable with two defs
408
409--- type-only or value-only references to type/value shadowed variables ---
410--- NOTE - TS gets these cases wrong
411
412type T = 1;
413const T = 2; // this T should be unused
414
415type U = T; // this U should be unused
416const U = 3;
417
418const _V = U;
419
420
421--- partially exported type/value shadowed variables ---
422--- NOTE - TS gets these cases wrong
423
424export interface Foo {}
425const Foo = 1; // this Foo should be unused
426
427interface Bar {} // this Bar should be unused
428export const Bar = 1;
429
430*/
431