From 8062050c04b124ebf36f3d8356164f21cc1138d9 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 16 Nov 2020 11:35:37 -0800 Subject: [PATCH 1/2] feat(eslint-plugin): [no-unused-vars] fork the base rule I wanted to avoid doing this, but not doing this restricts our logic too much - it causes problems when we want to consider reporting on things that the base rule wouldn't report on. Fixes #2714 Fixes #2648 Closes #2679 --- .../eslint-plugin/src/rules/no-unused-vars.ts | 500 +++++++----- .../src/util/collectUnusedVariables.ts | 729 ++++++++++++++++++ packages/eslint-plugin/src/util/index.ts | 1 + .../tests/rules/no-unused-vars.test.ts | 64 +- .../eslint-plugin/typings/eslint-rules.d.ts | 14 + .../src/ast-utils/predicates.ts | 22 + .../experimental-utils/src/ts-eslint/Scope.ts | 75 +- .../src/referencer/VisitorBase.ts | 13 +- 8 files changed, 1196 insertions(+), 222 deletions(-) create mode 100644 packages/eslint-plugin/src/util/collectUnusedVariables.ts diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index 8753452dd097..33153561a365 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; +type MessageIds = 'unusedVar'; +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,200 @@ 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 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), - ); - } - - 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); + 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 + ); }); } - }, - 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); - } - }, - 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'), + + return false; + } + + /** + * 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, ); - for (const variable of scope.variables) { - const moduleNodes = getModuleNameDeclarations(variable); + } - 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) - ) { - continue; + const unusedVariablesOriginal = util.collectUnusedVariables(context); + const unusedVariablesReturn: TSESLint.Scope.Variable[] = []; + for (const variable of unusedVariablesOriginal) { + // explicit global variables don't have definitions. + const def = variable.defs[0]; + if (def) { + // skip catch variables + if (def.type === TSESLint.Scope.DefinitionType.CatchClause) { + if (options.caughtErrors === 'none') { + continue; + } + // skip ignored parameters + if ( + '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: '', - }, - }); + if (def.type === TSESLint.Scope.DefinitionType.Parameter) { + // if "args" option is "none", skip any parameter + if (options.args === 'none') { + continue; + } + // skip ignored parameters + if ( + 'name' in def.name && + options.argsIgnorePattern?.test(def.name.name) + ) { + 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; + } } } - }, - [[ - '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 ( - ref.identifier.range[0] >= node.range[0] && - ref.identifier.range[1] <= node.range[1] - ) { - return true; - } - 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 +246,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 +275,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 +518,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 000000000000..ac9956daa06b --- /dev/null +++ b/packages/eslint-plugin/src/util/collectUnusedVariables.ts @@ -0,0 +1,729 @@ +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; + + for ( + let node: TSESTree.Node | undefined = currentNode; + node; + node = node.parent + ) { + 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; + } + } + + return this.#scopeManager.scopes[0] as T; + } + + private markVariableAsUsed(variable: TSESLint.Scope.Variable): void; + private markVariableAsUsed(identifier: 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 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); + }); + } + } + + //#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 Identifier(node: TSESTree.Identifier): void { + const scope = this.getScope(node); + if (scope.type === TSESLint.Scope.ScopeType.function) { + switch (node.name) { + case '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); + } + + break; + } + + case 'arguments': { + // skip implicit "arguments" variable + this.markVariableAsUsed(node); + break; + } + } + } + } + + protected MethodDefinition(node: TSESTree.MethodDefinition): void { + if (node.kind === 'set') { + // ignore setter parameters because they're syntactically required to exist + for (const param of node.value.params) { + this.visitPattern(param, id => { + this.markVariableAsUsed(id); + }); + } + } + } + + 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) { + 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 === '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))) + ); + } + + /** + * (bradzacher): 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 this specific code + * ``` + * for (var key in object) return; + * ``` + * + * I disagree with it, but what are you going to do. + * + * https://github.com/eslint/eslint/issues/2342 + */ + function isForInRef(ref: TSESLint.Scope.Reference): boolean { + let target = ref.identifier.parent!; + + // "for (var ...) { return; }" + if (target.type === AST_NODE_TYPES.VariableDeclarator) { + target = target.parent!.parent!; + } + + if (target.type !== AST_NODE_TYPES.ForInStatement) { + return false; + } + + // "for (...) { return; }" + if (target.body.type === AST_NODE_TYPES.BlockStatement) { + target = target.body.body[0]; + + // "for (...) return;" + } else { + target = target.body; + } + + // For empty loop body + if (!target) { + return false; + } + + return target.type === AST_NODE_TYPES.ReturnStatement; + } + + 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 => { + if (isForInRef(ref)) { + return true; + } + + 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 672f50dc4fff..af0a64eddbfc 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.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts index 9bc46bc75ade..49d744d4fd7a 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts @@ -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 4e10fe235786..93b75e3a956d 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 c23f30f17077..bf62a6f77cb2 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/ts-eslint/Scope.ts b/packages/experimental-utils/src/ts-eslint/Scope.ts index 2934a4d27561..6907e2290fd9 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 0d37ac31e352..8e06863a229d 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); From 40e40c436265de552b5a9c2e4a7ee80d0f93d04b Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 23 Nov 2020 13:12:54 -0800 Subject: [PATCH 2/2] feat: add eslint tests and fix bugs --- .../eslint-plugin/src/rules/no-unused-vars.ts | 97 +- .../src/util/collectUnusedVariables.ts | 176 +- .../no-unused-vars-eslint.test.ts | 2457 +++++++++++++++++ .../no-unused-vars.test.ts | 4 +- .../src/eslint-utils/InferTypesFromRule.ts | 21 +- .../experimental-utils/src/ts-eslint/Rule.ts | 9 +- .../src/ts-eslint/RuleTester.ts | 16 +- packages/types/src/ts-estree.ts | 6 + 8 files changed, 2649 insertions(+), 137 deletions(-) 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 (99%) diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index 33153561a365..aac30b87a9e2 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -7,8 +7,8 @@ import { PatternVisitor } from '@typescript-eslint/scope-manager'; import { getNameLocationInGlobalDirectiveComment } from 'eslint/lib/rules/utils/ast-utils'; import * as util from '../util'; -type MessageIds = 'unusedVar'; -type Options = [ +export type MessageIds = 'unusedVar'; +export type Options = [ | 'all' | 'local' | { @@ -180,50 +180,61 @@ export default util.createRule({ 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; + } const def = variable.defs[0]; - if (def) { - // skip catch variables - if (def.type === TSESLint.Scope.DefinitionType.CatchClause) { - if (options.caughtErrors === 'none') { - continue; - } - // skip ignored parameters - if ( - 'name' in def.name && - options.caughtErrorsIgnorePattern?.test(def.name.name) - ) { - continue; - } + + if ( + variable.scope.type === TSESLint.Scope.ScopeType.global && + options.vars === 'local' + ) { + // skip variables in the global scope if configured to + continue; + } + + // skip catch variables + if (def.type === TSESLint.Scope.DefinitionType.CatchClause) { + if (options.caughtErrors === 'none') { + continue; } + // skip ignored parameters + if ( + 'name' in def.name && + options.caughtErrorsIgnorePattern?.test(def.name.name) + ) { + continue; + } + } - if (def.type === TSESLint.Scope.DefinitionType.Parameter) { - // if "args" option is "none", skip any parameter - if (options.args === 'none') { - continue; - } - // skip ignored parameters - if ( - 'name' in def.name && - options.argsIgnorePattern?.test(def.name.name) - ) { - 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; - } + if (def.type === TSESLint.Scope.DefinitionType.Parameter) { + // if "args" option is "none", skip any parameter + if (options.args === 'none') { + continue; + } + // skip ignored parameters + if ( + 'name' in def.name && + options.argsIgnorePattern?.test(def.name.name) + ) { + 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; } } diff --git a/packages/eslint-plugin/src/util/collectUnusedVariables.ts b/packages/eslint-plugin/src/util/collectUnusedVariables.ts index ac9956daa06b..bd8ed859b6d5 100644 --- a/packages/eslint-plugin/src/util/collectUnusedVariables.ts +++ b/packages/eslint-plugin/src/util/collectUnusedVariables.ts @@ -92,11 +92,8 @@ class UnusedVarsVisitor< // 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; - for ( - let node: TSESTree.Node | undefined = currentNode; - node; - node = node.parent - ) { + let node: TSESTree.Node | undefined = currentNode; + while (node) { const scope = this.#scopeManager.acquire(node, inner); if (scope) { @@ -105,13 +102,16 @@ class UnusedVarsVisitor< } return scope as T; } + + node = node.parent; } return this.#scopeManager.scopes[0] as T; } - private markVariableAsUsed(variable: TSESLint.Scope.Variable): void; - private markVariableAsUsed(identifier: TSESTree.Identifier): void; + private markVariableAsUsed( + variableOrIdentifier: TSESLint.Scope.Variable | TSESTree.Identifier, + ): void; private markVariableAsUsed(name: string, parent: TSESTree.Node): void; private markVariableAsUsed( variableOrIdentifierOrName: @@ -153,6 +153,17 @@ class UnusedVarsVisitor< } } + 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 @@ -172,6 +183,19 @@ class UnusedVarsVisitor< } } + 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 @@ -188,39 +212,76 @@ class UnusedVarsVisitor< } } - protected Identifier(node: TSESTree.Identifier): void { - const scope = this.getScope(node); - if (scope.type === TSESLint.Scope.ScopeType.function) { - switch (node.name) { - case '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 FunctionDeclaration = this.visitFunction; - break; - } + protected FunctionExpression = this.visitFunction; - case 'arguments': { - // skip implicit "arguments" variable - this.markVariableAsUsed(node); - break; - } + 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 MethodDefinition(node: TSESTree.MethodDefinition): void { - if (node.kind === 'set') { - // ignore setter parameters because they're syntactically required to exist - for (const param of node.value.params) { - this.visitPattern(param, id => { - this.markVariableAsUsed(id); - }); + 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; @@ -330,6 +391,13 @@ const MERGABLE_TYPES = new Set([ 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) || @@ -355,7 +423,7 @@ function isExported(variable: TSESLint.Scope.Variable): boolean { if (node.type === AST_NODE_TYPES.VariableDeclarator) { node = node.parent!; - } else if (definition.type === 'Parameter') { + } else if (definition.type === TSESLint.Scope.DefinitionType.Parameter) { return false; } @@ -636,48 +704,6 @@ function isUsedVariable(variable: TSESLint.Scope.Variable): boolean { ); } - /** - * (bradzacher): 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 this specific code - * ``` - * for (var key in object) return; - * ``` - * - * I disagree with it, but what are you going to do. - * - * https://github.com/eslint/eslint/issues/2342 - */ - function isForInRef(ref: TSESLint.Scope.Reference): boolean { - let target = ref.identifier.parent!; - - // "for (var ...) { return; }" - if (target.type === AST_NODE_TYPES.VariableDeclarator) { - target = target.parent!.parent!; - } - - if (target.type !== AST_NODE_TYPES.ForInStatement) { - return false; - } - - // "for (...) { return; }" - if (target.body.type === AST_NODE_TYPES.BlockStatement) { - target = target.body.body[0]; - - // "for (...) return;" - } else { - target = target.body; - } - - // For empty loop body - if (!target) { - return false; - } - - return target.type === AST_NODE_TYPES.ReturnStatement; - } - const functionNodes = getFunctionDefinitions(variable); const isFunctionDefinition = functionNodes.size > 0; @@ -690,10 +716,6 @@ function isUsedVariable(variable: TSESLint.Scope.Variable): boolean { let rhsNode: TSESTree.Node | null = null; return variable.references.some(ref => { - if (isForInRef(ref)) { - return true; - } - const forItself = isReadForItself(ref, rhsNode); rhsNode = getRhsNode(ref, rhsNode); 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 000000000000..8ef68ae781ce --- /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 99% 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 49d744d4fd7a..9e06ea0dc484 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: { diff --git a/packages/experimental-utils/src/eslint-utils/InferTypesFromRule.ts b/packages/experimental-utils/src/eslint-utils/InferTypesFromRule.ts index 66e5b1153c3a..1fd2e752baa4 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 d305b29125b9..8ced374dd700 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 a1fa2104a91e..652567f6b9fd 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/types/src/ts-estree.ts b/packages/types/src/ts-estree.ts index 003b0fd2e736..4dd89c962bef 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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy