From a8227a6185dd24de4bfc7d766931643871155021 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 23 Nov 2020 16:45:50 -0800 Subject: [PATCH] feat(eslint-plugin): [no-unused-vars] fork the base rule (#2768) Fixes #2782 Fixes #2714 Fixes #2648 --- .../eslint-plugin/src/rules/no-unused-vars.ts | 499 ++-- .../src/util/collectUnusedVariables.ts | 751 +++++ packages/eslint-plugin/src/util/index.ts | 1 + .../no-unused-vars-eslint.test.ts | 2457 +++++++++++++++++ .../no-unused-vars.test.ts | 68 +- .../eslint-plugin/typings/eslint-rules.d.ts | 14 + .../src/ast-utils/predicates.ts | 22 + .../src/eslint-utils/InferTypesFromRule.ts | 21 +- .../experimental-utils/src/ts-eslint/Rule.ts | 9 +- .../src/ts-eslint/RuleTester.ts | 16 +- .../experimental-utils/src/ts-eslint/Scope.ts | 75 +- .../src/referencer/VisitorBase.ts | 13 +- packages/types/src/ts-estree.ts | 6 + 13 files changed, 3719 insertions(+), 233 deletions(-) create mode 100644 packages/eslint-plugin/src/util/collectUnusedVariables.ts create mode 100644 packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts rename packages/eslint-plugin/tests/rules/{ => no-unused-vars}/no-unused-vars.test.ts (95%) diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index 8753452dd09..aac30b87a9e 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -4,11 +4,33 @@ import { TSESTree, } from '@typescript-eslint/experimental-utils'; import { PatternVisitor } from '@typescript-eslint/scope-manager'; -import baseRule from 'eslint/lib/rules/no-unused-vars'; +import { getNameLocationInGlobalDirectiveComment } from 'eslint/lib/rules/utils/ast-utils'; import * as util from '../util'; -type MessageIds = util.InferMessageIdsTypeFromRule; -type Options = util.InferOptionsTypeFromRule; +export type MessageIds = 'unusedVar'; +export type Options = [ + | 'all' + | 'local' + | { + vars?: 'all' | 'local'; + varsIgnorePattern?: string; + args?: 'all' | 'after-used' | 'none'; + ignoreRestSiblings?: boolean; + argsIgnorePattern?: string; + caughtErrors?: 'all' | 'none'; + caughtErrorsIgnorePattern?: string; + }, +]; + +interface TranslatedOptions { + vars: 'all' | 'local'; + varsIgnorePattern?: RegExp; + args: 'all' | 'after-used' | 'none'; + ignoreRestSiblings: boolean; + argsIgnorePattern?: RegExp; + caughtErrors: 'all' | 'none'; + caughtErrorsIgnorePattern?: RegExp; +} export default util.createRule({ name: 'no-unused-vars', @@ -20,195 +42,211 @@ export default util.createRule({ recommended: 'warn', extendsBaseRule: true, }, - schema: baseRule.meta.schema, - messages: baseRule.meta.messages ?? { + schema: [ + { + oneOf: [ + { + enum: ['all', 'local'], + }, + { + type: 'object', + properties: { + vars: { + enum: ['all', 'local'], + }, + varsIgnorePattern: { + type: 'string', + }, + args: { + enum: ['all', 'after-used', 'none'], + }, + ignoreRestSiblings: { + type: 'boolean', + }, + argsIgnorePattern: { + type: 'string', + }, + caughtErrors: { + enum: ['all', 'none'], + }, + caughtErrorsIgnorePattern: { + type: 'string', + }, + }, + additionalProperties: false, + }, + ], + }, + ], + messages: { unusedVar: "'{{varName}}' is {{action}} but never used{{additional}}.", }, }, defaultOptions: [{}], create(context) { - const rules = baseRule.create(context); const filename = context.getFilename(); + const sourceCode = context.getSourceCode(); const MODULE_DECL_CACHE = new Map(); - /** - * Gets a list of TS module definitions for a specified variable. - * @param variable eslint-scope variable object. - */ - function getModuleNameDeclarations( - variable: TSESLint.Scope.Variable, - ): TSESTree.TSModuleDeclaration[] { - const moduleDeclarations: TSESTree.TSModuleDeclaration[] = []; - - variable.defs.forEach(def => { - if (def.type === 'TSModuleName') { - moduleDeclarations.push(def.node); - } - }); - - return moduleDeclarations; - } + const options = ((): TranslatedOptions => { + const options: TranslatedOptions = { + vars: 'all', + args: 'after-used', + ignoreRestSiblings: false, + caughtErrors: 'none', + }; + + const firstOption = context.options[0]; + + if (firstOption) { + if (typeof firstOption === 'string') { + options.vars = firstOption; + } else { + options.vars = firstOption.vars ?? options.vars; + options.args = firstOption.args ?? options.args; + options.ignoreRestSiblings = + firstOption.ignoreRestSiblings ?? options.ignoreRestSiblings; + options.caughtErrors = + firstOption.caughtErrors ?? options.caughtErrors; + + if (firstOption.varsIgnorePattern) { + options.varsIgnorePattern = new RegExp( + firstOption.varsIgnorePattern, + 'u', + ); + } - /** - * Determine if an identifier is referencing an enclosing name. - * This only applies to declarations that create their own scope (modules, functions, classes) - * @param ref The reference to check. - * @param nodes The candidate function nodes. - * @returns True if it's a self-reference, false if not. - */ - function isBlockSelfReference( - ref: TSESLint.Scope.Reference, - nodes: TSESTree.Node[], - ): boolean { - let scope: TSESLint.Scope.Scope | null = ref.from; + if (firstOption.argsIgnorePattern) { + options.argsIgnorePattern = new RegExp( + firstOption.argsIgnorePattern, + 'u', + ); + } - while (scope) { - if (nodes.indexOf(scope.block) >= 0) { - return true; + if (firstOption.caughtErrorsIgnorePattern) { + options.caughtErrorsIgnorePattern = new RegExp( + firstOption.caughtErrorsIgnorePattern, + 'u', + ); + } } - - scope = scope.upper; } + return options; + })(); + + function collectUnusedVariables(): TSESLint.Scope.Variable[] { + /** + * Determines if a variable has a sibling rest property + * @param variable eslint-scope variable object. + * @returns True if the variable is exported, false if not. + */ + function hasRestSpreadSibling( + variable: TSESLint.Scope.Variable, + ): boolean { + if (options.ignoreRestSiblings) { + return variable.defs.some(def => { + const propertyNode = def.name.parent!; + const patternNode = propertyNode.parent!; + + return ( + propertyNode.type === AST_NODE_TYPES.Property && + patternNode.type === AST_NODE_TYPES.ObjectPattern && + patternNode.properties[patternNode.properties.length - 1].type === + AST_NODE_TYPES.RestElement + ); + }); + } - return false; - } + return false; + } - function isExported( - variable: TSESLint.Scope.Variable, - target: AST_NODE_TYPES, - ): boolean { - // TS will require that all merged namespaces/interfaces are exported, so we only need to find one - return variable.defs.some( - def => - def.node.type === target && - (def.node.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration || - def.node.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration), - ); - } + /** + * Checks whether the given variable is after the last used parameter. + * @param variable The variable to check. + * @returns `true` if the variable is defined after the last used parameter. + */ + function isAfterLastUsedArg(variable: TSESLint.Scope.Variable): boolean { + const def = variable.defs[0]; + const params = context.getDeclaredVariables(def.node); + const posteriorParams = params.slice(params.indexOf(variable) + 1); + + // If any used parameters occur after this parameter, do not report. + return !posteriorParams.some( + v => v.references.length > 0 || v.eslintUsed, + ); + } - return { - ...rules, - 'TSCallSignatureDeclaration, TSConstructorType, TSConstructSignatureDeclaration, TSDeclareFunction, TSEmptyBodyFunctionExpression, TSFunctionType, TSMethodSignature'( - node: - | TSESTree.TSCallSignatureDeclaration - | TSESTree.TSConstructorType - | TSESTree.TSConstructSignatureDeclaration - | TSESTree.TSDeclareFunction - | TSESTree.TSEmptyBodyFunctionExpression - | TSESTree.TSFunctionType - | TSESTree.TSMethodSignature, - ): void { - // function type signature params create variables because they can be referenced within the signature, - // but they obviously aren't unused variables for the purposes of this rule. - for (const param of node.params) { - visitPattern(param, name => { - context.markVariableAsUsed(name.name); - }); + const unusedVariablesOriginal = util.collectUnusedVariables(context); + const unusedVariablesReturn: TSESLint.Scope.Variable[] = []; + for (const variable of unusedVariablesOriginal) { + // explicit global variables don't have definitions. + if (variable.defs.length === 0) { + unusedVariablesReturn.push(variable); + continue; } - }, - TSEnumDeclaration(): void { - // enum members create variables because they can be referenced within the enum, - // but they obviously aren't unused variables for the purposes of this rule. - const scope = context.getScope(); - for (const variable of scope.variables) { - context.markVariableAsUsed(variable.name); + const def = variable.defs[0]; + + if ( + variable.scope.type === TSESLint.Scope.ScopeType.global && + options.vars === 'local' + ) { + // skip variables in the global scope if configured to + continue; } - }, - TSMappedType(node): void { - // mapped types create a variable for their type name, but it's not necessary to reference it, - // so we shouldn't consider it as unused for the purpose of this rule. - context.markVariableAsUsed(node.typeParameter.name.name); - }, - TSModuleDeclaration(): void { - const childScope = context.getScope(); - const scope = util.nullThrows( - context.getScope().upper, - util.NullThrowsReasons.MissingToken(childScope.type, 'upper scope'), - ); - for (const variable of scope.variables) { - const moduleNodes = getModuleNameDeclarations(variable); + // skip catch variables + if (def.type === TSESLint.Scope.DefinitionType.CatchClause) { + if (options.caughtErrors === 'none') { + continue; + } + // skip ignored parameters if ( - moduleNodes.length === 0 || - // ignore unreferenced module definitions, as the base rule will report on them - variable.references.length === 0 || - // ignore exported nodes - isExported(variable, AST_NODE_TYPES.TSModuleDeclaration) + 'name' in def.name && + options.caughtErrorsIgnorePattern?.test(def.name.name) ) { continue; } - - // check if the only reference to a module's name is a self-reference in its body - // this won't be caught by the base rule because it doesn't understand TS modules - const isOnlySelfReferenced = variable.references.every(ref => { - return isBlockSelfReference(ref, moduleNodes); - }); - - if (isOnlySelfReferenced) { - context.report({ - node: variable.identifiers[0], - messageId: 'unusedVar', - data: { - varName: variable.name, - action: 'defined', - additional: '', - }, - }); - } - } - }, - [[ - 'TSParameterProperty > AssignmentPattern > Identifier.left', - 'TSParameterProperty > Identifier.parameter', - ].join(', ')](node: TSESTree.Identifier): void { - // just assume parameter properties are used as property usage tracking is beyond the scope of this rule - context.markVariableAsUsed(node.name); - }, - ':matches(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression) > Identifier[name="this"].params'( - node: TSESTree.Identifier, - ): void { - // this parameters should always be considered used as they're pseudo-parameters - context.markVariableAsUsed(node.name); - }, - 'TSInterfaceDeclaration, TSTypeAliasDeclaration'( - node: TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration, - ): void { - const variable = context.getScope().set.get(node.id.name); - if (!variable) { - return; - } - if ( - variable.references.length === 0 || - // ignore exported nodes - isExported(variable, node.type) - ) { - return; } - // check if the type is only self-referenced - // this won't be caught by the base rule because it doesn't understand self-referencing types - const isOnlySelfReferenced = variable.references.every(ref => { + if (def.type === TSESLint.Scope.DefinitionType.Parameter) { + // if "args" option is "none", skip any parameter + if (options.args === 'none') { + continue; + } + // skip ignored parameters if ( - ref.identifier.range[0] >= node.range[0] && - ref.identifier.range[1] <= node.range[1] + 'name' in def.name && + options.argsIgnorePattern?.test(def.name.name) ) { - return true; + continue; + } + // if "args" option is "after-used", skip used variables + if ( + options.args === 'after-used' && + util.isFunction(def.name.parent) && + !isAfterLastUsedArg(variable) + ) { + continue; + } + } else { + // skip ignored variables + if ( + 'name' in def.name && + options.varsIgnorePattern?.test(def.name.name) + ) { + continue; } - return false; - }); - if (isOnlySelfReferenced) { - context.report({ - node: variable.identifiers[0], - messageId: 'unusedVar', - data: { - varName: variable.name, - action: 'defined', - additional: '', - }, - }); } - }, + if (!hasRestSpreadSibling(variable)) { + unusedVariablesReturn.push(variable); + } + } + + return unusedVariablesReturn; + } + + return { // declaration file handling [ambientDeclarationSelector(AST_NODE_TYPES.Program, true)]( node: DeclarationSelectorNode, @@ -219,11 +257,6 @@ export default util.createRule({ markDeclarationChildAsUsed(node); }, - // global augmentation can be in any file, and they do not need exports - 'TSModuleDeclaration[declare = true][global = true]'(): void { - context.markVariableAsUsed('global'); - }, - // children of a namespace that is a child of a declared namespace are auto-exported [ambientDeclarationSelector( 'TSModuleDeclaration[declare = true] > TSModuleBlock TSModuleDeclaration > TSModuleBlock', @@ -253,6 +286,111 @@ export default util.createRule({ markDeclarationChildAsUsed(node); }, + + // collect + 'Program:exit'(programNode): void { + /** + * Generates the message data about the variable being defined and unused, + * including the ignore pattern if configured. + * @param unusedVar eslint-scope variable object. + * @returns The message data to be used with this unused variable. + */ + function getDefinedMessageData( + unusedVar: TSESLint.Scope.Variable, + ): Record { + const defType = unusedVar?.defs[0]?.type; + let type; + let pattern; + + if ( + defType === TSESLint.Scope.DefinitionType.CatchClause && + options.caughtErrorsIgnorePattern + ) { + type = 'args'; + pattern = options.caughtErrorsIgnorePattern.toString(); + } else if ( + defType === TSESLint.Scope.DefinitionType.Parameter && + options.argsIgnorePattern + ) { + type = 'args'; + pattern = options.argsIgnorePattern.toString(); + } else if ( + defType !== TSESLint.Scope.DefinitionType.Parameter && + options.varsIgnorePattern + ) { + type = 'vars'; + pattern = options.varsIgnorePattern.toString(); + } + + const additional = type + ? `. Allowed unused ${type} must match ${pattern}` + : ''; + + return { + varName: unusedVar.name, + action: 'defined', + additional, + }; + } + + /** + * Generate the warning message about the variable being + * assigned and unused, including the ignore pattern if configured. + * @param unusedVar eslint-scope variable object. + * @returns The message data to be used with this unused variable. + */ + function getAssignedMessageData( + unusedVar: TSESLint.Scope.Variable, + ): Record { + const additional = options.varsIgnorePattern + ? `. Allowed unused vars must match ${options.varsIgnorePattern.toString()}` + : ''; + + return { + varName: unusedVar.name, + action: 'assigned a value', + additional, + }; + } + + const unusedVars = collectUnusedVariables(); + + for (let i = 0, l = unusedVars.length; i < l; ++i) { + const unusedVar = unusedVars[i]; + + // Report the first declaration. + if (unusedVar.defs.length > 0) { + context.report({ + node: unusedVar.references.length + ? unusedVar.references[unusedVar.references.length - 1] + .identifier + : unusedVar.identifiers[0], + messageId: 'unusedVar', + data: unusedVar.references.some(ref => ref.isWrite()) + ? getAssignedMessageData(unusedVar) + : getDefinedMessageData(unusedVar), + }); + + // If there are no regular declaration, report the first `/*globals*/` comment directive. + } else if ( + 'eslintExplicitGlobalComments' in unusedVar && + unusedVar.eslintExplicitGlobalComments + ) { + const directiveComment = unusedVar.eslintExplicitGlobalComments[0]; + + context.report({ + node: programNode, + loc: getNameLocationInGlobalDirectiveComment( + sourceCode, + directiveComment, + unusedVar.name, + ), + messageId: 'unusedVar', + data: getDefinedMessageData(unusedVar), + }); + } + } + }, }; function checkModuleDeclForExportEquals( @@ -391,6 +529,31 @@ function bar( // bar should be unused _arg: typeof bar ) {} + +--- if an interface is merged into a namespace --- +--- NOTE - TS gets these cases wrong + +namespace Test { + interface Foo { // Foo should be unused here + a: string; + } + export namespace Foo { + export type T = 'b'; + } +} +type T = Test.Foo; // Error: Namespace 'Test' has no exported member 'Foo'. + + +namespace Test { + export interface Foo { + a: string; + } + namespace Foo { // Foo should be unused here + export type T = 'b'; + } +} +type T = Test.Foo.T; // Error: Namespace 'Test' has no exported member 'Foo'. + */ /* diff --git a/packages/eslint-plugin/src/util/collectUnusedVariables.ts b/packages/eslint-plugin/src/util/collectUnusedVariables.ts new file mode 100644 index 00000000000..bd8ed859b6d --- /dev/null +++ b/packages/eslint-plugin/src/util/collectUnusedVariables.ts @@ -0,0 +1,751 @@ +import { + AST_NODE_TYPES, + TSESLint, + TSESTree, +} from '@typescript-eslint/experimental-utils'; +import { ImplicitLibVariable } from '@typescript-eslint/scope-manager'; +import { Visitor } from '@typescript-eslint/scope-manager/dist/referencer/Visitor'; +import * as util from '.'; + +class UnusedVarsVisitor< + TMessageIds extends string, + TOptions extends readonly unknown[] +> extends Visitor { + private static readonly RESULTS_CACHE = new WeakMap< + TSESTree.Program, + ReadonlySet + >(); + + readonly #scopeManager: TSESLint.Scope.ScopeManager; + // readonly #unusedVariables = new Set(); + + private constructor(context: TSESLint.RuleContext) { + super({ + visitChildrenEvenIfSelectorExists: true, + }); + + this.#scopeManager = util.nullThrows( + context.getSourceCode().scopeManager, + 'Missing required scope manager', + ); + } + + public static collectUnusedVariables< + TMessageIds extends string, + TOptions extends readonly unknown[] + >( + context: TSESLint.RuleContext, + ): ReadonlySet { + const program = context.getSourceCode().ast; + const cached = this.RESULTS_CACHE.get(program); + if (cached) { + return cached; + } + + const visitor = new this(context); + visitor.visit(program); + + const unusedVars = visitor.collectUnusedVariables( + visitor.getScope(program), + ); + this.RESULTS_CACHE.set(program, unusedVars); + return unusedVars; + } + + private collectUnusedVariables( + scope: TSESLint.Scope.Scope, + unusedVariables = new Set(), + ): ReadonlySet { + for (const variable of scope.variables) { + if ( + // skip function expression names, + scope.functionExpressionScope || + // variables marked with markVariableAsUsed(), + variable.eslintUsed || + // implicit lib variables (from @typescript-eslint/scope-manager), + variable instanceof ImplicitLibVariable || + // basic exported variables + isExported(variable) || + // variables implicitly exported via a merged declaration + isMergableExported(variable) || + // used variables + isUsedVariable(variable) + ) { + continue; + } + + unusedVariables.add(variable); + } + + for (const childScope of scope.childScopes) { + this.collectUnusedVariables(childScope, unusedVariables); + } + + return unusedVariables; + } + + //#region HELPERS + + private getScope( + currentNode: TSESTree.Node, + ): T { + // On Program node, get the outermost scope to avoid return Node.js special function scope or ES modules scope. + const inner = currentNode.type !== AST_NODE_TYPES.Program; + + let node: TSESTree.Node | undefined = currentNode; + while (node) { + const scope = this.#scopeManager.acquire(node, inner); + + if (scope) { + if (scope.type === 'function-expression-name') { + return scope.childScopes[0] as T; + } + return scope as T; + } + + node = node.parent; + } + + return this.#scopeManager.scopes[0] as T; + } + + private markVariableAsUsed( + variableOrIdentifier: TSESLint.Scope.Variable | TSESTree.Identifier, + ): void; + private markVariableAsUsed(name: string, parent: TSESTree.Node): void; + private markVariableAsUsed( + variableOrIdentifierOrName: + | TSESLint.Scope.Variable + | TSESTree.Identifier + | string, + parent?: TSESTree.Node, + ): void { + if ( + typeof variableOrIdentifierOrName !== 'string' && + !('type' in variableOrIdentifierOrName) + ) { + variableOrIdentifierOrName.eslintUsed = true; + return; + } + + let name: string; + let node: TSESTree.Node; + if (typeof variableOrIdentifierOrName === 'string') { + name = variableOrIdentifierOrName; + node = parent!; + } else { + name = variableOrIdentifierOrName.name; + node = variableOrIdentifierOrName; + } + + let currentScope: TSESLint.Scope.Scope | null = this.getScope(node); + while (currentScope) { + const variable = currentScope.variables.find( + scopeVar => scopeVar.name === name, + ); + + if (variable) { + variable.eslintUsed = true; + return; + } + + currentScope = currentScope.upper; + } + } + + private visitFunction( + node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression, + ): void { + const scope = this.getScope(node); + // skip implicit "arguments" variable + const variable = scope.set.get('arguments'); + if (variable?.defs.length === 0) { + this.markVariableAsUsed(variable); + } + } + + private visitFunctionTypeSignature( + node: + | TSESTree.TSCallSignatureDeclaration + | TSESTree.TSConstructorType + | TSESTree.TSConstructSignatureDeclaration + | TSESTree.TSDeclareFunction + | TSESTree.TSEmptyBodyFunctionExpression + | TSESTree.TSFunctionType + | TSESTree.TSMethodSignature, + ): void { + // function type signature params create variables because they can be referenced within the signature, + // but they obviously aren't unused variables for the purposes of this rule. + for (const param of node.params) { + this.visitPattern(param, name => { + this.markVariableAsUsed(name); + }); + } + } + + private visitSetter( + node: TSESTree.MethodDefinition | TSESTree.Property, + ): void { + if (node.kind === 'set') { + // ignore setter parameters because they're syntactically required to exist + for (const param of (node.value as TSESTree.FunctionLike).params) { + this.visitPattern(param, id => { + this.markVariableAsUsed(id); + }); + } + } + } + + //#endregion HELPERS + + //#region VISITORS + // NOTE - This is a simple visitor - meaning it does not support selectors + + protected ClassDeclaration(node: TSESTree.ClassDeclaration): void { + // skip a variable of class itself name in the class scope + const scope = this.getScope(node); + for (const variable of scope.variables) { + if (variable.identifiers[0] === scope.block.id) { + this.markVariableAsUsed(variable); + return; + } + } + } + + protected FunctionDeclaration = this.visitFunction; + + protected FunctionExpression = this.visitFunction; + + protected ForInStatement(node: TSESTree.ForInStatement): void { + /** + * (Brad Zacher): I hate that this has to exist. + * But it is required for compat with the base ESLint rule. + * + * In 2015, ESLint decided to add an exception for these two specific cases + * ``` + * for (var key in object) return; + * + * var key; + * for (key in object) return; + * ``` + * + * I disagree with it, but what are you going to do... + * + * https://github.com/eslint/eslint/issues/2342 + */ + + let idOrVariable; + if (node.left.type === AST_NODE_TYPES.VariableDeclaration) { + const variable = this.#scopeManager.getDeclaredVariables(node.left)[0]; + if (!variable) { + return; + } + idOrVariable = variable; + } + if (node.left.type === AST_NODE_TYPES.Identifier) { + idOrVariable = node.left; + } + + if (idOrVariable == null) { + return; + } + + let body = node.body; + if (node.body.type === AST_NODE_TYPES.BlockStatement) { + if (node.body.body.length !== 1) { + return; + } + body = node.body.body[0]; + } + + if (body.type !== AST_NODE_TYPES.ReturnStatement) { + return; + } + + this.markVariableAsUsed(idOrVariable); + } + + protected Identifier(node: TSESTree.Identifier): void { + const scope = this.getScope(node); + if ( + scope.type === TSESLint.Scope.ScopeType.function && + node.name === 'this' + ) { + // this parameters should always be considered used as they're pseudo-parameters + if ('params' in scope.block && scope.block.params.includes(node)) { + this.markVariableAsUsed(node); + } + } + } + + protected MethodDefinition = this.visitSetter; + + protected Property = this.visitSetter; + + protected TSCallSignatureDeclaration = this.visitFunctionTypeSignature; + + protected TSConstructorType = this.visitFunctionTypeSignature; + + protected TSConstructSignatureDeclaration = this.visitFunctionTypeSignature; + + protected TSDeclareFunction = this.visitFunctionTypeSignature; + + protected TSEmptyBodyFunctionExpression = this.visitFunctionTypeSignature; + + protected TSEnumDeclaration(node: TSESTree.TSEnumDeclaration): void { + // enum members create variables because they can be referenced within the enum, + // but they obviously aren't unused variables for the purposes of this rule. + const scope = this.getScope(node); + for (const variable of scope.variables) { + this.markVariableAsUsed(variable); + } + } + + protected TSFunctionType = this.visitFunctionTypeSignature; + + protected TSMappedType(node: TSESTree.TSMappedType): void { + // mapped types create a variable for their type name, but it's not necessary to reference it, + // so we shouldn't consider it as unused for the purpose of this rule. + this.markVariableAsUsed(node.typeParameter.name); + } + + protected TSMethodSignature = this.visitFunctionTypeSignature; + + protected TSModuleDeclaration(node: TSESTree.TSModuleDeclaration): void { + // global augmentation can be in any file, and they do not need exports + if (node.global === true) { + this.markVariableAsUsed('global', node.parent!); + } + } + + protected TSParameterProperty(node: TSESTree.TSParameterProperty): void { + let identifier: TSESTree.Identifier | null = null; + switch (node.parameter.type) { + case AST_NODE_TYPES.AssignmentPattern: + if (node.parameter.left.type === AST_NODE_TYPES.Identifier) { + identifier = node.parameter.left; + } + break; + + case AST_NODE_TYPES.Identifier: + identifier = node.parameter; + break; + } + + if (identifier) { + this.markVariableAsUsed(identifier); + } + } + + //#endregion VISITORS +} + +//#region private helpers + +/** + * Checks the position of given nodes. + * @param inner A node which is expected as inside. + * @param outer A node which is expected as outside. + * @returns `true` if the `inner` node exists in the `outer` node. + */ +function isInside(inner: TSESTree.Node, outer: TSESTree.Node): boolean { + return inner.range[0] >= outer.range[0] && inner.range[1] <= outer.range[1]; +} + +/** + * Determine if an identifier is referencing an enclosing name. + * This only applies to declarations that create their own scope (modules, functions, classes) + * @param ref The reference to check. + * @param nodes The candidate function nodes. + * @returns True if it's a self-reference, false if not. + */ +function isSelfReference( + ref: TSESLint.Scope.Reference, + nodes: Set, +): boolean { + let scope: TSESLint.Scope.Scope | null = ref.from; + + while (scope) { + if (nodes.has(scope.block)) { + return true; + } + + scope = scope.upper; + } + + return false; +} + +const MERGABLE_TYPES = new Set([ + AST_NODE_TYPES.TSInterfaceDeclaration, + AST_NODE_TYPES.TSTypeAliasDeclaration, + AST_NODE_TYPES.TSModuleDeclaration, + AST_NODE_TYPES.ClassDeclaration, + AST_NODE_TYPES.FunctionDeclaration, +]); +/** + * Determine if the variable is directly exported + * @param variable the variable to check + * @param target the type of node that is expected to be exported + */ +function isMergableExported(variable: TSESLint.Scope.Variable): boolean { + // If all of the merged things are of the same type, TS will error if not all of them are exported - so we only need to find one + for (const def of variable.defs) { + // parameters can never be exported. + // their `node` prop points to the function decl, which can be exported + // so we need to special case them + if (def.type === TSESLint.Scope.DefinitionType.Parameter) { + continue; + } + + if ( + (MERGABLE_TYPES.has(def.node.type) && + def.node.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration) || + def.node.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration + ) { + return true; + } + } + + return false; +} + +/** + * Determines if a given variable is being exported from a module. + * @param variable eslint-scope variable object. + * @returns True if the variable is exported, false if not. + */ +function isExported(variable: TSESLint.Scope.Variable): boolean { + const definition = variable.defs[0]; + + if (definition) { + let node = definition.node; + + if (node.type === AST_NODE_TYPES.VariableDeclarator) { + node = node.parent!; + } else if (definition.type === TSESLint.Scope.DefinitionType.Parameter) { + return false; + } + + return node.parent!.type.indexOf('Export') === 0; + } + return false; +} + +/** + * Determines if the variable is used. + * @param variable The variable to check. + * @returns True if the variable is used + */ +function isUsedVariable(variable: TSESLint.Scope.Variable): boolean { + /** + * Gets a list of function definitions for a specified variable. + * @param variable eslint-scope variable object. + * @returns Function nodes. + */ + function getFunctionDefinitions( + variable: TSESLint.Scope.Variable, + ): Set { + const functionDefinitions = new Set(); + + variable.defs.forEach(def => { + // FunctionDeclarations + if (def.type === TSESLint.Scope.DefinitionType.FunctionName) { + functionDefinitions.add(def.node); + } + + // FunctionExpressions + if ( + def.type === TSESLint.Scope.DefinitionType.Variable && + (def.node.init?.type === AST_NODE_TYPES.FunctionExpression || + def.node.init?.type === AST_NODE_TYPES.ArrowFunctionExpression) + ) { + functionDefinitions.add(def.node.init); + } + }); + return functionDefinitions; + } + + function getTypeDeclarations( + variable: TSESLint.Scope.Variable, + ): Set { + const nodes = new Set(); + + variable.defs.forEach(def => { + if ( + def.node.type === AST_NODE_TYPES.TSInterfaceDeclaration || + def.node.type === AST_NODE_TYPES.TSTypeAliasDeclaration + ) { + nodes.add(def.node); + } + }); + + return nodes; + } + + function getModuleDeclarations( + variable: TSESLint.Scope.Variable, + ): Set { + const nodes = new Set(); + + variable.defs.forEach(def => { + if (def.node.type === AST_NODE_TYPES.TSModuleDeclaration) { + nodes.add(def.node); + } + }); + + return nodes; + } + + /** + * Checks if the ref is contained within one of the given nodes + */ + function isInsideOneOf( + ref: TSESLint.Scope.Reference, + nodes: Set, + ): boolean { + for (const node of nodes) { + if (isInside(ref.identifier, node)) { + return true; + } + } + + return false; + } + + /** + * If a given reference is left-hand side of an assignment, this gets + * the right-hand side node of the assignment. + * + * In the following cases, this returns null. + * + * - The reference is not the LHS of an assignment expression. + * - The reference is inside of a loop. + * - The reference is inside of a function scope which is different from + * the declaration. + * @param ref A reference to check. + * @param prevRhsNode The previous RHS node. + * @returns The RHS node or null. + */ + function getRhsNode( + ref: TSESLint.Scope.Reference, + prevRhsNode: TSESTree.Node | null, + ): TSESTree.Node | null { + /** + * Checks whether the given node is in a loop or not. + * @param node The node to check. + * @returns `true` if the node is in a loop. + */ + function isInLoop(node: TSESTree.Node): boolean { + let currentNode: TSESTree.Node | undefined = node; + while (currentNode) { + if (util.isFunction(currentNode)) { + break; + } + + if (util.isLoop(currentNode)) { + return true; + } + + currentNode = currentNode.parent; + } + + return false; + } + + const id = ref.identifier; + const parent = id.parent!; + const grandparent = parent.parent!; + const refScope = ref.from.variableScope; + const varScope = ref.resolved!.scope.variableScope; + const canBeUsedLater = refScope !== varScope || isInLoop(id); + + /* + * Inherits the previous node if this reference is in the node. + * This is for `a = a + a`-like code. + */ + if (prevRhsNode && isInside(id, prevRhsNode)) { + return prevRhsNode; + } + + if ( + parent.type === AST_NODE_TYPES.AssignmentExpression && + grandparent.type === AST_NODE_TYPES.ExpressionStatement && + id === parent.left && + !canBeUsedLater + ) { + return parent.right; + } + return null; + } + + /** + * Checks whether a given reference is a read to update itself or not. + * @param ref A reference to check. + * @param rhsNode The RHS node of the previous assignment. + * @returns The reference is a read to update itself. + */ + function isReadForItself( + ref: TSESLint.Scope.Reference, + rhsNode: TSESTree.Node | null, + ): boolean { + /** + * Checks whether a given Identifier node exists inside of a function node which can be used later. + * + * "can be used later" means: + * - the function is assigned to a variable. + * - the function is bound to a property and the object can be used later. + * - the function is bound as an argument of a function call. + * + * If a reference exists in a function which can be used later, the reference is read when the function is called. + * @param id An Identifier node to check. + * @param rhsNode The RHS node of the previous assignment. + * @returns `true` if the `id` node exists inside of a function node which can be used later. + */ + function isInsideOfStorableFunction( + id: TSESTree.Node, + rhsNode: TSESTree.Node, + ): boolean { + /** + * Finds a function node from ancestors of a node. + * @param node A start node to find. + * @returns A found function node. + */ + function getUpperFunction(node: TSESTree.Node): TSESTree.Node | null { + let currentNode: TSESTree.Node | undefined = node; + while (currentNode) { + if (util.isFunction(currentNode)) { + return currentNode; + } + currentNode = currentNode.parent; + } + + return null; + } + + /** + * Checks whether a given function node is stored to somewhere or not. + * If the function node is stored, the function can be used later. + * @param funcNode A function node to check. + * @param rhsNode The RHS node of the previous assignment. + * @returns `true` if under the following conditions: + * - the funcNode is assigned to a variable. + * - the funcNode is bound as an argument of a function call. + * - the function is bound to a property and the object satisfies above conditions. + */ + function isStorableFunction( + funcNode: TSESTree.Node, + rhsNode: TSESTree.Node, + ): boolean { + let node = funcNode; + let parent = funcNode.parent; + + while (parent && isInside(parent, rhsNode)) { + switch (parent.type) { + case AST_NODE_TYPES.SequenceExpression: + if (parent.expressions[parent.expressions.length - 1] !== node) { + return false; + } + break; + + case AST_NODE_TYPES.CallExpression: + case AST_NODE_TYPES.NewExpression: + return parent.callee !== node; + + case AST_NODE_TYPES.AssignmentExpression: + case AST_NODE_TYPES.TaggedTemplateExpression: + case AST_NODE_TYPES.YieldExpression: + return true; + + default: + if ( + parent.type.endsWith('Statement') || + parent.type.endsWith('Declaration') + ) { + /* + * If it encountered statements, this is a complex pattern. + * Since analyzing complex patterns is hard, this returns `true` to avoid false positive. + */ + return true; + } + } + + node = parent; + parent = parent.parent; + } + + return false; + } + + const funcNode = getUpperFunction(id); + + return ( + !!funcNode && + isInside(funcNode, rhsNode) && + isStorableFunction(funcNode, rhsNode) + ); + } + + const id = ref.identifier; + const parent = id.parent!; + const grandparent = parent.parent!; + + return ( + ref.isRead() && // in RHS of an assignment for itself. e.g. `a = a + 1` + // self update. e.g. `a += 1`, `a++` + ((parent.type === AST_NODE_TYPES.AssignmentExpression && + grandparent.type === AST_NODE_TYPES.ExpressionStatement && + parent.left === id) || + (parent.type === AST_NODE_TYPES.UpdateExpression && + grandparent.type === AST_NODE_TYPES.ExpressionStatement) || + (!!rhsNode && + isInside(id, rhsNode) && + !isInsideOfStorableFunction(id, rhsNode))) + ); + } + + const functionNodes = getFunctionDefinitions(variable); + const isFunctionDefinition = functionNodes.size > 0; + + const typeDeclNodes = getTypeDeclarations(variable); + const isTypeDecl = typeDeclNodes.size > 0; + + const moduleDeclNodes = getModuleDeclarations(variable); + const isModuleDecl = moduleDeclNodes.size > 0; + + let rhsNode: TSESTree.Node | null = null; + + return variable.references.some(ref => { + const forItself = isReadForItself(ref, rhsNode); + + rhsNode = getRhsNode(ref, rhsNode); + + return ( + ref.isRead() && + !forItself && + !(isFunctionDefinition && isSelfReference(ref, functionNodes)) && + !(isTypeDecl && isInsideOneOf(ref, typeDeclNodes)) && + !(isModuleDecl && isSelfReference(ref, moduleDeclNodes)) + ); + }); +} + +//#endregion private helpers + +/** + * Collects the set of unused variables for a given context. + * + * Due to complexity, this does not take into consideration: + * - variables within declaration files + * - variables within ambient module declarations + */ +function collectUnusedVariables< + TMessageIds extends string, + TOptions extends readonly unknown[] +>( + context: Readonly>, +): ReadonlySet { + return UnusedVarsVisitor.collectUnusedVariables(context); +} + +export { collectUnusedVariables }; diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 672f50dc4ff..af0a64eddbf 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -1,6 +1,7 @@ import { ESLintUtils } from '@typescript-eslint/experimental-utils'; export * from './astUtils'; +export * from './collectUnusedVariables'; export * from './createRule'; export * from './isTypeReadonly'; export * from './misc'; diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts new file mode 100644 index 00000000000..8ef68ae781c --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts @@ -0,0 +1,2457 @@ +// The following tests are adapted from the tests in eslint. +// Original Code: https://github.com/eslint/eslint/blob/0cb81a9b90dd6b92bac383022f886e501bd2cb31/tests/lib/rules/no-unused-vars.js +// Licence : https://github.com/eslint/eslint/blob/0cb81a9b90dd6b92bac383022f886e501bd2cb31/LICENSE + +'use strict'; + +import { + AST_NODE_TYPES, + TSESLint, +} from '@typescript-eslint/experimental-utils'; +import rule, { MessageIds } from '../../../src/rules/no-unused-vars'; +import { RuleTester } from '../../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + // espree defaults to `script`, so we need to mirror it + sourceType: 'script', + }, +}); + +ruleTester.defineRule('use-every-a', context => { + /** + * Mark a variable as used + */ + function useA(): void { + context.markVariableAsUsed('a'); + } + return { + VariableDeclaration: useA, + ReturnStatement: useA, + }; +}); + +/** + * Returns an expected error for defined-but-not-used variables. + * @param varName The name of the variable + * @param [additional] The additional text for the message data + * @param [type] The node type (defaults to "Identifier") + * @returns An expected error object + */ +function definedError( + varName: string, + additional = '', + type = AST_NODE_TYPES.Identifier, +): TSESLint.TestCaseError { + return { + messageId: 'unusedVar', + data: { + varName, + action: 'defined', + additional, + }, + type, + }; +} + +/** + * Returns an expected error for assigned-but-not-used variables. + * @param varName The name of the variable + * @param [additional] The additional text for the message data + * @param [type] The node type (defaults to "Identifier") + * @returns An expected error object + */ +function assignedError( + varName: string, + additional = '', + type = AST_NODE_TYPES.Identifier, +): TSESLint.TestCaseError { + return { + messageId: 'unusedVar', + data: { + varName, + action: 'assigned a value', + additional, + }, + type, + }; +} + +ruleTester.run('no-unused-vars', rule, { + valid: [ + 'var foo = 5;\n\nlabel: while (true) {\n console.log(foo);\n break label;\n}', + 'var foo = 5;\n\nwhile (true) {\n console.log(foo);\n break;\n}', + { + code: ` +for (let prop in box) { + box[prop] = parseInt(box[prop]); +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + ` +var box = { a: 2 }; +for (var prop in box) { + box[prop] = parseInt(box[prop]); +} + `, + ` +f({ + set foo(a) { + return; + }, +}); + `, + { + code: ` +a; +var a; + `, + options: ['all'], + }, + { + code: ` +var a = 10; +alert(a); + `, + options: ['all'], + }, + { + code: ` +var a = 10; +(function () { + alert(a); +})(); + `, + options: ['all'], + }, + { + code: ` +var a = 10; +(function () { + setTimeout(function () { + alert(a); + }, 0); +})(); + `, + options: ['all'], + }, + { + code: ` +var a = 10; +d[a] = 0; + `, + options: ['all'], + }, + { + code: ` +(function () { + var a = 10; + return a; +})(); + `, + options: ['all'], + }, + { + code: '(function g() {})();', + options: ['all'], + }, + { + code: ` +function f(a) { + alert(a); +} +f(); + `, + options: ['all'], + }, + { + code: ` +var c = 0; +function f(a) { + var b = a; + return b; +} +f(c); + `, + options: ['all'], + }, + { + code: ` +function a(x, y) { + return y; +} +a(); + `, + options: ['all'], + }, + { + code: ` +var arr1 = [1, 2]; +var arr2 = [3, 4]; +for (var i in arr1) { + arr1[i] = 5; +} +for (var i in arr2) { + arr2[i] = 10; +} + `, + options: ['all'], + }, + { + code: 'var a = 10;', + options: ['local'], + }, + { + code: ` +var min = 'min'; +Math[min]; + `, + options: ['all'], + }, + { + code: ` +Foo.bar = function (baz) { + return baz; +}; + `, + options: ['all'], + }, + 'myFunc(function foo() {}.bind(this));', + 'myFunc(function foo() {}.toString());', + ` +function foo(first, second) { + doStuff(function () { + console.log(second); + }); +} +foo(); + `, + ` +(function () { + var doSomething = function doSomething() {}; + doSomething(); +})(); + `, + ` +try { +} catch (e) {} + `, + '/*global a */ a;', + { + code: ` +var a = 10; +(function () { + alert(a); +})(); + `, + options: [{ vars: 'all' }], + }, + { + code: ` +function g(bar, baz) { + return baz; +} +g(); + `, + options: [{ vars: 'all' }], + }, + { + code: ` +function g(bar, baz) { + return baz; +} +g(); + `, + options: [{ vars: 'all', args: 'after-used' }], + }, + { + code: ` +function g(bar, baz) { + return bar; +} +g(); + `, + options: [{ vars: 'all', args: 'none' }], + }, + { + code: ` +function g(bar, baz) { + return 2; +} +g(); + `, + options: [{ vars: 'all', args: 'none' }], + }, + { + code: ` +function g(bar, baz) { + return bar + baz; +} +g(); + `, + options: [{ vars: 'local', args: 'all' }], + }, + { + code: ` +var g = function (bar, baz) { + return 2; +}; +g(); + `, + options: [{ vars: 'all', args: 'none' }], + }, + ` +(function z() { + z(); +})(); + `, + { + code: ' ', + globals: { a: true }, + }, + { + code: ` +var who = 'Paul'; +module.exports = \`Hello \${who}!\`; + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'export var foo = 123;', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: 'export function foo() {}', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: ` +let toUpper = partial => partial.toUpperCase; +export { toUpper }; + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: 'export class foo {}', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: ` +class Foo {} +var x = new Foo(); +x.foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const foo = 'hello!'; +function bar(foobar = foo) { + foobar.replace(/!$/, ' world!'); +} +bar(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + ` +function Foo() {} +var x = new Foo(); +x.foo(); + `, + ` +function foo() { + var foo = 1; + return foo; +} +foo(); + `, + ` +function foo(foo) { + return foo; +} +foo(1); + `, + ` +function foo() { + function foo() { + return 1; + } + return foo(); +} +foo(); + `, + { + code: ` +function foo() { + var foo = 1; + return foo; +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function foo(foo) { + return foo; +} +foo(1); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function foo() { + function foo() { + return 1; + } + return foo(); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +const [y = x] = []; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +const { y = x } = {}; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +const { + z: [y = x], +} = {}; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = []; +const { z: [y] = x } = {}; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +let y; +[y = x] = []; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +let y; +({ + z: [y = x], +} = {}); +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = []; +let y; +({ z: [y] = x } = {}); +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +function foo(y = x) { + bar(y); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +function foo({ y = x } = {}) { + bar(y); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +function foo( + y = function (z = x) { + bar(z); + }, +) { + y(); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +function foo( + y = function () { + bar(x); + }, +) { + y(); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +var [y = x] = []; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +var { y = x } = {}; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +var { + z: [y = x], +} = {}; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = []; +var { z: [y] = x } = {}; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1, + y; +[y = x] = []; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1, + y; +({ + z: [y = x], +} = {}); +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = [], + y; +({ z: [y] = x } = {}); +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +function foo(y = x) { + bar(y); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +function foo({ y = x } = {}) { + bar(y); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +function foo( + y = function (z = x) { + bar(z); + }, +) { + y(); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +function foo( + y = function () { + bar(x); + }, +) { + y(); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + + // exported variables should work + "/*exported toaster*/ var toaster = 'great';", + ` +/*exported toaster, poster*/ var toaster = 1; +poster = 0; + `, + { + code: '/*exported x*/ var { x } = y;', + parserOptions: { ecmaVersion: 6 }, + }, + { + code: '/*exported x, y*/ var { x, y } = z;', + parserOptions: { ecmaVersion: 6 }, + }, + + // Can mark variables as used via context.markVariableAsUsed() + '/*eslint use-every-a:1*/ var a;', + ` +/*eslint use-every-a:1*/ !function (a) { + return 1; +}; + `, + ` +/*eslint use-every-a:1*/ !function () { + var a; + return 1; +}; + `, + + // ignore pattern + { + code: 'var _a;', + options: [{ vars: 'all', varsIgnorePattern: '^_' }], + }, + { + code: ` +var a; +function foo() { + var _b; +} +foo(); + `, + options: [{ vars: 'local', varsIgnorePattern: '^_' }], + }, + { + code: ` +function foo(_a) {} +foo(); + `, + options: [{ args: 'all', argsIgnorePattern: '^_' }], + }, + { + code: ` +function foo(a, _b) { + return a; +} +foo(); + `, + options: [{ args: 'after-used', argsIgnorePattern: '^_' }], + }, + { + code: ` +var [firstItemIgnored, secondItem] = items; +console.log(secondItem); + `, + options: [{ vars: 'all', varsIgnorePattern: '[iI]gnored' }], + parserOptions: { ecmaVersion: 6 }, + }, + + // for-in loops (see #2342) + ` +(function (obj) { + var name; + for (name in obj) return; +})({}); + `, + ` +(function (obj) { + var name; + for (name in obj) { + return; + } +})({}); + `, + ` +(function (obj) { + for (var name in obj) { + return true; + } +})({}); + `, + ` +(function (obj) { + for (var name in obj) return true; +})({}); + `, + + { + code: ` +(function (obj) { + let name; + for (name in obj) return; +})({}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +(function (obj) { + let name; + for (name in obj) { + return; + } +})({}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +(function (obj) { + for (let name in obj) { + return true; + } +})({}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +(function (obj) { + for (let name in obj) return true; +})({}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + + { + code: ` +(function (obj) { + for (const name in obj) { + return true; + } +})({}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +(function (obj) { + for (const name in obj) return true; +})({}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + + // caughtErrors + { + code: ` +try { +} catch (err) { + console.error(err); +} + `, + options: [{ caughtErrors: 'all' }], + }, + { + code: ` +try { +} catch (err) {} + `, + options: [{ caughtErrors: 'none' }], + }, + { + code: ` +try { +} catch (ignoreErr) {} + `, + options: [{ caughtErrors: 'all', caughtErrorsIgnorePattern: '^ignore' }], + }, + + // caughtErrors with other combinations + { + code: ` +try { +} catch (err) {} + `, + options: [{ vars: 'all', args: 'all' }], + }, + + // Using object rest for variable omission + { + code: ` +const data = { type: 'coords', x: 1, y: 2 }; +const { type, ...coords } = data; +console.log(coords); + `, + options: [{ ignoreRestSiblings: true }], + parserOptions: { ecmaVersion: 2018 }, + }, + + // https://github.com/eslint/eslint/issues/6348 + ` +var a = 0, + b; +b = a = a + 1; +foo(b); + `, + ` +var a = 0, + b; +b = a += a + 1; +foo(b); + `, + ` +var a = 0, + b; +b = a++; +foo(b); + `, + ` +function foo(a) { + var b = (a = a + 1); + bar(b); +} +foo(); + `, + ` +function foo(a) { + var b = (a += a + 1); + bar(b); +} +foo(); + `, + ` +function foo(a) { + var b = a++; + bar(b); +} +foo(); + `, + + // https://github.com/eslint/eslint/issues/6576 + [ + 'var unregisterFooWatcher;', + '// ...', + 'unregisterFooWatcher = $scope.$watch( "foo", function() {', + ' // ...some code..', + ' unregisterFooWatcher();', + '});', + ].join('\n'), + [ + 'var ref;', + 'ref = setInterval(', + ' function(){', + ' clearInterval(ref);', + ' }, 10);', + ].join('\n'), + [ + 'var _timer;', + 'function f() {', + ' _timer = setTimeout(function () {}, _timer ? 100 : 0);', + '}', + 'f();', + ].join('\n'), + ` +function foo(cb) { + cb = (function () { + function something(a) { + cb(1 + a); + } + register(something); + })(); +} +foo(); + `, + { + code: ` +function* foo(cb) { + cb = yield function (a) { + cb(1 + a); + }; +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function foo(cb) { + cb = tag\`hello\${function (a) { + cb(1 + a); + }}\`; +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + ` +function foo(cb) { + var b; + cb = b = function (a) { + cb(1 + a); + }; + b(); +} +foo(); + `, + + // https://github.com/eslint/eslint/issues/6646 + [ + 'function someFunction() {', + ' var a = 0, i;', + ' for (i = 0; i < 2; i++) {', + ' a = myFunction(a);', + ' }', + '}', + 'someFunction();', + ].join('\n'), + + // https://github.com/eslint/eslint/issues/7124 + { + code: ` +(function (a, b, { c, d }) { + d; +}); + `, + options: [{ argsIgnorePattern: 'c' }], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +(function (a, b, { c, d }) { + c; +}); + `, + options: [{ argsIgnorePattern: 'd' }], + parserOptions: { ecmaVersion: 6 }, + }, + + // https://github.com/eslint/eslint/issues/7250 + { + code: ` +(function (a, b, c) { + c; +}); + `, + options: [{ argsIgnorePattern: 'c' }], + }, + { + code: ` +(function (a, b, { c, d }) { + c; +}); + `, + options: [{ argsIgnorePattern: '[cd]' }], + parserOptions: { ecmaVersion: 6 }, + }, + + // https://github.com/eslint/eslint/issues/7351 + { + code: ` +(class { + set foo(UNUSED) {} +}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +class Foo { + set bar(UNUSED) {} +} +console.log(Foo); + `, + parserOptions: { ecmaVersion: 6 }, + }, + + // https://github.com/eslint/eslint/issues/8119 + { + code: '({ a, ...rest }) => rest;', + options: [{ args: 'all', ignoreRestSiblings: true }], + parserOptions: { ecmaVersion: 2018 }, + }, + + // https://github.com/eslint/eslint/issues/10952 + ` +/*eslint use-every-a:1*/ !function (b, a) { + return 1; +}; + `, + + // https://github.com/eslint/eslint/issues/10982 + ` +var a = function () { + a(); +}; +a(); + `, + ` +var a = function () { + return function () { + a(); + }; +}; +a(); + `, + { + code: ` +const a = () => { + a(); +}; +a(); + `, + parserOptions: { ecmaVersion: 2015 }, + }, + { + code: ` +const a = () => () => { + a(); +}; +a(); + `, + parserOptions: { ecmaVersion: 2015 }, + }, + + // export * as ns from "source" + { + code: "export * as ns from 'source';", + parserOptions: { ecmaVersion: 2020, sourceType: 'module' }, + }, + + // import.meta + { + code: 'import.meta;', + parserOptions: { ecmaVersion: 2020, sourceType: 'module' }, + }, + ], + invalid: [ + { + code: ` +function foox() { + return foox(); +} + `, + errors: [definedError('foox')], + }, + { + code: ` +(function () { + function foox() { + if (true) { + return foox(); + } + } +})(); + `, + errors: [definedError('foox')], + }, + { + code: 'var a = 10;', + errors: [assignedError('a')], + }, + { + code: ` +function f() { + var a = 1; + return function () { + f((a *= 2)); + }; +} + `, + errors: [definedError('f')], + }, + { + code: ` +function f() { + var a = 1; + return function () { + f(++a); + }; +} + `, + errors: [definedError('f')], + }, + { + code: '/*global a */', + errors: [definedError('a', '', AST_NODE_TYPES.Program)], + }, + { + code: ` +function foo(first, second) { + doStuff(function () { + console.log(second); + }); +} + `, + errors: [definedError('foo')], + }, + { + code: 'var a = 10;', + options: ['all'], + errors: [assignedError('a')], + }, + { + code: ` +var a = 10; +a = 20; + `, + options: ['all'], + errors: [assignedError('a')], + }, + { + code: ` +var a = 10; +(function () { + var a = 1; + alert(a); +})(); + `, + options: ['all'], + errors: [assignedError('a')], + }, + { + code: ` +var a = 10, + b = 0, + c = null; +alert(a + b); + `, + options: ['all'], + errors: [assignedError('c')], + }, + { + code: ` +var a = 10, + b = 0, + c = null; +setTimeout(function () { + var b = 2; + alert(a + b + c); +}, 0); + `, + options: ['all'], + errors: [assignedError('b')], + }, + { + code: ` +var a = 10, + b = 0, + c = null; +setTimeout(function () { + var b = 2; + var c = 2; + alert(a + b + c); +}, 0); + `, + options: ['all'], + errors: [assignedError('b'), assignedError('c')], + }, + { + code: ` +function f() { + var a = []; + return a.map(function () {}); +} + `, + options: ['all'], + errors: [definedError('f')], + }, + { + code: ` +function f() { + var a = []; + return a.map(function g() {}); +} + `, + options: ['all'], + errors: [definedError('f')], + }, + { + code: ` +function foo() { + function foo(x) { + return x; + } + return function () { + return foo; + }; +} + `, + errors: [ + { + messageId: 'unusedVar', + data: { varName: 'foo', action: 'defined', additional: '' }, + line: 2, + type: AST_NODE_TYPES.Identifier, + }, + ], + }, + { + code: ` +function f() { + var x; + function a() { + x = 42; + } + function b() { + alert(x); + } +} + `, + options: ['all'], + errors: [definedError('f'), definedError('a'), definedError('b')], + }, + { + code: ` +function f(a) {} +f(); + `, + options: ['all'], + errors: [definedError('a')], + }, + { + code: ` +function a(x, y, z) { + return y; +} +a(); + `, + options: ['all'], + errors: [definedError('z')], + }, + { + code: 'var min = Math.min;', + options: ['all'], + errors: [assignedError('min')], + }, + { + code: 'var min = { min: 1 };', + options: ['all'], + errors: [assignedError('min')], + }, + { + code: ` +Foo.bar = function (baz) { + return 1; +}; + `, + options: ['all'], + errors: [definedError('baz')], + }, + { + code: 'var min = { min: 1 };', + options: [{ vars: 'all' }], + errors: [assignedError('min')], + }, + { + code: ` +function gg(baz, bar) { + return baz; +} +gg(); + `, + options: [{ vars: 'all' }], + errors: [definedError('bar')], + }, + { + code: ` +(function (foo, baz, bar) { + return baz; +})(); + `, + options: [{ vars: 'all', args: 'after-used' }], + errors: [definedError('bar')], + }, + { + code: ` +(function (foo, baz, bar) { + return baz; +})(); + `, + options: [{ vars: 'all', args: 'all' }], + errors: [definedError('foo'), definedError('bar')], + }, + { + code: ` +(function z(foo) { + var bar = 33; +})(); + `, + options: [{ vars: 'all', args: 'all' }], + errors: [definedError('foo'), assignedError('bar')], + }, + { + code: ` +(function z(foo) { + z(); +})(); + `, + options: [{}], + errors: [definedError('foo')], + }, + { + code: ` +function f() { + var a = 1; + return function () { + f((a = 2)); + }; +} + `, + options: [{}], + errors: [definedError('f'), assignedError('a')], + }, + { + code: "import x from 'y';", + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('x')], + }, + { + code: ` +export function fn2({ x, y }) { + console.log(x); +} + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('y')], + }, + { + code: ` +export function fn2(x, y) { + console.log(x); +} + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('y')], + }, + + // exported + { + code: ` +/*exported max*/ var max = 1, + min = { min: 1 }; + `, + errors: [assignedError('min')], + }, + { + code: '/*exported x*/ var { x, y } = z;', + parserOptions: { ecmaVersion: 6 }, + errors: [assignedError('y')], + }, + + // ignore pattern + { + code: ` +var _a; +var b; + `, + options: [{ vars: 'all', varsIgnorePattern: '^_' }], + errors: [ + { + line: 3, + column: 5, + messageId: 'unusedVar', + data: { + varName: 'b', + action: 'defined', + additional: '. Allowed unused vars must match /^_/u', + }, + }, + ], + }, + { + code: ` +var a; +function foo() { + var _b; + var c_; +} +foo(); + `, + options: [{ vars: 'local', varsIgnorePattern: '^_' }], + errors: [ + { + line: 5, + column: 7, + messageId: 'unusedVar', + data: { + varName: 'c_', + action: 'defined', + additional: '. Allowed unused vars must match /^_/u', + }, + }, + ], + }, + { + code: ` +function foo(a, _b) {} +foo(); + `, + options: [{ args: 'all', argsIgnorePattern: '^_' }], + errors: [ + { + line: 2, + column: 14, + messageId: 'unusedVar', + data: { + varName: 'a', + action: 'defined', + additional: '. Allowed unused args must match /^_/u', + }, + }, + ], + }, + { + code: ` +function foo(a, _b, c) { + return a; +} +foo(); + `, + options: [{ args: 'after-used', argsIgnorePattern: '^_' }], + errors: [ + { + line: 2, + column: 21, + messageId: 'unusedVar', + data: { + varName: 'c', + action: 'defined', + additional: '. Allowed unused args must match /^_/u', + }, + }, + ], + }, + { + code: ` +function foo(_a) {} +foo(); + `, + options: [{ args: 'all', argsIgnorePattern: '[iI]gnored' }], + errors: [ + { + line: 2, + column: 14, + messageId: 'unusedVar', + data: { + varName: '_a', + action: 'defined', + additional: '. Allowed unused args must match /[iI]gnored/u', + }, + }, + ], + }, + { + code: 'var [firstItemIgnored, secondItem] = items;', + options: [{ vars: 'all', varsIgnorePattern: '[iI]gnored' }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + line: 1, + column: 24, + messageId: 'unusedVar', + data: { + varName: 'secondItem', + action: 'assigned a value', + additional: '. Allowed unused vars must match /[iI]gnored/u', + }, + }, + ], + }, + + // for-in loops (see #2342) + { + code: ` +(function (obj) { + var name; + for (name in obj) { + i(); + return; + } +})({}); + `, + errors: [ + { + line: 4, + column: 8, + messageId: 'unusedVar', + data: { + varName: 'name', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + { + code: ` +(function (obj) { + var name; + for (name in obj) { + } +})({}); + `, + errors: [ + { + line: 4, + column: 8, + messageId: 'unusedVar', + data: { + varName: 'name', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + { + code: ` +(function (obj) { + for (var name in obj) { + } +})({}); + `, + errors: [ + { + line: 3, + column: 12, + messageId: 'unusedVar', + data: { + varName: 'name', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + + // https://github.com/eslint/eslint/issues/3617 + { + code: ` +/* global foobar, foo, bar */ +foobar; + `, + errors: [ + { + line: 2, + endLine: 2, + column: 19, + endColumn: 22, + messageId: 'unusedVar', + data: { + varName: 'foo', + action: 'defined', + additional: '', + }, + }, + { + line: 2, + endLine: 2, + column: 24, + endColumn: 27, + messageId: 'unusedVar', + data: { + varName: 'bar', + action: 'defined', + additional: '', + }, + }, + ], + }, + { + code: ` +/* global foobar, + foo, + bar + */ +foobar; + `, + errors: [ + { + line: 3, + column: 4, + endLine: 3, + endColumn: 7, + messageId: 'unusedVar', + data: { + varName: 'foo', + action: 'defined', + additional: '', + }, + }, + { + line: 4, + column: 4, + endLine: 4, + endColumn: 7, + messageId: 'unusedVar', + data: { + varName: 'bar', + action: 'defined', + additional: '', + }, + }, + ], + }, + + // Rest property sibling without ignoreRestSiblings + { + code: ` +const data = { type: 'coords', x: 1, y: 2 }; +const { type, ...coords } = data; +console.log(coords); + `, + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + line: 3, + column: 9, + messageId: 'unusedVar', + data: { + varName: 'type', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + + // Unused rest property with ignoreRestSiblings + { + code: ` +const data = { type: 'coords', x: 2, y: 2 }; +const { type, ...coords } = data; +console.log(type); + `, + options: [{ ignoreRestSiblings: true }], + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + line: 3, + column: 18, + messageId: 'unusedVar', + data: { + varName: 'coords', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + + // Unused rest property without ignoreRestSiblings + { + code: ` +const data = { type: 'coords', x: 3, y: 2 }; +const { type, ...coords } = data; +console.log(type); + `, + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + line: 3, + column: 18, + messageId: 'unusedVar', + data: { + varName: 'coords', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + + // Nested array destructuring with rest property + { + code: ` +const data = { vars: ['x', 'y'], x: 1, y: 2 }; +const { + vars: [x], + ...coords +} = data; +console.log(coords); + `, + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + line: 4, + column: 10, + messageId: 'unusedVar', + data: { + varName: 'x', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + + // Nested object destructuring with rest property + { + code: ` +const data = { defaults: { x: 0 }, x: 1, y: 2 }; +const { + defaults: { x }, + ...coords +} = data; +console.log(coords); + `, + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + line: 4, + column: 15, + messageId: 'unusedVar', + data: { + varName: 'x', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + + // https://github.com/eslint/eslint/issues/8119 + { + code: '({ a, ...rest }) => {};', + options: [{ args: 'all', ignoreRestSiblings: true }], + parserOptions: { ecmaVersion: 2018 }, + errors: [definedError('rest')], + }, + + // https://github.com/eslint/eslint/issues/3714 + { + // cspell:disable-next-line + code: '/* global a$fooz,$foo */\na$fooz;', + errors: [ + { + line: 1, + column: 18, + endLine: 1, + endColumn: 22, + messageId: 'unusedVar', + data: { + varName: '$foo', + action: 'defined', + additional: '', + }, + }, + ], + }, + { + // cspell:disable-next-line + code: '/* globals a$fooz, $ */\na$fooz;', + errors: [ + { + line: 1, + column: 20, + endLine: 1, + endColumn: 21, + messageId: 'unusedVar', + data: { + varName: '$', + action: 'defined', + additional: '', + }, + }, + ], + }, + { + code: '/*globals $foo*/', + errors: [ + { + line: 1, + column: 11, + endLine: 1, + endColumn: 15, + messageId: 'unusedVar', + data: { + varName: '$foo', + action: 'defined', + additional: '', + }, + }, + ], + }, + { + code: '/* global global*/', + errors: [ + { + line: 1, + column: 11, + endLine: 1, + endColumn: 17, + messageId: 'unusedVar', + data: { + varName: 'global', + action: 'defined', + additional: '', + }, + }, + ], + }, + { + code: '/*global foo:true*/', + errors: [ + { + line: 1, + column: 10, + endLine: 1, + endColumn: 13, + messageId: 'unusedVar', + data: { + varName: 'foo', + action: 'defined', + additional: '', + }, + }, + ], + }, + + // non ascii. + { + code: '/*global 変数, 数*/\n変数;', + errors: [ + { + line: 1, + column: 14, + endLine: 1, + endColumn: 15, + messageId: 'unusedVar', + data: { + varName: '数', + action: 'defined', + additional: '', + }, + }, + ], + }, + + // surrogate pair. + { + code: ` +/*global 𠮷𩸽, 𠮷*/ +𠮷𩸽; + `, + env: { es6: true }, + errors: [ + { + line: 2, + column: 16, + endLine: 2, + endColumn: 18, + messageId: 'unusedVar', + data: { + varName: '𠮷', + action: 'defined', + additional: '', + }, + }, + ], + }, + + // https://github.com/eslint/eslint/issues/4047 + { + code: 'export default function (a) {}', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('a')], + }, + { + code: ` +export default function (a, b) { + console.log(a); +} + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('b')], + }, + { + code: 'export default (function (a) {});', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('a')], + }, + { + code: ` +export default (function (a, b) { + console.log(a); +}); + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('b')], + }, + { + code: 'export default a => {};', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('a')], + }, + { + code: ` +export default (a, b) => { + console.log(a); +}; + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('b')], + }, + + // caughtErrors + { + code: ` +try { +} catch (err) {} + `, + options: [{ caughtErrors: 'all' }], + errors: [definedError('err')], + }, + { + code: ` +try { +} catch (err) {} + `, + options: [{ caughtErrors: 'all', caughtErrorsIgnorePattern: '^ignore' }], + errors: [ + definedError('err', '. Allowed unused args must match /^ignore/u'), + ], + }, + + // multiple try catch with one success + { + code: ` +try { +} catch (ignoreErr) {} +try { +} catch (err) {} + `, + options: [{ caughtErrors: 'all', caughtErrorsIgnorePattern: '^ignore' }], + errors: [ + definedError('err', '. Allowed unused args must match /^ignore/u'), + ], + }, + + // multiple try catch both fail + { + code: ` +try { +} catch (error) {} +try { +} catch (err) {} + `, + options: [{ caughtErrors: 'all', caughtErrorsIgnorePattern: '^ignore' }], + errors: [ + definedError('error', '. Allowed unused args must match /^ignore/u'), + definedError('err', '. Allowed unused args must match /^ignore/u'), + ], + }, + + // caughtErrors with other configs + { + code: ` +try { +} catch (err) {} + `, + options: [{ vars: 'all', args: 'all', caughtErrors: 'all' }], + errors: [definedError('err')], + }, + + // no conflict in ignore patterns + { + code: ` +try { +} catch (err) {} + `, + options: [ + { + vars: 'all', + args: 'all', + caughtErrors: 'all', + argsIgnorePattern: '^er', + }, + ], + errors: [definedError('err')], + }, + + // Ignore reads for modifications to itself: https://github.com/eslint/eslint/issues/6348 + { + code: ` +var a = 0; +a = a + 1; + `, + errors: [assignedError('a')], + }, + { + code: ` +var a = 0; +a = a + a; + `, + errors: [assignedError('a')], + }, + { + code: ` +var a = 0; +a += a + 1; + `, + errors: [assignedError('a')], + }, + { + code: ` +var a = 0; +a++; + `, + errors: [assignedError('a')], + }, + { + code: ` +function foo(a) { + a = a + 1; +} +foo(); + `, + errors: [assignedError('a')], + }, + { + code: ` +function foo(a) { + a += a + 1; +} +foo(); + `, + errors: [assignedError('a')], + }, + { + code: ` +function foo(a) { + a++; +} +foo(); + `, + errors: [assignedError('a')], + }, + { + code: ` +var a = 3; +a = a * 5 + 6; + `, + errors: [assignedError('a')], + }, + { + code: ` +var a = 2, + b = 4; +a = a * 2 + b; + `, + errors: [assignedError('a')], + }, + + // https://github.com/eslint/eslint/issues/6576 (For coverage) + { + code: ` +function foo(cb) { + cb = function (a) { + cb(1 + a); + }; + bar(not_cb); +} +foo(); + `, + errors: [assignedError('cb')], + }, + { + code: ` +function foo(cb) { + cb = (function (a) { + return cb(1 + a); + })(); +} +foo(); + `, + errors: [assignedError('cb')], + }, + { + code: ` +function foo(cb) { + cb = + (function (a) { + cb(1 + a); + }, + cb); +} +foo(); + `, + errors: [assignedError('cb')], + }, + { + code: ` +function foo(cb) { + cb = + (0, + function (a) { + cb(1 + a); + }); +} +foo(); + `, + errors: [assignedError('cb')], + }, + + // https://github.com/eslint/eslint/issues/6646 + { + code: [ + 'while (a) {', + ' function foo(b) {', + ' b = b + 1;', + ' }', + ' foo()', + '}', + ].join('\n'), + errors: [assignedError('b')], + }, + + // https://github.com/eslint/eslint/issues/7124 + { + code: '(function (a, b, c) {});', + options: [{ argsIgnorePattern: 'c' }], + errors: [ + definedError('a', '. Allowed unused args must match /c/u'), + definedError('b', '. Allowed unused args must match /c/u'), + ], + }, + { + code: '(function (a, b, { c, d }) {});', + options: [{ argsIgnorePattern: '[cd]' }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + definedError('a', '. Allowed unused args must match /[cd]/u'), + definedError('b', '. Allowed unused args must match /[cd]/u'), + ], + }, + { + code: '(function (a, b, { c, d }) {});', + options: [{ argsIgnorePattern: 'c' }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + definedError('a', '. Allowed unused args must match /c/u'), + definedError('b', '. Allowed unused args must match /c/u'), + definedError('d', '. Allowed unused args must match /c/u'), + ], + }, + { + code: '(function (a, b, { c, d }) {});', + options: [{ argsIgnorePattern: 'd' }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + definedError('a', '. Allowed unused args must match /d/u'), + definedError('b', '. Allowed unused args must match /d/u'), + definedError('c', '. Allowed unused args must match /d/u'), + ], + }, + { + code: ` +/*global +foo*/ + `, + errors: [ + { + line: 3, + column: 1, + endLine: 3, + endColumn: 4, + messageId: 'unusedVar', + data: { + varName: 'foo', + action: 'defined', + additional: '', + }, + }, + ], + }, + + // https://github.com/eslint/eslint/issues/8442 + { + code: ` +(function ({ a }, b) { + return b; +})(); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [definedError('a')], + }, + { + code: ` +(function ({ a }, { b, c }) { + return b; +})(); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [definedError('a'), definedError('c')], + }, + { + code: ` +(function ({ a, b }, { c }) { + return b; +})(); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [definedError('a'), definedError('c')], + }, + { + code: ` +(function ([a], b) { + return b; +})(); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [definedError('a')], + }, + { + code: ` +(function ([a], [b, c]) { + return b; +})(); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [definedError('a'), definedError('c')], + }, + { + code: ` +(function ([a, b], [c]) { + return b; +})(); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [definedError('a'), definedError('c')], + }, + + // https://github.com/eslint/eslint/issues/9774 + { + code: '(function (_a) {})();', + options: [{ args: 'all', varsIgnorePattern: '^_' }], + errors: [definedError('_a')], + }, + { + code: '(function (_a) {})();', + options: [{ args: 'all', caughtErrorsIgnorePattern: '^_' }], + errors: [definedError('_a')], + }, + + // https://github.com/eslint/eslint/issues/10982 + { + code: ` +var a = function () { + a(); +}; + `, + errors: [assignedError('a')], + }, + { + code: ` +var a = function () { + return function () { + a(); + }; +}; + `, + errors: [assignedError('a')], + }, + { + code: ` +const a = () => { + a(); +}; + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [assignedError('a')], + }, + { + code: ` +const a = () => () => { + a(); +}; + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [assignedError('a')], + }, + { + code: ` +let myArray = [1, 2, 3, 4].filter(x => x == 0); +myArray = myArray.filter(x => x == 1); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + ...assignedError('myArray'), + line: 3, + column: 11, + }, + ], + }, + { + code: ` +const a = 1; +a += 1; + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + ...assignedError('a'), + line: 3, + column: 1, + }, + ], + }, + { + code: ` +var a = function () { + a(); +}; + `, + errors: [ + { + ...assignedError('a'), + line: 3, + column: 3, + }, + ], + }, + { + code: ` +var a = function () { + return function () { + a(); + }; +}; + `, + errors: [ + { + ...assignedError('a'), + line: 4, + column: 5, + }, + ], + }, + { + code: ` +const a = () => { + a(); +}; + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + ...assignedError('a'), + line: 3, + column: 3, + }, + ], + }, + { + code: ` +const a = () => () => { + a(); +}; + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + ...assignedError('a'), + line: 3, + column: 3, + }, + ], + }, + { + code: ` +let a = 'a'; +a = 10; +function foo() { + a = 11; + a = () => { + a = 13; + }; +} + `, + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { + ...definedError('foo'), + line: 4, + column: 10, + }, + { + ...assignedError('a'), + line: 7, + column: 5, + }, + ], + }, + { + code: ` +let c = 'c'; +c = 10; +function foo1() { + c = 11; + c = () => { + c = 13; + }; +} +c = foo1; + `, + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { + ...assignedError('c'), + line: 10, + column: 1, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts similarity index 95% rename from packages/eslint-plugin/tests/rules/no-unused-vars.test.ts rename to packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts index 9bc46bc75ad..9e06ea0dc48 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts @@ -1,5 +1,5 @@ -import rule from '../../src/rules/no-unused-vars'; -import { noFormat, RuleTester } from '../RuleTester'; +import rule from '../../../src/rules/no-unused-vars'; +import { noFormat, RuleTester } from '../../RuleTester'; const ruleTester = new RuleTester({ parserOptions: { @@ -927,6 +927,39 @@ export declare function setAlignment(value: \`\${VerticalAlignment}-\${Horizonta type EnthusiasticGreeting = \`\${Uppercase} - \${Lowercase} - \${Capitalize} - \${Uncapitalize}\`; export type HELLO = EnthusiasticGreeting<"heLLo">; `, + // https://github.com/typescript-eslint/typescript-eslint/issues/2714 + { + code: ` +interface IItem { + title: string; + url: string; + children?: IItem[]; +} + `, + // unreported because it's in a decl file, even though it's only self-referenced + filename: 'foo.d.ts', + }, + // https://github.com/typescript-eslint/typescript-eslint/issues/2648 + { + code: ` +namespace _Foo { + export const bar = 1; + export const baz = Foo.bar; +} + `, + // ignored by pattern, even though it's only self-referenced + options: [{ varsIgnorePattern: '^_' }], + }, + { + code: ` +interface _Foo { + a: string; + b: Foo; +} + `, + // ignored by pattern, even though it's only self-referenced + options: [{ varsIgnorePattern: '^_' }], + }, ], invalid: [ @@ -1376,8 +1409,8 @@ namespace Foo { action: 'defined', additional: '', }, - line: 2, - column: 11, + line: 4, + column: 15, }, ], }, @@ -1408,8 +1441,8 @@ namespace Foo { action: 'defined', additional: '', }, - line: 3, - column: 13, + line: 5, + column: 17, }, ], }, @@ -1424,7 +1457,7 @@ interface Foo { errors: [ { messageId: 'unusedVar', - line: 2, + line: 4, data: { varName: 'Foo', action: 'defined', @@ -1575,5 +1608,26 @@ export namespace Foo { }, ], }, + { + code: ` +interface Foo { + a: string; +} +interface Foo { + b: Foo; +} + `, + errors: [ + { + messageId: 'unusedVar', + line: 6, + data: { + varName: 'Foo', + action: 'defined', + additional: '', + }, + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 4e10fe23578..93b75e3a956 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -811,3 +811,17 @@ declare module 'eslint/lib/rules/space-infix-ops' { >; export = rule; } + +declare module 'eslint/lib/rules/utils/ast-utils' { + import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; + + const utils: { + getNameLocationInGlobalDirectiveComment( + sourceCode: TSESLint.SourceCode, + comment: TSESTree.Comment, + name: string, + ): TSESTree.SourceLocation; + }; + + export = utils; +} diff --git a/packages/experimental-utils/src/ast-utils/predicates.ts b/packages/experimental-utils/src/ast-utils/predicates.ts index c23f30f1707..bf62a6f77cb 100644 --- a/packages/experimental-utils/src/ast-utils/predicates.ts +++ b/packages/experimental-utils/src/ast-utils/predicates.ts @@ -214,6 +214,27 @@ function isAwaitKeyword( return node?.type === AST_TOKEN_TYPES.Identifier && node.value === 'await'; } +function isLoop( + node: TSESTree.Node | undefined | null, +): node is + | TSESTree.DoWhileStatement + | TSESTree.ForStatement + | TSESTree.ForInStatement + | TSESTree.ForOfStatement + | TSESTree.WhileStatement { + if (!node) { + return false; + } + + return ( + node.type === AST_NODE_TYPES.DoWhileStatement || + node.type === AST_NODE_TYPES.ForStatement || + node.type === AST_NODE_TYPES.ForInStatement || + node.type === AST_NODE_TYPES.ForOfStatement || + node.type === AST_NODE_TYPES.WhileStatement + ); +} + export { isAwaitExpression, isAwaitKeyword, @@ -223,6 +244,7 @@ export { isFunctionOrFunctionType, isFunctionType, isIdentifier, + isLoop, isLogicalOrOperator, isNonNullAssertionPunctuator, isNotNonNullAssertionPunctuator, diff --git a/packages/experimental-utils/src/eslint-utils/InferTypesFromRule.ts b/packages/experimental-utils/src/eslint-utils/InferTypesFromRule.ts index 66e5b1153c3..1fd2e752baa 100644 --- a/packages/experimental-utils/src/eslint-utils/InferTypesFromRule.ts +++ b/packages/experimental-utils/src/eslint-utils/InferTypesFromRule.ts @@ -1,25 +1,26 @@ -import { RuleModule } from '../ts-eslint'; +import { RuleCreateFunction, RuleModule } from '../ts-eslint'; -type InferOptionsTypeFromRuleNever = T extends RuleModule< - never, - infer TOptions -> - ? TOptions - : unknown; /** * Uses type inference to fetch the TOptions type from the given RuleModule */ -type InferOptionsTypeFromRule = T extends RuleModule +type InferOptionsTypeFromRule = T extends RuleModule< + infer _TMessageIds, + infer TOptions +> + ? TOptions + : T extends RuleCreateFunction ? TOptions - : InferOptionsTypeFromRuleNever; + : unknown; /** * Uses type inference to fetch the TMessageIds type from the given RuleModule */ type InferMessageIdsTypeFromRule = T extends RuleModule< infer TMessageIds, - unknown[] + infer _TOptions > + ? TMessageIds + : T extends RuleCreateFunction ? TMessageIds : unknown; diff --git a/packages/experimental-utils/src/ts-eslint/Rule.ts b/packages/experimental-utils/src/ts-eslint/Rule.ts index d305b29125b..8ced374dd70 100644 --- a/packages/experimental-utils/src/ts-eslint/Rule.ts +++ b/packages/experimental-utils/src/ts-eslint/Rule.ts @@ -440,9 +440,12 @@ interface RuleModule< create(context: Readonly>): TRuleListener; } -type RuleCreateFunction = ( - context: Readonly>, -) => RuleListener; +type RuleCreateFunction< + TMessageIds extends string = never, + TOptions extends readonly unknown[] = unknown[], + // for extending base rules + TRuleListener extends RuleListener = RuleListener +> = (context: Readonly>) => TRuleListener; export { ReportDescriptor, diff --git a/packages/experimental-utils/src/ts-eslint/RuleTester.ts b/packages/experimental-utils/src/ts-eslint/RuleTester.ts index a1fa2104a91..652567f6b9f 100644 --- a/packages/experimental-utils/src/ts-eslint/RuleTester.ts +++ b/packages/experimental-utils/src/ts-eslint/RuleTester.ts @@ -1,7 +1,7 @@ import { RuleTester as ESLintRuleTester } from 'eslint'; import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '../ts-estree'; import { ParserOptions } from './ParserOptions'; -import { RuleModule } from './Rule'; +import { RuleCreateFunction, RuleModule } from './Rule'; interface ValidTestCase> { /** @@ -19,7 +19,7 @@ interface ValidTestCase> { /** * The additional global variables. */ - readonly globals?: Record; + readonly globals?: Record; /** * Options for the test case. */ @@ -157,6 +157,18 @@ declare class RuleTesterBase { * @param callback the test callback */ static it?: (text: string, callback: () => void) => void; + + /** + * Define a rule for one particular run of tests. + * @param name The name of the rule to define. + * @param rule The rule definition. + */ + defineRule>( + name: string, + rule: + | RuleModule + | RuleCreateFunction, + ): void; } class RuleTester extends (ESLintRuleTester as typeof RuleTesterBase) {} diff --git a/packages/experimental-utils/src/ts-eslint/Scope.ts b/packages/experimental-utils/src/ts-eslint/Scope.ts index 2934a4d2756..6907e2290fd 100644 --- a/packages/experimental-utils/src/ts-eslint/Scope.ts +++ b/packages/experimental-utils/src/ts-eslint/Scope.ts @@ -1,56 +1,51 @@ /* eslint-disable @typescript-eslint/no-namespace */ import * as scopeManager from '@typescript-eslint/scope-manager'; -import { TSESTree } from '@typescript-eslint/types'; namespace Scope { - // ESLint defines global variables using the eslint-scope Variable class - // So a variable in the scope may be either of these - declare class ESLintScopeVariable { - public readonly defs: Definition[]; - public readonly identifiers: TSESTree.Identifier[]; - public readonly name: string; - public readonly references: Reference[]; - public readonly scope: Scope; - - /** - * Written to by ESLint. - * If this key exists, this variable is a global variable added by ESLint. - * If this is `true`, this variable can be assigned arbitrary values. - * If this is `false`, this variable is readonly. - */ - public writeable?: boolean; // note that this isn't a typo - ESlint uses this spelling here - - /** - * Written to by ESLint. - * This property is undefined if there are no globals directive comments. - * The array of globals directive comments which defined this global variable in the source code file. - */ - public eslintExplicitGlobal?: boolean; - - /** - * Written to by ESLint. - * The configured value in config files. This can be different from `variable.writeable` if there are globals directive comments. - */ - public eslintImplicitGlobalSetting?: 'readonly' | 'writable'; - - /** - * Written to by ESLint. - * If this key exists, it is a global variable added by ESLint. - * If `true`, this global variable was defined by a globals directive comment in the source code file. - */ - public eslintExplicitGlobalComments?: TSESTree.Comment[]; - } - export type ScopeManager = scopeManager.ScopeManager; export type Reference = scopeManager.Reference; - export type Variable = scopeManager.Variable | ESLintScopeVariable; + export type Variable = + | scopeManager.Variable + | scopeManager.ESLintScopeVariable; export type Scope = scopeManager.Scope; export const ScopeType = scopeManager.ScopeType; // TODO - in the next major, clean this up with a breaking change export type DefinitionType = scopeManager.Definition; export type Definition = scopeManager.Definition; export const DefinitionType = scopeManager.DefinitionType; + + export namespace Definitions { + export type CatchClauseDefinition = scopeManager.CatchClauseDefinition; + export type ClassNameDefinition = scopeManager.ClassNameDefinition; + export type FunctionNameDefinition = scopeManager.FunctionNameDefinition; + export type ImplicitGlobalVariableDefinition = scopeManager.ImplicitGlobalVariableDefinition; + export type ImportBindingDefinition = scopeManager.ImportBindingDefinition; + export type ParameterDefinition = scopeManager.ParameterDefinition; + export type TSEnumMemberDefinition = scopeManager.TSEnumMemberDefinition; + export type TSEnumNameDefinition = scopeManager.TSEnumNameDefinition; + export type TSModuleNameDefinition = scopeManager.TSModuleNameDefinition; + export type TypeDefinition = scopeManager.TypeDefinition; + export type VariableDefinition = scopeManager.VariableDefinition; + } + export namespace Scopes { + export type BlockScope = scopeManager.BlockScope; + export type CatchScope = scopeManager.CatchScope; + export type ClassScope = scopeManager.ClassScope; + export type ConditionalTypeScope = scopeManager.ConditionalTypeScope; + export type ForScope = scopeManager.ForScope; + export type FunctionExpressionNameScope = scopeManager.FunctionExpressionNameScope; + export type FunctionScope = scopeManager.FunctionScope; + export type FunctionTypeScope = scopeManager.FunctionTypeScope; + export type GlobalScope = scopeManager.GlobalScope; + export type MappedTypeScope = scopeManager.MappedTypeScope; + export type ModuleScope = scopeManager.ModuleScope; + export type SwitchScope = scopeManager.SwitchScope; + export type TSEnumScope = scopeManager.TSEnumScope; + export type TSModuleScope = scopeManager.TSModuleScope; + export type TypeScope = scopeManager.TypeScope; + export type WithScope = scopeManager.WithScope; + } } export { Scope }; diff --git a/packages/scope-manager/src/referencer/VisitorBase.ts b/packages/scope-manager/src/referencer/VisitorBase.ts index 0d37ac31e35..8e06863a229 100644 --- a/packages/scope-manager/src/referencer/VisitorBase.ts +++ b/packages/scope-manager/src/referencer/VisitorBase.ts @@ -3,6 +3,7 @@ import { visitorKeys, VisitorKeys } from '@typescript-eslint/visitor-keys'; interface VisitorOptions { childVisitorKeys?: VisitorKeys | null; + visitChildrenEvenIfSelectorExists?: boolean; } function isObject(obj: unknown): obj is Record { @@ -18,8 +19,11 @@ type NodeVisitor = { abstract class VisitorBase { readonly #childVisitorKeys: VisitorKeys; + readonly #visitChildrenEvenIfSelectorExists: boolean; constructor(options: VisitorOptions) { this.#childVisitorKeys = options.childVisitorKeys ?? visitorKeys; + this.#visitChildrenEvenIfSelectorExists = + options.visitChildrenEvenIfSelectorExists ?? false; } /** @@ -29,13 +33,13 @@ abstract class VisitorBase { */ visitChildren( node: T | null | undefined, - excludeArr?: (keyof T)[], + excludeArr: (keyof T)[] = [], ): void { if (node == null || node.type == null) { return; } - const exclude = new Set(excludeArr) as Set; + const exclude = new Set(excludeArr.concat(['parent'])) as Set; const children = this.#childVisitorKeys[node.type] ?? Object.keys(node); for (const key of children) { if (exclude.has(key)) { @@ -69,7 +73,10 @@ abstract class VisitorBase { const visitor = (this as NodeVisitor)[node.type]; if (visitor) { - return visitor.call(this, node); + visitor.call(this, node); + if (!this.#visitChildrenEvenIfSelectorExists) { + return; + } } this.visitChildren(node); diff --git a/packages/types/src/ts-estree.ts b/packages/types/src/ts-estree.ts index 003b0fd2e73..4dd89c962be 100644 --- a/packages/types/src/ts-estree.ts +++ b/packages/types/src/ts-estree.ts @@ -376,6 +376,12 @@ export type Expression = | TSUnaryExpression | YieldExpression; export type ForInitialiser = Expression | VariableDeclaration; +export type FunctionLike = + | ArrowFunctionExpression + | FunctionDeclaration + | FunctionExpression + | TSDeclareFunction + | TSEmptyBodyFunctionExpression; export type ImportClause = | ImportDefaultSpecifier | ImportNamespaceSpecifier