From 40e40c436265de552b5a9c2e4a7ee80d0f93d04b Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 23 Nov 2020 13:12:54 -0800 Subject: [PATCH] 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 33153561a36..aac30b87a9e 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 ac9956daa06..bd8ed859b6d 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 00000000000..8ef68ae781c --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts @@ -0,0 +1,2457 @@ +// The following tests are adapted from the tests in eslint. +// Original Code: https://github.com/eslint/eslint/blob/0cb81a9b90dd6b92bac383022f886e501bd2cb31/tests/lib/rules/no-unused-vars.js +// Licence : https://github.com/eslint/eslint/blob/0cb81a9b90dd6b92bac383022f886e501bd2cb31/LICENSE + +'use strict'; + +import { + AST_NODE_TYPES, + TSESLint, +} from '@typescript-eslint/experimental-utils'; +import rule, { MessageIds } from '../../../src/rules/no-unused-vars'; +import { RuleTester } from '../../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + // espree defaults to `script`, so we need to mirror it + sourceType: 'script', + }, +}); + +ruleTester.defineRule('use-every-a', context => { + /** + * Mark a variable as used + */ + function useA(): void { + context.markVariableAsUsed('a'); + } + return { + VariableDeclaration: useA, + ReturnStatement: useA, + }; +}); + +/** + * Returns an expected error for defined-but-not-used variables. + * @param varName The name of the variable + * @param [additional] The additional text for the message data + * @param [type] The node type (defaults to "Identifier") + * @returns An expected error object + */ +function definedError( + varName: string, + additional = '', + type = AST_NODE_TYPES.Identifier, +): TSESLint.TestCaseError { + return { + messageId: 'unusedVar', + data: { + varName, + action: 'defined', + additional, + }, + type, + }; +} + +/** + * Returns an expected error for assigned-but-not-used variables. + * @param varName The name of the variable + * @param [additional] The additional text for the message data + * @param [type] The node type (defaults to "Identifier") + * @returns An expected error object + */ +function assignedError( + varName: string, + additional = '', + type = AST_NODE_TYPES.Identifier, +): TSESLint.TestCaseError { + return { + messageId: 'unusedVar', + data: { + varName, + action: 'assigned a value', + additional, + }, + type, + }; +} + +ruleTester.run('no-unused-vars', rule, { + valid: [ + 'var foo = 5;\n\nlabel: while (true) {\n console.log(foo);\n break label;\n}', + 'var foo = 5;\n\nwhile (true) {\n console.log(foo);\n break;\n}', + { + code: ` +for (let prop in box) { + box[prop] = parseInt(box[prop]); +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + ` +var box = { a: 2 }; +for (var prop in box) { + box[prop] = parseInt(box[prop]); +} + `, + ` +f({ + set foo(a) { + return; + }, +}); + `, + { + code: ` +a; +var a; + `, + options: ['all'], + }, + { + code: ` +var a = 10; +alert(a); + `, + options: ['all'], + }, + { + code: ` +var a = 10; +(function () { + alert(a); +})(); + `, + options: ['all'], + }, + { + code: ` +var a = 10; +(function () { + setTimeout(function () { + alert(a); + }, 0); +})(); + `, + options: ['all'], + }, + { + code: ` +var a = 10; +d[a] = 0; + `, + options: ['all'], + }, + { + code: ` +(function () { + var a = 10; + return a; +})(); + `, + options: ['all'], + }, + { + code: '(function g() {})();', + options: ['all'], + }, + { + code: ` +function f(a) { + alert(a); +} +f(); + `, + options: ['all'], + }, + { + code: ` +var c = 0; +function f(a) { + var b = a; + return b; +} +f(c); + `, + options: ['all'], + }, + { + code: ` +function a(x, y) { + return y; +} +a(); + `, + options: ['all'], + }, + { + code: ` +var arr1 = [1, 2]; +var arr2 = [3, 4]; +for (var i in arr1) { + arr1[i] = 5; +} +for (var i in arr2) { + arr2[i] = 10; +} + `, + options: ['all'], + }, + { + code: 'var a = 10;', + options: ['local'], + }, + { + code: ` +var min = 'min'; +Math[min]; + `, + options: ['all'], + }, + { + code: ` +Foo.bar = function (baz) { + return baz; +}; + `, + options: ['all'], + }, + 'myFunc(function foo() {}.bind(this));', + 'myFunc(function foo() {}.toString());', + ` +function foo(first, second) { + doStuff(function () { + console.log(second); + }); +} +foo(); + `, + ` +(function () { + var doSomething = function doSomething() {}; + doSomething(); +})(); + `, + ` +try { +} catch (e) {} + `, + '/*global a */ a;', + { + code: ` +var a = 10; +(function () { + alert(a); +})(); + `, + options: [{ vars: 'all' }], + }, + { + code: ` +function g(bar, baz) { + return baz; +} +g(); + `, + options: [{ vars: 'all' }], + }, + { + code: ` +function g(bar, baz) { + return baz; +} +g(); + `, + options: [{ vars: 'all', args: 'after-used' }], + }, + { + code: ` +function g(bar, baz) { + return bar; +} +g(); + `, + options: [{ vars: 'all', args: 'none' }], + }, + { + code: ` +function g(bar, baz) { + return 2; +} +g(); + `, + options: [{ vars: 'all', args: 'none' }], + }, + { + code: ` +function g(bar, baz) { + return bar + baz; +} +g(); + `, + options: [{ vars: 'local', args: 'all' }], + }, + { + code: ` +var g = function (bar, baz) { + return 2; +}; +g(); + `, + options: [{ vars: 'all', args: 'none' }], + }, + ` +(function z() { + z(); +})(); + `, + { + code: ' ', + globals: { a: true }, + }, + { + code: ` +var who = 'Paul'; +module.exports = \`Hello \${who}!\`; + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'export var foo = 123;', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: 'export function foo() {}', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: ` +let toUpper = partial => partial.toUpperCase; +export { toUpper }; + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: 'export class foo {}', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: ` +class Foo {} +var x = new Foo(); +x.foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const foo = 'hello!'; +function bar(foobar = foo) { + foobar.replace(/!$/, ' world!'); +} +bar(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + ` +function Foo() {} +var x = new Foo(); +x.foo(); + `, + ` +function foo() { + var foo = 1; + return foo; +} +foo(); + `, + ` +function foo(foo) { + return foo; +} +foo(1); + `, + ` +function foo() { + function foo() { + return 1; + } + return foo(); +} +foo(); + `, + { + code: ` +function foo() { + var foo = 1; + return foo; +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function foo(foo) { + return foo; +} +foo(1); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function foo() { + function foo() { + return 1; + } + return foo(); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +const [y = x] = []; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +const { y = x } = {}; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +const { + z: [y = x], +} = {}; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = []; +const { z: [y] = x } = {}; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +let y; +[y = x] = []; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +let y; +({ + z: [y = x], +} = {}); +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = []; +let y; +({ z: [y] = x } = {}); +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +function foo(y = x) { + bar(y); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +function foo({ y = x } = {}) { + bar(y); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +function foo( + y = function (z = x) { + bar(z); + }, +) { + y(); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +function foo( + y = function () { + bar(x); + }, +) { + y(); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +var [y = x] = []; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +var { y = x } = {}; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +var { + z: [y = x], +} = {}; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = []; +var { z: [y] = x } = {}; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1, + y; +[y = x] = []; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1, + y; +({ + z: [y = x], +} = {}); +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = [], + y; +({ z: [y] = x } = {}); +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +function foo(y = x) { + bar(y); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +function foo({ y = x } = {}) { + bar(y); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +function foo( + y = function (z = x) { + bar(z); + }, +) { + y(); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +function foo( + y = function () { + bar(x); + }, +) { + y(); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + + // exported variables should work + "/*exported toaster*/ var toaster = 'great';", + ` +/*exported toaster, poster*/ var toaster = 1; +poster = 0; + `, + { + code: '/*exported x*/ var { x } = y;', + parserOptions: { ecmaVersion: 6 }, + }, + { + code: '/*exported x, y*/ var { x, y } = z;', + parserOptions: { ecmaVersion: 6 }, + }, + + // Can mark variables as used via context.markVariableAsUsed() + '/*eslint use-every-a:1*/ var a;', + ` +/*eslint use-every-a:1*/ !function (a) { + return 1; +}; + `, + ` +/*eslint use-every-a:1*/ !function () { + var a; + return 1; +}; + `, + + // ignore pattern + { + code: 'var _a;', + options: [{ vars: 'all', varsIgnorePattern: '^_' }], + }, + { + code: ` +var a; +function foo() { + var _b; +} +foo(); + `, + options: [{ vars: 'local', varsIgnorePattern: '^_' }], + }, + { + code: ` +function foo(_a) {} +foo(); + `, + options: [{ args: 'all', argsIgnorePattern: '^_' }], + }, + { + code: ` +function foo(a, _b) { + return a; +} +foo(); + `, + options: [{ args: 'after-used', argsIgnorePattern: '^_' }], + }, + { + code: ` +var [firstItemIgnored, secondItem] = items; +console.log(secondItem); + `, + options: [{ vars: 'all', varsIgnorePattern: '[iI]gnored' }], + parserOptions: { ecmaVersion: 6 }, + }, + + // for-in loops (see #2342) + ` +(function (obj) { + var name; + for (name in obj) return; +})({}); + `, + ` +(function (obj) { + var name; + for (name in obj) { + return; + } +})({}); + `, + ` +(function (obj) { + for (var name in obj) { + return true; + } +})({}); + `, + ` +(function (obj) { + for (var name in obj) return true; +})({}); + `, + + { + code: ` +(function (obj) { + let name; + for (name in obj) return; +})({}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +(function (obj) { + let name; + for (name in obj) { + return; + } +})({}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +(function (obj) { + for (let name in obj) { + return true; + } +})({}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +(function (obj) { + for (let name in obj) return true; +})({}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + + { + code: ` +(function (obj) { + for (const name in obj) { + return true; + } +})({}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +(function (obj) { + for (const name in obj) return true; +})({}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + + // caughtErrors + { + code: ` +try { +} catch (err) { + console.error(err); +} + `, + options: [{ caughtErrors: 'all' }], + }, + { + code: ` +try { +} catch (err) {} + `, + options: [{ caughtErrors: 'none' }], + }, + { + code: ` +try { +} catch (ignoreErr) {} + `, + options: [{ caughtErrors: 'all', caughtErrorsIgnorePattern: '^ignore' }], + }, + + // caughtErrors with other combinations + { + code: ` +try { +} catch (err) {} + `, + options: [{ vars: 'all', args: 'all' }], + }, + + // Using object rest for variable omission + { + code: ` +const data = { type: 'coords', x: 1, y: 2 }; +const { type, ...coords } = data; +console.log(coords); + `, + options: [{ ignoreRestSiblings: true }], + parserOptions: { ecmaVersion: 2018 }, + }, + + // https://github.com/eslint/eslint/issues/6348 + ` +var a = 0, + b; +b = a = a + 1; +foo(b); + `, + ` +var a = 0, + b; +b = a += a + 1; +foo(b); + `, + ` +var a = 0, + b; +b = a++; +foo(b); + `, + ` +function foo(a) { + var b = (a = a + 1); + bar(b); +} +foo(); + `, + ` +function foo(a) { + var b = (a += a + 1); + bar(b); +} +foo(); + `, + ` +function foo(a) { + var b = a++; + bar(b); +} +foo(); + `, + + // https://github.com/eslint/eslint/issues/6576 + [ + 'var unregisterFooWatcher;', + '// ...', + 'unregisterFooWatcher = $scope.$watch( "foo", function() {', + ' // ...some code..', + ' unregisterFooWatcher();', + '});', + ].join('\n'), + [ + 'var ref;', + 'ref = setInterval(', + ' function(){', + ' clearInterval(ref);', + ' }, 10);', + ].join('\n'), + [ + 'var _timer;', + 'function f() {', + ' _timer = setTimeout(function () {}, _timer ? 100 : 0);', + '}', + 'f();', + ].join('\n'), + ` +function foo(cb) { + cb = (function () { + function something(a) { + cb(1 + a); + } + register(something); + })(); +} +foo(); + `, + { + code: ` +function* foo(cb) { + cb = yield function (a) { + cb(1 + a); + }; +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function foo(cb) { + cb = tag\`hello\${function (a) { + cb(1 + a); + }}\`; +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + ` +function foo(cb) { + var b; + cb = b = function (a) { + cb(1 + a); + }; + b(); +} +foo(); + `, + + // https://github.com/eslint/eslint/issues/6646 + [ + 'function someFunction() {', + ' var a = 0, i;', + ' for (i = 0; i < 2; i++) {', + ' a = myFunction(a);', + ' }', + '}', + 'someFunction();', + ].join('\n'), + + // https://github.com/eslint/eslint/issues/7124 + { + code: ` +(function (a, b, { c, d }) { + d; +}); + `, + options: [{ argsIgnorePattern: 'c' }], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +(function (a, b, { c, d }) { + c; +}); + `, + options: [{ argsIgnorePattern: 'd' }], + parserOptions: { ecmaVersion: 6 }, + }, + + // https://github.com/eslint/eslint/issues/7250 + { + code: ` +(function (a, b, c) { + c; +}); + `, + options: [{ argsIgnorePattern: 'c' }], + }, + { + code: ` +(function (a, b, { c, d }) { + c; +}); + `, + options: [{ argsIgnorePattern: '[cd]' }], + parserOptions: { ecmaVersion: 6 }, + }, + + // https://github.com/eslint/eslint/issues/7351 + { + code: ` +(class { + set foo(UNUSED) {} +}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +class Foo { + set bar(UNUSED) {} +} +console.log(Foo); + `, + parserOptions: { ecmaVersion: 6 }, + }, + + // https://github.com/eslint/eslint/issues/8119 + { + code: '({ a, ...rest }) => rest;', + options: [{ args: 'all', ignoreRestSiblings: true }], + parserOptions: { ecmaVersion: 2018 }, + }, + + // https://github.com/eslint/eslint/issues/10952 + ` +/*eslint use-every-a:1*/ !function (b, a) { + return 1; +}; + `, + + // https://github.com/eslint/eslint/issues/10982 + ` +var a = function () { + a(); +}; +a(); + `, + ` +var a = function () { + return function () { + a(); + }; +}; +a(); + `, + { + code: ` +const a = () => { + a(); +}; +a(); + `, + parserOptions: { ecmaVersion: 2015 }, + }, + { + code: ` +const a = () => () => { + a(); +}; +a(); + `, + parserOptions: { ecmaVersion: 2015 }, + }, + + // export * as ns from "source" + { + code: "export * as ns from 'source';", + parserOptions: { ecmaVersion: 2020, sourceType: 'module' }, + }, + + // import.meta + { + code: 'import.meta;', + parserOptions: { ecmaVersion: 2020, sourceType: 'module' }, + }, + ], + invalid: [ + { + code: ` +function foox() { + return foox(); +} + `, + errors: [definedError('foox')], + }, + { + code: ` +(function () { + function foox() { + if (true) { + return foox(); + } + } +})(); + `, + errors: [definedError('foox')], + }, + { + code: 'var a = 10;', + errors: [assignedError('a')], + }, + { + code: ` +function f() { + var a = 1; + return function () { + f((a *= 2)); + }; +} + `, + errors: [definedError('f')], + }, + { + code: ` +function f() { + var a = 1; + return function () { + f(++a); + }; +} + `, + errors: [definedError('f')], + }, + { + code: '/*global a */', + errors: [definedError('a', '', AST_NODE_TYPES.Program)], + }, + { + code: ` +function foo(first, second) { + doStuff(function () { + console.log(second); + }); +} + `, + errors: [definedError('foo')], + }, + { + code: 'var a = 10;', + options: ['all'], + errors: [assignedError('a')], + }, + { + code: ` +var a = 10; +a = 20; + `, + options: ['all'], + errors: [assignedError('a')], + }, + { + code: ` +var a = 10; +(function () { + var a = 1; + alert(a); +})(); + `, + options: ['all'], + errors: [assignedError('a')], + }, + { + code: ` +var a = 10, + b = 0, + c = null; +alert(a + b); + `, + options: ['all'], + errors: [assignedError('c')], + }, + { + code: ` +var a = 10, + b = 0, + c = null; +setTimeout(function () { + var b = 2; + alert(a + b + c); +}, 0); + `, + options: ['all'], + errors: [assignedError('b')], + }, + { + code: ` +var a = 10, + b = 0, + c = null; +setTimeout(function () { + var b = 2; + var c = 2; + alert(a + b + c); +}, 0); + `, + options: ['all'], + errors: [assignedError('b'), assignedError('c')], + }, + { + code: ` +function f() { + var a = []; + return a.map(function () {}); +} + `, + options: ['all'], + errors: [definedError('f')], + }, + { + code: ` +function f() { + var a = []; + return a.map(function g() {}); +} + `, + options: ['all'], + errors: [definedError('f')], + }, + { + code: ` +function foo() { + function foo(x) { + return x; + } + return function () { + return foo; + }; +} + `, + errors: [ + { + messageId: 'unusedVar', + data: { varName: 'foo', action: 'defined', additional: '' }, + line: 2, + type: AST_NODE_TYPES.Identifier, + }, + ], + }, + { + code: ` +function f() { + var x; + function a() { + x = 42; + } + function b() { + alert(x); + } +} + `, + options: ['all'], + errors: [definedError('f'), definedError('a'), definedError('b')], + }, + { + code: ` +function f(a) {} +f(); + `, + options: ['all'], + errors: [definedError('a')], + }, + { + code: ` +function a(x, y, z) { + return y; +} +a(); + `, + options: ['all'], + errors: [definedError('z')], + }, + { + code: 'var min = Math.min;', + options: ['all'], + errors: [assignedError('min')], + }, + { + code: 'var min = { min: 1 };', + options: ['all'], + errors: [assignedError('min')], + }, + { + code: ` +Foo.bar = function (baz) { + return 1; +}; + `, + options: ['all'], + errors: [definedError('baz')], + }, + { + code: 'var min = { min: 1 };', + options: [{ vars: 'all' }], + errors: [assignedError('min')], + }, + { + code: ` +function gg(baz, bar) { + return baz; +} +gg(); + `, + options: [{ vars: 'all' }], + errors: [definedError('bar')], + }, + { + code: ` +(function (foo, baz, bar) { + return baz; +})(); + `, + options: [{ vars: 'all', args: 'after-used' }], + errors: [definedError('bar')], + }, + { + code: ` +(function (foo, baz, bar) { + return baz; +})(); + `, + options: [{ vars: 'all', args: 'all' }], + errors: [definedError('foo'), definedError('bar')], + }, + { + code: ` +(function z(foo) { + var bar = 33; +})(); + `, + options: [{ vars: 'all', args: 'all' }], + errors: [definedError('foo'), assignedError('bar')], + }, + { + code: ` +(function z(foo) { + z(); +})(); + `, + options: [{}], + errors: [definedError('foo')], + }, + { + code: ` +function f() { + var a = 1; + return function () { + f((a = 2)); + }; +} + `, + options: [{}], + errors: [definedError('f'), assignedError('a')], + }, + { + code: "import x from 'y';", + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('x')], + }, + { + code: ` +export function fn2({ x, y }) { + console.log(x); +} + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('y')], + }, + { + code: ` +export function fn2(x, y) { + console.log(x); +} + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('y')], + }, + + // exported + { + code: ` +/*exported max*/ var max = 1, + min = { min: 1 }; + `, + errors: [assignedError('min')], + }, + { + code: '/*exported x*/ var { x, y } = z;', + parserOptions: { ecmaVersion: 6 }, + errors: [assignedError('y')], + }, + + // ignore pattern + { + code: ` +var _a; +var b; + `, + options: [{ vars: 'all', varsIgnorePattern: '^_' }], + errors: [ + { + line: 3, + column: 5, + messageId: 'unusedVar', + data: { + varName: 'b', + action: 'defined', + additional: '. Allowed unused vars must match /^_/u', + }, + }, + ], + }, + { + code: ` +var a; +function foo() { + var _b; + var c_; +} +foo(); + `, + options: [{ vars: 'local', varsIgnorePattern: '^_' }], + errors: [ + { + line: 5, + column: 7, + messageId: 'unusedVar', + data: { + varName: 'c_', + action: 'defined', + additional: '. Allowed unused vars must match /^_/u', + }, + }, + ], + }, + { + code: ` +function foo(a, _b) {} +foo(); + `, + options: [{ args: 'all', argsIgnorePattern: '^_' }], + errors: [ + { + line: 2, + column: 14, + messageId: 'unusedVar', + data: { + varName: 'a', + action: 'defined', + additional: '. Allowed unused args must match /^_/u', + }, + }, + ], + }, + { + code: ` +function foo(a, _b, c) { + return a; +} +foo(); + `, + options: [{ args: 'after-used', argsIgnorePattern: '^_' }], + errors: [ + { + line: 2, + column: 21, + messageId: 'unusedVar', + data: { + varName: 'c', + action: 'defined', + additional: '. Allowed unused args must match /^_/u', + }, + }, + ], + }, + { + code: ` +function foo(_a) {} +foo(); + `, + options: [{ args: 'all', argsIgnorePattern: '[iI]gnored' }], + errors: [ + { + line: 2, + column: 14, + messageId: 'unusedVar', + data: { + varName: '_a', + action: 'defined', + additional: '. Allowed unused args must match /[iI]gnored/u', + }, + }, + ], + }, + { + code: 'var [firstItemIgnored, secondItem] = items;', + options: [{ vars: 'all', varsIgnorePattern: '[iI]gnored' }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + line: 1, + column: 24, + messageId: 'unusedVar', + data: { + varName: 'secondItem', + action: 'assigned a value', + additional: '. Allowed unused vars must match /[iI]gnored/u', + }, + }, + ], + }, + + // for-in loops (see #2342) + { + code: ` +(function (obj) { + var name; + for (name in obj) { + i(); + return; + } +})({}); + `, + errors: [ + { + line: 4, + column: 8, + messageId: 'unusedVar', + data: { + varName: 'name', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + { + code: ` +(function (obj) { + var name; + for (name in obj) { + } +})({}); + `, + errors: [ + { + line: 4, + column: 8, + messageId: 'unusedVar', + data: { + varName: 'name', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + { + code: ` +(function (obj) { + for (var name in obj) { + } +})({}); + `, + errors: [ + { + line: 3, + column: 12, + messageId: 'unusedVar', + data: { + varName: 'name', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + + // https://github.com/eslint/eslint/issues/3617 + { + code: ` +/* global foobar, foo, bar */ +foobar; + `, + errors: [ + { + line: 2, + endLine: 2, + column: 19, + endColumn: 22, + messageId: 'unusedVar', + data: { + varName: 'foo', + action: 'defined', + additional: '', + }, + }, + { + line: 2, + endLine: 2, + column: 24, + endColumn: 27, + messageId: 'unusedVar', + data: { + varName: 'bar', + action: 'defined', + additional: '', + }, + }, + ], + }, + { + code: ` +/* global foobar, + foo, + bar + */ +foobar; + `, + errors: [ + { + line: 3, + column: 4, + endLine: 3, + endColumn: 7, + messageId: 'unusedVar', + data: { + varName: 'foo', + action: 'defined', + additional: '', + }, + }, + { + line: 4, + column: 4, + endLine: 4, + endColumn: 7, + messageId: 'unusedVar', + data: { + varName: 'bar', + action: 'defined', + additional: '', + }, + }, + ], + }, + + // Rest property sibling without ignoreRestSiblings + { + code: ` +const data = { type: 'coords', x: 1, y: 2 }; +const { type, ...coords } = data; +console.log(coords); + `, + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + line: 3, + column: 9, + messageId: 'unusedVar', + data: { + varName: 'type', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + + // Unused rest property with ignoreRestSiblings + { + code: ` +const data = { type: 'coords', x: 2, y: 2 }; +const { type, ...coords } = data; +console.log(type); + `, + options: [{ ignoreRestSiblings: true }], + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + line: 3, + column: 18, + messageId: 'unusedVar', + data: { + varName: 'coords', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + + // Unused rest property without ignoreRestSiblings + { + code: ` +const data = { type: 'coords', x: 3, y: 2 }; +const { type, ...coords } = data; +console.log(type); + `, + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + line: 3, + column: 18, + messageId: 'unusedVar', + data: { + varName: 'coords', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + + // Nested array destructuring with rest property + { + code: ` +const data = { vars: ['x', 'y'], x: 1, y: 2 }; +const { + vars: [x], + ...coords +} = data; +console.log(coords); + `, + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + line: 4, + column: 10, + messageId: 'unusedVar', + data: { + varName: 'x', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + + // Nested object destructuring with rest property + { + code: ` +const data = { defaults: { x: 0 }, x: 1, y: 2 }; +const { + defaults: { x }, + ...coords +} = data; +console.log(coords); + `, + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + line: 4, + column: 15, + messageId: 'unusedVar', + data: { + varName: 'x', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + + // https://github.com/eslint/eslint/issues/8119 + { + code: '({ a, ...rest }) => {};', + options: [{ args: 'all', ignoreRestSiblings: true }], + parserOptions: { ecmaVersion: 2018 }, + errors: [definedError('rest')], + }, + + // https://github.com/eslint/eslint/issues/3714 + { + // cspell:disable-next-line + code: '/* global a$fooz,$foo */\na$fooz;', + errors: [ + { + line: 1, + column: 18, + endLine: 1, + endColumn: 22, + messageId: 'unusedVar', + data: { + varName: '$foo', + action: 'defined', + additional: '', + }, + }, + ], + }, + { + // cspell:disable-next-line + code: '/* globals a$fooz, $ */\na$fooz;', + errors: [ + { + line: 1, + column: 20, + endLine: 1, + endColumn: 21, + messageId: 'unusedVar', + data: { + varName: '$', + action: 'defined', + additional: '', + }, + }, + ], + }, + { + code: '/*globals $foo*/', + errors: [ + { + line: 1, + column: 11, + endLine: 1, + endColumn: 15, + messageId: 'unusedVar', + data: { + varName: '$foo', + action: 'defined', + additional: '', + }, + }, + ], + }, + { + code: '/* global global*/', + errors: [ + { + line: 1, + column: 11, + endLine: 1, + endColumn: 17, + messageId: 'unusedVar', + data: { + varName: 'global', + action: 'defined', + additional: '', + }, + }, + ], + }, + { + code: '/*global foo:true*/', + errors: [ + { + line: 1, + column: 10, + endLine: 1, + endColumn: 13, + messageId: 'unusedVar', + data: { + varName: 'foo', + action: 'defined', + additional: '', + }, + }, + ], + }, + + // non ascii. + { + code: '/*global 変数, 数*/\n変数;', + errors: [ + { + line: 1, + column: 14, + endLine: 1, + endColumn: 15, + messageId: 'unusedVar', + data: { + varName: '数', + action: 'defined', + additional: '', + }, + }, + ], + }, + + // surrogate pair. + { + code: ` +/*global 𠮷𩸽, 𠮷*/ +𠮷𩸽; + `, + env: { es6: true }, + errors: [ + { + line: 2, + column: 16, + endLine: 2, + endColumn: 18, + messageId: 'unusedVar', + data: { + varName: '𠮷', + action: 'defined', + additional: '', + }, + }, + ], + }, + + // https://github.com/eslint/eslint/issues/4047 + { + code: 'export default function (a) {}', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('a')], + }, + { + code: ` +export default function (a, b) { + console.log(a); +} + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('b')], + }, + { + code: 'export default (function (a) {});', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('a')], + }, + { + code: ` +export default (function (a, b) { + console.log(a); +}); + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('b')], + }, + { + code: 'export default a => {};', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('a')], + }, + { + code: ` +export default (a, b) => { + console.log(a); +}; + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('b')], + }, + + // caughtErrors + { + code: ` +try { +} catch (err) {} + `, + options: [{ caughtErrors: 'all' }], + errors: [definedError('err')], + }, + { + code: ` +try { +} catch (err) {} + `, + options: [{ caughtErrors: 'all', caughtErrorsIgnorePattern: '^ignore' }], + errors: [ + definedError('err', '. Allowed unused args must match /^ignore/u'), + ], + }, + + // multiple try catch with one success + { + code: ` +try { +} catch (ignoreErr) {} +try { +} catch (err) {} + `, + options: [{ caughtErrors: 'all', caughtErrorsIgnorePattern: '^ignore' }], + errors: [ + definedError('err', '. Allowed unused args must match /^ignore/u'), + ], + }, + + // multiple try catch both fail + { + code: ` +try { +} catch (error) {} +try { +} catch (err) {} + `, + options: [{ caughtErrors: 'all', caughtErrorsIgnorePattern: '^ignore' }], + errors: [ + definedError('error', '. Allowed unused args must match /^ignore/u'), + definedError('err', '. Allowed unused args must match /^ignore/u'), + ], + }, + + // caughtErrors with other configs + { + code: ` +try { +} catch (err) {} + `, + options: [{ vars: 'all', args: 'all', caughtErrors: 'all' }], + errors: [definedError('err')], + }, + + // no conflict in ignore patterns + { + code: ` +try { +} catch (err) {} + `, + options: [ + { + vars: 'all', + args: 'all', + caughtErrors: 'all', + argsIgnorePattern: '^er', + }, + ], + errors: [definedError('err')], + }, + + // Ignore reads for modifications to itself: https://github.com/eslint/eslint/issues/6348 + { + code: ` +var a = 0; +a = a + 1; + `, + errors: [assignedError('a')], + }, + { + code: ` +var a = 0; +a = a + a; + `, + errors: [assignedError('a')], + }, + { + code: ` +var a = 0; +a += a + 1; + `, + errors: [assignedError('a')], + }, + { + code: ` +var a = 0; +a++; + `, + errors: [assignedError('a')], + }, + { + code: ` +function foo(a) { + a = a + 1; +} +foo(); + `, + errors: [assignedError('a')], + }, + { + code: ` +function foo(a) { + a += a + 1; +} +foo(); + `, + errors: [assignedError('a')], + }, + { + code: ` +function foo(a) { + a++; +} +foo(); + `, + errors: [assignedError('a')], + }, + { + code: ` +var a = 3; +a = a * 5 + 6; + `, + errors: [assignedError('a')], + }, + { + code: ` +var a = 2, + b = 4; +a = a * 2 + b; + `, + errors: [assignedError('a')], + }, + + // https://github.com/eslint/eslint/issues/6576 (For coverage) + { + code: ` +function foo(cb) { + cb = function (a) { + cb(1 + a); + }; + bar(not_cb); +} +foo(); + `, + errors: [assignedError('cb')], + }, + { + code: ` +function foo(cb) { + cb = (function (a) { + return cb(1 + a); + })(); +} +foo(); + `, + errors: [assignedError('cb')], + }, + { + code: ` +function foo(cb) { + cb = + (function (a) { + cb(1 + a); + }, + cb); +} +foo(); + `, + errors: [assignedError('cb')], + }, + { + code: ` +function foo(cb) { + cb = + (0, + function (a) { + cb(1 + a); + }); +} +foo(); + `, + errors: [assignedError('cb')], + }, + + // https://github.com/eslint/eslint/issues/6646 + { + code: [ + 'while (a) {', + ' function foo(b) {', + ' b = b + 1;', + ' }', + ' foo()', + '}', + ].join('\n'), + errors: [assignedError('b')], + }, + + // https://github.com/eslint/eslint/issues/7124 + { + code: '(function (a, b, c) {});', + options: [{ argsIgnorePattern: 'c' }], + errors: [ + definedError('a', '. Allowed unused args must match /c/u'), + definedError('b', '. Allowed unused args must match /c/u'), + ], + }, + { + code: '(function (a, b, { c, d }) {});', + options: [{ argsIgnorePattern: '[cd]' }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + definedError('a', '. Allowed unused args must match /[cd]/u'), + definedError('b', '. Allowed unused args must match /[cd]/u'), + ], + }, + { + code: '(function (a, b, { c, d }) {});', + options: [{ argsIgnorePattern: 'c' }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + definedError('a', '. Allowed unused args must match /c/u'), + definedError('b', '. Allowed unused args must match /c/u'), + definedError('d', '. Allowed unused args must match /c/u'), + ], + }, + { + code: '(function (a, b, { c, d }) {});', + options: [{ argsIgnorePattern: 'd' }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + definedError('a', '. Allowed unused args must match /d/u'), + definedError('b', '. Allowed unused args must match /d/u'), + definedError('c', '. Allowed unused args must match /d/u'), + ], + }, + { + code: ` +/*global +foo*/ + `, + errors: [ + { + line: 3, + column: 1, + endLine: 3, + endColumn: 4, + messageId: 'unusedVar', + data: { + varName: 'foo', + action: 'defined', + additional: '', + }, + }, + ], + }, + + // https://github.com/eslint/eslint/issues/8442 + { + code: ` +(function ({ a }, b) { + return b; +})(); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [definedError('a')], + }, + { + code: ` +(function ({ a }, { b, c }) { + return b; +})(); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [definedError('a'), definedError('c')], + }, + { + code: ` +(function ({ a, b }, { c }) { + return b; +})(); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [definedError('a'), definedError('c')], + }, + { + code: ` +(function ([a], b) { + return b; +})(); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [definedError('a')], + }, + { + code: ` +(function ([a], [b, c]) { + return b; +})(); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [definedError('a'), definedError('c')], + }, + { + code: ` +(function ([a, b], [c]) { + return b; +})(); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [definedError('a'), definedError('c')], + }, + + // https://github.com/eslint/eslint/issues/9774 + { + code: '(function (_a) {})();', + options: [{ args: 'all', varsIgnorePattern: '^_' }], + errors: [definedError('_a')], + }, + { + code: '(function (_a) {})();', + options: [{ args: 'all', caughtErrorsIgnorePattern: '^_' }], + errors: [definedError('_a')], + }, + + // https://github.com/eslint/eslint/issues/10982 + { + code: ` +var a = function () { + a(); +}; + `, + errors: [assignedError('a')], + }, + { + code: ` +var a = function () { + return function () { + a(); + }; +}; + `, + errors: [assignedError('a')], + }, + { + code: ` +const a = () => { + a(); +}; + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [assignedError('a')], + }, + { + code: ` +const a = () => () => { + a(); +}; + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [assignedError('a')], + }, + { + code: ` +let myArray = [1, 2, 3, 4].filter(x => x == 0); +myArray = myArray.filter(x => x == 1); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + ...assignedError('myArray'), + line: 3, + column: 11, + }, + ], + }, + { + code: ` +const a = 1; +a += 1; + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + ...assignedError('a'), + line: 3, + column: 1, + }, + ], + }, + { + code: ` +var a = function () { + a(); +}; + `, + errors: [ + { + ...assignedError('a'), + line: 3, + column: 3, + }, + ], + }, + { + code: ` +var a = function () { + return function () { + a(); + }; +}; + `, + errors: [ + { + ...assignedError('a'), + line: 4, + column: 5, + }, + ], + }, + { + code: ` +const a = () => { + a(); +}; + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + ...assignedError('a'), + line: 3, + column: 3, + }, + ], + }, + { + code: ` +const a = () => () => { + a(); +}; + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + ...assignedError('a'), + line: 3, + column: 3, + }, + ], + }, + { + code: ` +let a = 'a'; +a = 10; +function foo() { + a = 11; + a = () => { + a = 13; + }; +} + `, + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { + ...definedError('foo'), + line: 4, + column: 10, + }, + { + ...assignedError('a'), + line: 7, + column: 5, + }, + ], + }, + { + code: ` +let c = 'c'; +c = 10; +function foo1() { + c = 11; + c = () => { + c = 13; + }; +} +c = foo1; + `, + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { + ...assignedError('c'), + line: 10, + column: 1, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts similarity index 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 49d744d4fd7..9e06ea0dc48 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts @@ -1,5 +1,5 @@ -import rule from '../../src/rules/no-unused-vars'; -import { noFormat, RuleTester } from '../RuleTester'; +import rule from '../../../src/rules/no-unused-vars'; +import { noFormat, RuleTester } from '../../RuleTester'; const ruleTester = new RuleTester({ parserOptions: { diff --git a/packages/experimental-utils/src/eslint-utils/InferTypesFromRule.ts b/packages/experimental-utils/src/eslint-utils/InferTypesFromRule.ts index 66e5b1153c3..1fd2e752baa 100644 --- a/packages/experimental-utils/src/eslint-utils/InferTypesFromRule.ts +++ b/packages/experimental-utils/src/eslint-utils/InferTypesFromRule.ts @@ -1,25 +1,26 @@ -import { RuleModule } from '../ts-eslint'; +import { RuleCreateFunction, RuleModule } from '../ts-eslint'; -type InferOptionsTypeFromRuleNever = T extends RuleModule< - never, - infer TOptions -> - ? TOptions - : unknown; /** * Uses type inference to fetch the TOptions type from the given RuleModule */ -type InferOptionsTypeFromRule = T extends RuleModule +type InferOptionsTypeFromRule = T extends RuleModule< + infer _TMessageIds, + infer TOptions +> + ? TOptions + : T extends RuleCreateFunction ? TOptions - : InferOptionsTypeFromRuleNever; + : unknown; /** * Uses type inference to fetch the TMessageIds type from the given RuleModule */ type InferMessageIdsTypeFromRule = T extends RuleModule< infer TMessageIds, - unknown[] + infer _TOptions > + ? TMessageIds + : T extends RuleCreateFunction ? TMessageIds : unknown; diff --git a/packages/experimental-utils/src/ts-eslint/Rule.ts b/packages/experimental-utils/src/ts-eslint/Rule.ts index d305b29125b..8ced374dd70 100644 --- a/packages/experimental-utils/src/ts-eslint/Rule.ts +++ b/packages/experimental-utils/src/ts-eslint/Rule.ts @@ -440,9 +440,12 @@ interface RuleModule< create(context: Readonly>): TRuleListener; } -type RuleCreateFunction = ( - context: Readonly>, -) => RuleListener; +type RuleCreateFunction< + TMessageIds extends string = never, + TOptions extends readonly unknown[] = unknown[], + // for extending base rules + TRuleListener extends RuleListener = RuleListener +> = (context: Readonly>) => TRuleListener; export { ReportDescriptor, diff --git a/packages/experimental-utils/src/ts-eslint/RuleTester.ts b/packages/experimental-utils/src/ts-eslint/RuleTester.ts index a1fa2104a91..652567f6b9f 100644 --- a/packages/experimental-utils/src/ts-eslint/RuleTester.ts +++ b/packages/experimental-utils/src/ts-eslint/RuleTester.ts @@ -1,7 +1,7 @@ import { RuleTester as ESLintRuleTester } from 'eslint'; import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '../ts-estree'; import { ParserOptions } from './ParserOptions'; -import { RuleModule } from './Rule'; +import { RuleCreateFunction, RuleModule } from './Rule'; interface ValidTestCase> { /** @@ -19,7 +19,7 @@ interface ValidTestCase> { /** * The additional global variables. */ - readonly globals?: Record; + readonly globals?: Record; /** * Options for the test case. */ @@ -157,6 +157,18 @@ declare class RuleTesterBase { * @param callback the test callback */ static it?: (text: string, callback: () => void) => void; + + /** + * Define a rule for one particular run of tests. + * @param name The name of the rule to define. + * @param rule The rule definition. + */ + defineRule>( + name: string, + rule: + | RuleModule + | RuleCreateFunction, + ): void; } class RuleTester extends (ESLintRuleTester as typeof RuleTesterBase) {} diff --git a/packages/types/src/ts-estree.ts b/packages/types/src/ts-estree.ts index 003b0fd2e73..4dd89c962be 100644 --- a/packages/types/src/ts-estree.ts +++ b/packages/types/src/ts-estree.ts @@ -376,6 +376,12 @@ export type Expression = | TSUnaryExpression | YieldExpression; export type ForInitialiser = Expression | VariableDeclaration; +export type FunctionLike = + | ArrowFunctionExpression + | FunctionDeclaration + | FunctionExpression + | TSDeclareFunction + | TSEmptyBodyFunctionExpression; export type ImportClause = | ImportDefaultSpecifier | ImportNamespaceSpecifier