UNPKG

@typescript-eslint/eslint-plugin

Version:
993 lines (979 loc) • 50.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const scope_manager_1 = require("@typescript-eslint/scope-manager"); const utils_1 = require("@typescript-eslint/utils"); const util_1 = require("../util"); const referenceContainsTypeQuery_1 = require("../util/referenceContainsTypeQuery"); // this is a superset of DefinitionType which defines sub-types for better granularity var VariableType; (function (VariableType) { // New sub-types VariableType[VariableType["ArrayDestructure"] = 0] = "ArrayDestructure"; // DefinitionType VariableType[VariableType["CatchClause"] = 1] = "CatchClause"; VariableType[VariableType["ClassName"] = 2] = "ClassName"; VariableType[VariableType["FunctionName"] = 3] = "FunctionName"; VariableType[VariableType["ImportBinding"] = 4] = "ImportBinding"; VariableType[VariableType["ImplicitGlobalVariable"] = 5] = "ImplicitGlobalVariable"; VariableType[VariableType["Parameter"] = 6] = "Parameter"; VariableType[VariableType["TSEnumMember"] = 7] = "TSEnumMember"; VariableType[VariableType["TSEnumName"] = 8] = "TSEnumName"; VariableType[VariableType["TSModuleName"] = 9] = "TSModuleName"; VariableType[VariableType["Type"] = 10] = "Type"; VariableType[VariableType["Variable"] = 11] = "Variable"; })(VariableType || (VariableType = {})); const isCommaToken = { predicate: (token) => token.type === utils_1.AST_TOKEN_TYPES.Punctuator && token.value === ',', tokenChar: ',', }; const isLeftCurlyToken = { predicate: (token) => token.type === utils_1.AST_TOKEN_TYPES.Punctuator && token.value === '{', tokenChar: '{', }; const isRightCurlyToken = { predicate: (token) => token.type === utils_1.AST_TOKEN_TYPES.Punctuator && token.value === '}', tokenChar: '}', }; function assertToken({ predicate, tokenChar }, token) { if (token == null) { throw new Error(`Expected a valid "${tokenChar}" token, but found no token`); } if (!predicate(token)) { throw new Error(`Expected a valid "${tokenChar}" token, but got "${token.value}" instead`); } return token; } exports.default = (0, util_1.createRule)({ name: 'no-unused-vars', meta: { type: 'problem', docs: { description: 'Disallow unused variables', extendsBaseRule: true, recommended: 'recommended', }, fixable: 'code', hasSuggestions: true, messages: { removeUnusedImportDeclaration: 'Remove unused import declaration.', removeUnusedVar: 'Remove unused variable "{{varName}}".', unusedVar: "'{{varName}}' is {{action}} but never used{{additional}}.", usedIgnoredVar: "'{{varName}}' is marked as ignored but is used{{additional}}.", usedOnlyAsType: "'{{varName}}' is {{action}} but only used as a type{{additional}}.", }, schema: [ { oneOf: [ { type: 'string', description: 'Broad setting for unused variables to target.', enum: ['all', 'local'], }, { type: 'object', additionalProperties: false, properties: { args: { type: 'string', description: 'Whether to check all, some, or no arguments.', enum: ['all', 'after-used', 'none'], }, argsIgnorePattern: { type: 'string', description: 'Regular expressions of argument names to not check for usage.', }, caughtErrors: { type: 'string', description: 'Whether to check catch block arguments.', enum: ['all', 'none'], }, caughtErrorsIgnorePattern: { type: 'string', description: 'Regular expressions of catch block argument names to not check for usage.', }, destructuredArrayIgnorePattern: { type: 'string', description: 'Regular expressions of destructured array variable names to not check for usage.', }, enableAutofixRemoval: { type: 'object', additionalProperties: false, description: 'Configurable automatic fixes for different types of unused variables.', properties: { imports: { type: 'boolean', description: 'Whether to enable automatic removal of unused imports.', }, }, }, ignoreClassWithStaticInitBlock: { type: 'boolean', description: 'Whether to ignore classes with at least one static initialization block.', }, ignoreRestSiblings: { type: 'boolean', description: 'Whether to ignore sibling properties in `...` destructurings.', }, ignoreUsingDeclarations: { type: 'boolean', description: 'Whether to ignore using or await using declarations.', }, reportUsedIgnorePattern: { type: 'boolean', description: 'Whether to report variables that match any of the valid ignore pattern options if they have been used.', }, vars: { type: 'string', description: 'Whether to check all variables or only locally-declared variables.', enum: ['all', 'local'], }, varsIgnorePattern: { type: 'string', description: 'Regular expressions of variable names to not check for usage.', }, }, }, ], }, ], }, defaultOptions: [{}], create(context, [firstOption]) { const MODULE_DECL_CACHE = new Map(); const reportedUnusedVariables = new Set(); function areAllSpecifiersUnused(decl) { return context.sourceCode.getDeclaredVariables(decl).every(variable => { return reportedUnusedVariables.has(variable); }); } const report = (unusedVar, opts) => { reportedUnusedVariables.add(unusedVar); const writeReferences = unusedVar.references.filter(ref => ref.isWrite() && ref.from.variableScope === unusedVar.scope.variableScope); const id = writeReferences.length ? writeReferences[writeReferences.length - 1].identifier : unusedVar.identifiers[0]; const { start } = id.loc; const idLength = id.name.length; const loc = { start, end: { column: start.column + idLength, line: start.line, }, }; const fixer = (() => { const { messageId, fix, useAutofix } = (() => { if (unusedVar.defs.length !== 1) { // If there's multiple definitions then we'd have to clean them all // up! That's complicated and messy so for now let's just ignore it. return {}; } const { type, def } = defToVariableType(unusedVar.defs[0]); switch (type) { case VariableType.ArrayDestructure: // TODO(bradzacher) -- this would be really easy to implement and // is side-effect free! return {}; case VariableType.CatchClause: // TODO(bradzacher) -- this would be really easy to implement and // is side-effect free! return {}; case VariableType.ClassName: // This would be easy to implement -- but classes can have // side-effects in static initializers / static blocks. So it's // dangerous to ever auto-fix remove them. // // Perhaps as an always-suggestion fixer...? return {}; case VariableType.FunctionName: // TODO(bradzacher) -- this would be really easy to implement and // is side-effect free! return {}; case VariableType.ImportBinding: return { ...getImportFixer(def), useAutofix: options.enableAutofixRemoval.imports, }; case VariableType.ImplicitGlobalVariable: // We don't report these via this code path, so no fixer is possible return {}; case VariableType.Parameter: // This is easy to implement -- but we cannot implement it cos it // changes the signature of the function which in turn might // introduce type errors in consumers. // // Also parameters can have default values which might have // side-effects. // // Perhaps as an always-suggestion fixer...? return {}; case VariableType.TSEnumMember: // We don't report unused enum members so no fixer is ever possible return {}; case VariableType.TSEnumName: // TODO(bradzacher) -- this would be really easy to implement and // is side-effect free! return {}; case VariableType.TSModuleName: // This is easy to implement -- but TS namespaces are eagerly // initialized -- meaning that they might have side-effects in // the body. So it's dangerous to ever auto-fix remove them. // // Perhaps as an always-suggestion fixer...? return {}; case VariableType.Type: // TODO(bradzacher) -- this would be really easy to implement and // is side-effect free! return {}; case VariableType.Variable: // TODO(bradzacher) -- this would be really easy to implement return {}; } })(); if (!fix) { return {}; } if (useAutofix) { return { fix }; } const data = { varName: unusedVar.name, }; return { suggest: [ { messageId: messageId ?? 'removeUnusedVar', data, fix, }, ], }; })(); context.report({ ...opts, ...fixer, loc, node: id, }); }; const options = (() => { const options = { args: 'after-used', caughtErrors: 'all', enableAutofixRemoval: { imports: false, }, ignoreClassWithStaticInitBlock: false, ignoreRestSiblings: false, ignoreUsingDeclarations: false, reportUsedIgnorePattern: false, vars: 'all', }; 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.ignoreUsingDeclarations = firstOption.ignoreUsingDeclarations ?? options.ignoreUsingDeclarations; options.caughtErrors = firstOption.caughtErrors ?? options.caughtErrors; options.ignoreClassWithStaticInitBlock = firstOption.ignoreClassWithStaticInitBlock ?? options.ignoreClassWithStaticInitBlock; options.reportUsedIgnorePattern = firstOption.reportUsedIgnorePattern ?? options.reportUsedIgnorePattern; if (firstOption.varsIgnorePattern) { options.varsIgnorePattern = new RegExp(firstOption.varsIgnorePattern, 'u'); } if (firstOption.argsIgnorePattern) { options.argsIgnorePattern = new RegExp(firstOption.argsIgnorePattern, 'u'); } if (firstOption.caughtErrorsIgnorePattern) { options.caughtErrorsIgnorePattern = new RegExp(firstOption.caughtErrorsIgnorePattern, 'u'); } if (firstOption.destructuredArrayIgnorePattern) { options.destructuredArrayIgnorePattern = new RegExp(firstOption.destructuredArrayIgnorePattern, 'u'); } if (firstOption.enableAutofixRemoval) { // eslint-disable-next-line unicorn/no-lonely-if -- will add more cases later if (firstOption.enableAutofixRemoval.imports != null) { options.enableAutofixRemoval.imports = firstOption.enableAutofixRemoval.imports; } } } return options; })(); function removeNodeWithTrailingNewline(fixer, node) { const sourceCode = context.sourceCode; const { line: startLine } = node.loc.start; const { line: endLine } = node.loc.end; // Expand range: start of first line to start of next line (or EOF) const lineRangeStart = sourceCode.getIndexFromLoc({ column: 0, line: startLine, }); const lineRangeEnd = endLine < sourceCode.lines.length ? sourceCode.getIndexFromLoc({ column: 0, line: endLine + 1 }) : sourceCode.text.length; // If node is the only non-whitespace on its line(s), remove full line(s) if (sourceCode.getText(node) === sourceCode.text.slice(lineRangeStart, lineRangeEnd).trim()) { return fixer.removeRange([lineRangeStart, lineRangeEnd]); } return fixer.remove(node); } function getImportFixer(def) { switch (def.node.type) { case utils_1.AST_NODE_TYPES.TSImportEqualsDeclaration: // import equals declarations can only have one binding -- so we can // just remove entire import declaration return { messageId: 'removeUnusedImportDeclaration', fix: fixer => removeNodeWithTrailingNewline(fixer, def.node), }; case utils_1.AST_NODE_TYPES.ImportDefaultSpecifier: { const importDecl = def.node.parent; if (importDecl.specifiers.length === 1 || areAllSpecifiersUnused(importDecl)) { // all specifiers are unused -- so we can just remove entire import // declaration return { messageId: 'removeUnusedImportDeclaration', fix: fixer => removeNodeWithTrailingNewline(fixer, importDecl), }; } // in this branch we know the following things: // 1) there is at least one specifier that is used // 2) the default specifier is unused // // by process of elimination we can deduce that there is at least one // named specifier that is used // // i.e. the code must be import Unused, { Used, ... } from 'module'; // // there's one or more unused named specifiers, so we must remove the // default specifier in isolation including the trailing comma. // // import Unused, { Used, ... } from 'module'; // ^^^^^^^ remove this // // NOTE: we could also remove the spaces between the comma and the // opening curly brace -- but this does risk removing comments. To be // safe we'll be conservative for now // // TODO(bradzacher) -- consider removing the extra space whilst also // preserving comments. return { messageId: 'removeUnusedVar', fix: fixer => { const comma = (0, util_1.nullThrows)(context.sourceCode.getTokenAfter(def.node), util_1.NullThrowsReasons.MissingToken(',', 'import specifier')); assertToken(isCommaToken, comma); return fixer.removeRange([ Math.min(def.node.range[0], comma.range[0]), Math.max(def.node.range[1], comma.range[1]), ]); }, }; } case utils_1.AST_NODE_TYPES.ImportSpecifier: { // guaranteed to NOT be in an export statement as we're inspecting an // import const importDecl = def.node.parent; if (importDecl.specifiers.length === 1 || areAllSpecifiersUnused(importDecl)) { // all specifiers are unused -- so we can just remove entire import // declaration return { messageId: 'removeUnusedImportDeclaration', fix: fixer => removeNodeWithTrailingNewline(fixer, importDecl), }; } return { messageId: 'removeUnusedVar', fix: fixer => { const usedNamedSpecifiers = context.sourceCode .getDeclaredVariables(importDecl) .map(variable => { if (reportedUnusedVariables.has(variable)) { return null; } const specifier = variable.defs[0].node; if (specifier.type !== utils_1.AST_NODE_TYPES.ImportSpecifier) { return null; } return specifier; }) .filter(v => v != null); if (usedNamedSpecifiers.length === 0) { // in this branch we know the following things: // 1) there is at least one specifier that is used // 2) all named specifiers are unused // // by process of elimination we can deduce that there is a // default specifier and it is the only used specifier // // i.e. the code must be import Used, { Unused, ... } from // 'module'; // // So we can just remove the entire curly content and the comma // before, eg import Used, { Unused, ... } from 'module'; // ^^^^^^^^^^^^^^^^^ remove this const leftCurly = assertToken(isLeftCurlyToken, context.sourceCode.getFirstToken(importDecl, isLeftCurlyToken.predicate)); const leftToken = assertToken(isCommaToken, context.sourceCode.getTokenBefore(leftCurly)); const rightToken = assertToken(isRightCurlyToken, context.sourceCode.getFirstToken(importDecl, isRightCurlyToken.predicate)); return fixer.removeRange([ leftToken.range[0], rightToken.range[1], ]); } // in this branch we know there is at least one used named // specifier which means we have to remove each unused specifier // in isolation. // // there's 3 possible cases to care about: import { Unused, // Used... } from 'module'; import { ...Used, Unused } from // 'module'; import { ...Used, Unused, } from 'module'; // // Note that because of the above usedNamedSpecifiers check we // know that we don't have one of these cases: import { Unused } // from 'module'; import { Unused, Unused... } from 'module'; // import { ...Unused, Unused, } from 'module'; // // The result is that we know that there _must_ be a comma that // needs cleaning up // // try to remove the leading comma first as it leads to a nicer // fix output in most cases // // leading preferred: import { Used, Unused, Used } from 'module'; // ^^^^^^^^ remove import { Used, Used } from 'module'; // // trailing preferred: import { Used, Unused, Used } from // 'module'; ^^^^^^^ remove import { Used, Used } from // 'module'; ^^ ugly double space // // But we need to still fallback to the trailing comma for cases // where the unused specifier is the first in the import eg: // import { Unused, Used } from 'module'; const maybeComma = context.sourceCode.getTokenBefore(def.node); const comma = maybeComma && isCommaToken.predicate(maybeComma) ? maybeComma : assertToken(isCommaToken, context.sourceCode.getTokenAfter(def.node)); return fixer.removeRange([ Math.min(def.node.range[0], comma.range[0]), Math.max(def.node.range[1], comma.range[1]), ]); }, }; } case utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier: { // namespace specifiers cannot be used with any other specifier -- so // we can just remove entire import declaration const importDecl = def.node.parent; return { messageId: 'removeUnusedImportDeclaration', fix: fixer => removeNodeWithTrailingNewline(fixer, importDecl), }; } } } /** * Determines what variable type a def is. * @param def the declaration to check * @returns a simple name for the types of variables that this rule supports */ function defToVariableType(def) { /* * This `destructuredArrayIgnorePattern` error report works differently from the catch * clause and parameter error reports. _Both_ the `varsIgnorePattern` and the * `destructuredArrayIgnorePattern` will be checked for array destructuring. However, * for the purposes of the report, the currently defined behavior is to only inform the * user of the `destructuredArrayIgnorePattern` if it's present (regardless of the fact * that the `varsIgnorePattern` would also apply). If it's not present, the user will be * informed of the `varsIgnorePattern`, assuming that's present. */ if (options.destructuredArrayIgnorePattern && def.name.parent.type === utils_1.AST_NODE_TYPES.ArrayPattern) { return { type: VariableType.ArrayDestructure, def }; } switch (def.type) { case scope_manager_1.DefinitionType.CatchClause: return { type: VariableType.CatchClause, def }; case scope_manager_1.DefinitionType.ClassName: return { type: VariableType.ClassName, def }; case scope_manager_1.DefinitionType.FunctionName: return { type: VariableType.FunctionName, def }; case scope_manager_1.DefinitionType.ImplicitGlobalVariable: return { type: VariableType.ImplicitGlobalVariable, def }; case scope_manager_1.DefinitionType.ImportBinding: return { type: VariableType.ImportBinding, def }; case scope_manager_1.DefinitionType.Parameter: return { type: VariableType.Parameter, def }; case scope_manager_1.DefinitionType.TSEnumName: return { type: VariableType.TSEnumName, def }; case scope_manager_1.DefinitionType.TSEnumMember: return { type: VariableType.TSEnumMember, def }; case scope_manager_1.DefinitionType.TSModuleName: return { type: VariableType.TSModuleName, def }; case scope_manager_1.DefinitionType.Type: return { type: VariableType.Type, def }; case scope_manager_1.DefinitionType.Variable: return { type: VariableType.Variable, def }; } } /** * Gets a given variable's description and configured ignore pattern * based on the provided variableType * @param variableType a simple name for the types of variables that this rule supports * @returns the given variable's description and * ignore pattern */ function getVariableDescription(variableType) { switch (variableType) { case VariableType.ArrayDestructure: return { pattern: options.destructuredArrayIgnorePattern?.toString(), variableDescription: 'elements of array destructuring', }; case VariableType.CatchClause: return { pattern: options.caughtErrorsIgnorePattern?.toString(), variableDescription: 'caught errors', }; case VariableType.Parameter: return { pattern: options.argsIgnorePattern?.toString(), variableDescription: 'args', }; default: return { pattern: options.varsIgnorePattern?.toString(), variableDescription: 'vars', }; } } /** * 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) { const def = unusedVar.defs.at(0); let additionalMessageData = ''; if (def) { const { pattern, variableDescription } = getVariableDescription(defToVariableType(def).type); if (pattern && variableDescription) { additionalMessageData = `. Allowed unused ${variableDescription} must match ${pattern}`; } } return { action: 'defined', additional: additionalMessageData, varName: unusedVar.name, }; } /** * 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) { const def = unusedVar.defs.at(0); let additionalMessageData = ''; if (def) { const { pattern, variableDescription } = getVariableDescription(defToVariableType(def).type); if (pattern && variableDescription) { additionalMessageData = `. Allowed unused ${variableDescription} must match ${pattern}`; } } return { action: 'assigned a value', additional: additionalMessageData, varName: unusedVar.name, }; } /** * Generate the warning message about a variable being used even though * it is marked as being ignored. * @param variable eslint-scope variable object * @param variableType a simple name for the types of variables that this rule supports * @returns The message data to be used with this used ignored variable. */ function getUsedIgnoredMessageData(variable, variableType) { const { pattern, variableDescription } = getVariableDescription(variableType); let additionalMessageData = ''; if (pattern && variableDescription) { additionalMessageData = `. Used ${variableDescription} must not match ${pattern}`; } return { additional: additionalMessageData, varName: variable.name, }; } function collectUnusedVariables() { /** * Checks whether a node is a sibling of the rest property or not. * @param node a node to check * @returns True if the node is a sibling of the rest property, otherwise false. */ function hasRestSibling(node) { return (node.type === utils_1.AST_NODE_TYPES.Property && node.parent.type === utils_1.AST_NODE_TYPES.ObjectPattern && node.parent.properties[node.parent.properties.length - 1].type === utils_1.AST_NODE_TYPES.RestElement); } /** * 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) { if (options.ignoreRestSiblings) { const hasRestSiblingDefinition = variable.defs.some(def => hasRestSibling(def.name.parent)); const hasRestSiblingReference = variable.references.some(ref => hasRestSibling(ref.identifier.parent)); return hasRestSiblingDefinition || hasRestSiblingReference; } 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) { const def = variable.defs[0]; const params = context.sourceCode.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); } const analysisResults = (0, util_1.collectVariables)(context); const variables = [ ...Array.from(analysisResults.unusedVariables, variable => ({ used: false, variable, })), ...Array.from(analysisResults.usedVariables, variable => ({ used: true, variable, })), ]; const unusedVariablesReturn = []; for (const { used, variable } of variables) { // explicit global variables don't have definitions. if (variable.defs.length === 0) { if (!used) { unusedVariablesReturn.push(variable); } continue; } const def = variable.defs[0]; if (variable.scope.type === utils_1.TSESLint.Scope.ScopeType.global && options.vars === 'local') { // skip variables in the global scope if configured to continue; } const refUsedInArrayPatterns = variable.references.some(ref => ref.identifier.parent.type === utils_1.AST_NODE_TYPES.ArrayPattern); // skip elements of array destructuring patterns if ((def.name.parent.type === utils_1.AST_NODE_TYPES.ArrayPattern || refUsedInArrayPatterns) && def.name.type === utils_1.AST_NODE_TYPES.Identifier && options.destructuredArrayIgnorePattern?.test(def.name.name)) { if (options.reportUsedIgnorePattern && used) { report(variable, { messageId: 'usedIgnoredVar', data: getUsedIgnoredMessageData(variable, VariableType.ArrayDestructure), }); } continue; } if (def.type === utils_1.TSESLint.Scope.DefinitionType.ClassName) { const hasStaticBlock = def.node.body.body.some(node => node.type === utils_1.AST_NODE_TYPES.StaticBlock); if (options.ignoreClassWithStaticInitBlock && hasStaticBlock) { continue; } } // skip catch variables if (def.type === utils_1.TSESLint.Scope.DefinitionType.CatchClause) { if (options.caughtErrors === 'none') { continue; } // skip ignored parameters if (options.caughtErrorsIgnorePattern?.test(def.name.name)) { if (options.reportUsedIgnorePattern && used) { report(variable, { messageId: 'usedIgnoredVar', data: getUsedIgnoredMessageData(variable, VariableType.CatchClause), }); } continue; } } else if (def.type === utils_1.TSESLint.Scope.DefinitionType.Parameter) { // if "args" option is "none", skip any parameter if (options.args === 'none') { continue; } // skip ignored parameters if (def.name.type === utils_1.AST_NODE_TYPES.Identifier && options.argsIgnorePattern?.test(def.name.name)) { if (options.reportUsedIgnorePattern && used) { report(variable, { messageId: 'usedIgnoredVar', data: getUsedIgnoredMessageData(variable, VariableType.Parameter), }); } continue; } // if "args" option is "after-used", skip used variables if (options.args === 'after-used' && (0, util_1.isFunction)(def.name.parent) && !isAfterLastUsedArg(variable)) { continue; } } // skip ignored variables else if (def.name.type === utils_1.AST_NODE_TYPES.Identifier && options.varsIgnorePattern?.test(def.name.name)) { if (options.reportUsedIgnorePattern && used && /* enum members are always marked as 'used' by `collectVariables`, but in reality they may be used or unused. either way, don't complain about their naming. */ def.type !== utils_1.TSESLint.Scope.DefinitionType.TSEnumMember) { report(variable, { messageId: 'usedIgnoredVar', data: getUsedIgnoredMessageData(variable, VariableType.Variable), }); } continue; } if (def.type === utils_1.TSESLint.Scope.DefinitionType.Variable && options.ignoreUsingDeclarations && (def.parent.kind === 'await using' || def.parent.kind === 'using')) { continue; } if (hasRestSpreadSibling(variable)) { continue; } // in case another rule has run and used the collectUnusedVariables, // we want to ensure our selectors that marked variables as used are respected if (variable.eslintUsed) { continue; } if (!used) { unusedVariablesReturn.push(variable); } } return unusedVariablesReturn; } return { // top-level declaration file handling [ambientDeclarationSelector(utils_1.AST_NODE_TYPES.Program)](node) { if (!(0, util_1.isDefinitionFile)(context.filename)) { return; } const moduleDecl = (0, util_1.nullThrows)(node.parent, util_1.NullThrowsReasons.MissingParent); if (checkForOverridingExportStatements(moduleDecl)) { return; } markDeclarationChildAsUsed(node); }, // children of a namespace that is a child of a declared namespace are auto-exported [ambientDeclarationSelector('TSModuleDeclaration[declare = true] > TSModuleBlock TSModuleDeclaration > TSModuleBlock')](node) { const moduleDecl = (0, util_1.nullThrows)(node.parent.parent, util_1.NullThrowsReasons.MissingParent); if (checkForOverridingExportStatements(moduleDecl)) { return; } markDeclarationChildAsUsed(node); }, // declared namespace handling [ambientDeclarationSelector('TSModuleDeclaration[declare = true] > TSModuleBlock')](node) { const moduleDecl = (0, util_1.nullThrows)(node.parent.parent, util_1.NullThrowsReasons.MissingParent); if (checkForOverridingExportStatements(moduleDecl)) { return; } markDeclarationChildAsUsed(node); }, // namespace handling in definition files [ambientDeclarationSelector('TSModuleDeclaration > TSModuleBlock')](node) { if (!(0, util_1.isDefinitionFile)(context.filename)) { return; } const moduleDecl = (0, util_1.nullThrows)(node.parent.parent, util_1.NullThrowsReasons.MissingParent); if (checkForOverridingExportStatements(moduleDecl)) { return; } markDeclarationChildAsUsed(node); }, // collect 'Program:exit'(programNode) { const unusedVars = collectUnusedVariables(); for (const unusedVar of unusedVars) { // Report the first declaration. if (unusedVar.defs.length > 0) { const usedOnlyAsType = unusedVar.references.some(ref => (0, referenceContainsTypeQuery_1.referenceContainsTypeQuery)(ref.identifier)); const messageId = usedOnlyAsType ? 'usedOnlyAsType' : 'unusedVar'; const isImportUsedOnlyAsType = usedOnlyAsType && unusedVar.defs.some(def => def.type === scope_manager_1.DefinitionType.ImportBinding); if (isImportUsedOnlyAsType) { continue; } report(unusedVar, { messageId, 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({ loc: (0, util_1.getNameLocationInGlobalDirectiveComment)(context.sourceCode, directiveComment, unusedVar.name), node: programNode, messageId: 'unusedVar', data: getDefinedMessageData(unusedVar), }); } } }, }; function checkForOverridingExportStatements(node) { const cached = MODULE_DECL_CACHE.get(node); if (cached != null) { return cached; } const body = getStatementsOfNode(node); if (hasOverridingExportStatement(body)) { MODULE_DECL_CACHE.set(node, true); return true; } MODULE_DECL_CACHE.set(node, false); return false; } function ambientDeclarationSelector(parent) { return [ // Types are ambiently exported `${parent} > :matches(${[ utils_1.AST_NODE_TYPES.TSInterfaceDeclaration, utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration, ].join(', ')})`, // Value things are ambiently exported if they are "declare"d `${parent} > :matches(${[ utils_1.AST_NODE_TYPES.ClassDeclaration, utils_1.AST_NODE_TYPES.TSDeclareFunction, utils_1.AST_NODE_TYPES.TSEnumDeclaration, utils_1.AST_NODE_TYPES.TSModuleDeclaration, utils_1.AST_NODE_TYPES.VariableDeclaration, ].join(', ')})`, ].join(', '); } function markDeclarationChildAsUsed(node) { const identifiers = []; switch (node.type) { case utils_1.AST_NODE_TYPES.TSInterfaceDeclaration: case utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration: case utils_1.AST_NODE_TYPES.ClassDeclaration: case utils_1.AST_NODE_TYPES.FunctionDeclaration: case utils_1.AST_NODE_TYPES.TSDeclareFunction: case utils_1.AST_NODE_TYPES.TSEnumDeclaration: case utils_1.AST_NODE_TYPES.TSModuleDeclaration: if (node.id?.type === utils_1.AST_NODE_TYPES.Identifier) { identifiers.push(node.id); } break; case utils_1.AST_NODE_TYPES.VariableDeclaration: for (const declaration of node.declarations) { visitPattern(declaration, pattern => { identifiers.push(pattern); }); } break; } let scope = context.sourceCode.getScope(node); const shouldUseUpperScope = [ utils_1.AST_NODE_TYPES.TSDeclareFunction, utils_1.AST_NODE_TYPES.TSModuleDeclaration, ].includes(node.type); if (scope.variableScope !== scope) { scope = scope.variableScope; } else if (shouldUseUpperScope && scope.upper) { scope = scope.upper; } for (const id of identifiers) { const superVar = scope.set.get(id.name); if (superVar) { superVar.eslintUsed = true; } } } function visitPattern(node, cb) { const visitor = new scope_manager_1.PatternVisitor({}, node, cb); visitor.visit(node); } }, }); function hasOverridingExportStatement(body) { for (const statement of body) { if ((statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration && statement.declaration == null) || statement.type === utils_1.AST_NODE_TYPES.ExportAllDeclaration || statement.type === utils_1.AST_NODE_TYPES.TSExportAssignment) { return true; } if (statement.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration && statement.declaration.type === utils_1.AST_NODE_TYPES.Identifier) { return true; } } return false; } function getStatementsOfNode(block) { if (block.type === utils_1.AST_NODE_TYPES.Program) { return block.body; } return block.body.body; } /* ###### TODO ###### Edge cases that aren't currently handled due to laziness and them being super edgy edge cases --- function params referenced in typeof type refs in the function declaration --- --- NOTE - TS gets these cases wrong function _foo( arg: number // arg should be unused ): typeof arg { return 1 as any; } function _bar( arg: number, // arg should be unused _arg2: typeof arg, ) {} --- function names referenced in typeof type refs in the function declaration --- --- NOTE - TS gets these cases right function foo( // foo should be unused ): typeof foo { return 1 as any; } 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'; }