📄 no-unused-vars¶
📊 Analysis Summary¶
| Metric | Count |
|---|---|
| 🔧 Functions | 30 |
| 📦 Imports | 19 |
| 📊 Variables & Constants | 3 |
| 📐 Interfaces | 2 |
| 📑 Type Aliases | 5 |
| 🎯 Enums | 1 |
📚 Table of Contents¶
🛠️ File Location:¶
📂 packages/eslint-plugin/src/rules/no-unused-vars.ts
📤 Default Export¶
| Property | Value |
|---|---|
name |
'no-unused-vars' |
meta.type |
'problem' |
meta.docs.description |
'Disallow unused variables' |
meta.docs.extendsBaseRule |
true |
meta.docs.recommended |
'recommended' |
meta.fixable |
'code' |
meta.hasSuggestions |
true |
meta.messages.removeUnusedImportDeclaration |
'Remove unused import declaration.' |
meta.messages.removeUnusedVar |
'Remove unused variable "{{varName}}".' |
meta.messages.unusedVar |
"'{{varName}}' is {{action}} but never used{{additional}}." |
meta.messages.usedIgnoredVar |
"'{{varName}}' is marked as ignored but is used{{additional}}." |
meta.messages.usedOnlyAsType |
"'{{varName}}' is {{action}} but only used as a type{{additional}}." |
meta.schema |
[ { oneOf: [ { type: 'string', description: 'Broad setting for unused variables to target.', enum: ['all', 'local'], ... |
defaultOptions |
[{}] |
Entry point: create — documented under Functions.
📦 Imports¶
| Name | Source |
|---|---|
Definition |
@typescript-eslint/scope-manager |
ImportBindingDefinition |
@typescript-eslint/scope-manager |
ScopeVariable |
@typescript-eslint/scope-manager |
TSESTree |
@typescript-eslint/utils |
DefinitionType |
@typescript-eslint/scope-manager |
PatternVisitor |
@typescript-eslint/scope-manager |
AST_NODE_TYPES |
@typescript-eslint/utils |
AST_TOKEN_TYPES |
@typescript-eslint/utils |
TSESLint |
@typescript-eslint/utils |
MakeRequired |
../util |
collectVariables |
../util |
createRule |
../util |
getNameLocationInGlobalDirectiveComment |
../util |
isDefinitionFile |
../util |
isFunction |
../util |
nullThrows |
../util |
NullThrowsReasons |
../util |
referenceContainsTypePredicate |
../util/referenceContainsTypePredicate |
referenceContainsTypeQuery |
../util/referenceContainsTypeQuery |
Variables & Constants¶
| Name | Type | Kind | Value | Exported |
|---|---|---|---|---|
isCommaToken |
Predicate |
const | { predicate: (token: TSESTree.Token): boolean => token.type === AST_TOKEN_TYP... |
✗ |
isLeftCurlyToken |
Predicate |
const | { predicate: (token: TSESTree.Token): boolean => token.type === AST_TOKEN_TYP... |
✗ |
isRightCurlyToken |
Predicate |
const | { predicate: (token: TSESTree.Token): boolean => token.type === AST_TOKEN_TYP... |
✗ |
Functions¶
create(context: any, [firstOption]: any): { [x: string]: (node: any) => void; 'Program:exit'(programN…¶
Parameters:
contextany[firstOption]any
Returns: { [x: string]: (node: any) => void; 'Program:exit'(programNode: any): void; }
Calls:
context.sourceCode.getDeclaredVariables(decl).everyreportedUnusedVariables.hasreportedUnusedVariables.addunusedVar.references.filterref.isWritecomplex_call_9876complex_call_9932defToVariableTypegetImportFixercontext.reportcomplex_call_13729sourceCode.getIndexFromLocsourceCode.getTextsourceCode.text.slice(lineRangeStart, lineRangeEnd).trimfixer.removeRangefixer.removeremoveNodeWithTrailingNewlineareAllSpecifiersUnusednullThrows (from ../util)context.sourceCode.getTokenAfterNullThrowsReasons.MissingTokenassertTokenMath.minMath.maxcontext.sourceCode .getDeclaredVariables(importDecl) .map(variable => { if (reportedUnusedVariables.has(variable)) { return null; } const specifier = variable.defs[0].node; if (specifier.type !== AST_NODE_TYPES.ImportSpecifier) { return null; } return specifier; }) .filtercontext.sourceCode.getFirstTokencontext.sourceCode.getTokenBeforeisCommaToken.predicateoptions.destructuredArrayIgnorePattern?.toStringoptions.caughtErrorsIgnorePattern?.toStringoptions.argsIgnorePattern?.toStringoptions.varsIgnorePattern?.toStringunusedVar.defs.atgetVariableDescriptionvariable.defs.somehasRestSiblingvariable.references.somecontext.sourceCode.getDeclaredVariablesparams.sliceparams.indexOfposteriorParams.somecollectVariables (from ../util)Array.fromunusedVariablesReturn.pushoptions.destructuredArrayIgnorePattern?.testreportgetUsedIgnoredMessageDatadef.node.body.body.someoptions.caughtErrorsIgnorePattern?.testoptions.argsIgnorePattern?.testisFunction (from ../util)isAfterLastUsedArgoptions.varsIgnorePattern?.testhasRestSpreadSiblingambientDeclarationSelectorisDefinitionFile (from ../util)checkForOverridingExportStatementsmarkDeclarationChildAsUsedcollectUnusedVariablesunusedVar.references.somereferenceContainsTypeQuery (from ../util/referenceContainsTypeQuery)referenceContainsTypePredicate (from ../util/referenceContainsTypePredicate)unusedVar.defs.somegetAssignedMessageDatagetDefinedMessageDatagetNameLocationInGlobalDirectiveComment (from ../util)MODULE_DECL_CACHE.getgetStatementsOfNodehasOverridingExportStatementMODULE_DECL_CACHE.set[ // Types are ambiently exported${parent} > :matches(${[ AST_NODE_TYPES.TSInterfaceDeclaration, AST_NODE_TYPES.TSTypeAliasDeclaration, ].join(', ')}), // Value things are ambiently exported if they are "declare"d${parent} > :matches(${[ AST_NODE_TYPES.ClassDeclaration, AST_NODE_TYPES.TSDeclareFunction, AST_NODE_TYPES.TSEnumDeclaration, AST_NODE_TYPES.TSModuleDeclaration, AST_NODE_TYPES.VariableDeclaration, ].join(', ')}), ].join[ AST_NODE_TYPES.TSInterfaceDeclaration, AST_NODE_TYPES.TSTypeAliasDeclaration, ].join[ AST_NODE_TYPES.ClassDeclaration, AST_NODE_TYPES.TSDeclareFunction, AST_NODE_TYPES.TSEnumDeclaration, AST_NODE_TYPES.TSModuleDeclaration, AST_NODE_TYPES.VariableDeclaration, ].joinidentifiers.pushvisitPatterncontext.sourceCode.getScope[ AST_NODE_TYPES.TSDeclareFunction, AST_NODE_TYPES.TSModuleDeclaration, ].includesscope.set.getvisitor.visit
Internal Comments:
// 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.
// TODO(bradzacher) -- this would be really easy to implement and (x5)
// is side-effect free! (x5)
// 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.
// (x30)
// Perhaps as an always-suggestion fixer...? (x3)
// We don't report these via this code path, so no fixer is possible
// 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.
// We don't report unused enum members so no fixer is ever possible
// 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.
// TODO(bradzacher) -- this would be really easy to implement
// eslint-disable-next-line unicorn/no-lonely-if -- will add more cases later
// Expand range: start of first line to start of next line (or EOF) (x2)
// If node is the only non-whitespace on its line(s), remove full line(s)
// import equals declarations can only have one binding -- so we can
// just remove entire import declaration
// all specifiers are unused -- so we can just remove entire import (x2)
// declaration (x2)
// in this branch we know the following things: (x3)
// 1) there is at least one specifier that is used (x3)
// 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.
// guaranteed to NOT be in an export statement as we're inspecting an (x2)
// import (x2)
// 2) all named specifiers are unused (x2)
// by process of elimination we can deduce that there is a (x2)
// default specifier and it is the only used specifier (x2)
// i.e. the code must be import Used, { Unused, ... } from (x2)
// 'module'; (x2)
// So we can just remove the entire curly content and the comma (x2)
// before, eg import Used, { Unused, ... } from 'module'; (x2)
// ^^^^^^^^^^^^^^^^^ remove this (x2)
// in this branch we know there is at least one used named (x2)
// specifier which means we have to remove each unused specifier (x2)
// in isolation. (x2)
// there's 3 possible cases to care about: import { Unused, (x2)
// Used... } from 'module'; import { ...Used, Unused } from (x2)
// 'module'; import { ...Used, Unused, } from 'module'; (x2)
// Note that because of the above usedNamedSpecifiers check we (x2)
// know that we don't have one of these cases: import { Unused } (x2)
// from 'module'; import { Unused, Unused... } from 'module'; (x2)
// import { ...Unused, Unused, } from 'module'; (x2)
// The result is that we know that there _must_ be a comma that (x2)
// needs cleaning up (x2)
// try to remove the leading comma first as it leads to a nicer (x2)
// fix output in most cases (x2)
// leading preferred: import { Used, Unused, Used } from 'module'; (x2)
// ^^^^^^^^ remove import { Used, Used } from 'module'; (x2)
// trailing preferred: import { Used, Unused, Used } from (x2)
// 'module'; ^^^^^^^ remove import { Used, Used } from (x2)
// 'module'; ^^ ugly double space (x2)
// But we need to still fallback to the trailing comma for cases (x2)
// where the unused specifier is the first in the import eg: (x2)
// import { Unused, Used } from 'module'; (x2)
// namespace specifiers cannot be used with any other specifier -- so (x2)
// we can just remove entire import declaration (x2)
/**
* 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
*/
/*
* 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.
*/
/**
* 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
*/
/**
* 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.
*/
/**
* 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.
*/
/**
* 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.
*/
/**
* 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.
*/
/**
* 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.
*/
/**
* 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.
*/
// If any used parameters occur after this parameter, do not report.
// explicit global variables don't have definitions.
// skip variables in the global scope if configured to
// skip elements of array destructuring patterns
// skip catch variables
// skip ignored parameters (x2)
// if "args" option is "none", skip any parameter
// if "args" option is "after-used", skip used variables
/* 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. */ (x3)
// in case another rule has run and used the collectUnusedVariables,
// we want to ensure our selectors that marked variables as used are respected
// top-level declaration file handling (x2)
// children of a namespace that is a child of a declared namespace are auto-exported (x2)
// declared namespace handling (x2)
// namespace handling in definition files (x2)
// collect (x2)
// Report the first declaration.
// Types are ambiently exported (x2)
// Value things are ambiently exported if they are "declare"d (x2)
Code
create(context, [firstOption]) {
const MODULE_DECL_CACHE = new Map<
ModuleDeclarationWithBody | TSESTree.Program,
boolean
>();
const reportedUnusedVariables = new Set<ScopeVariable>();
function areAllSpecifiersUnused(decl: TSESTree.ImportDeclaration): boolean {
return context.sourceCode.getDeclaredVariables(decl).every(variable => {
return reportedUnusedVariables.has(variable);
});
}
const report = (
unusedVar: ScopeVariable,
opts: {
data: TSESLint.ReportDescriptorMessageData;
messageId: MessageIds;
node?: TSESTree.Node;
},
) => {
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 } = ((): {
fix?: TSESLint.ReportFixFunction;
messageId?: MessageIds;
useAutofix?: boolean;
} => {
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' as const),
data,
fix,
},
],
};
})();
context.report({
...opts,
...fixer,
loc,
node: id,
});
};
const options = ((): TranslatedOptions => {
const options: TranslatedOptions = {
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: TSESLint.RuleFixer,
node: TSESTree.Node,
): TSESLint.RuleFix {
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: ImportBindingDefinition): {
fix: TSESLint.ReportFixFunction;
messageId: MessageIds;
} {
switch (def.node.type) {
case 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 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 = nullThrows(
context.sourceCode.getTokenAfter(def.node),
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 AST_NODE_TYPES.ImportSpecifier: {
// guaranteed to NOT be in an export statement as we're inspecting an
// import
const importDecl = def.node.parent as Exclude<
typeof def.node.parent,
TSESTree.ExportAllDeclaration | TSESTree.ExportNamedDeclaration
>;
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 !== 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 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: Definition): DefToVariableType {
/*
* 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 === AST_NODE_TYPES.ArrayPattern
) {
return { type: VariableType.ArrayDestructure, def };
}
switch (def.type) {
case DefinitionType.CatchClause:
return { type: VariableType.CatchClause, def };
case DefinitionType.ClassName:
return { type: VariableType.ClassName, def };
case DefinitionType.FunctionName:
return { type: VariableType.FunctionName, def };
case DefinitionType.ImplicitGlobalVariable:
return { type: VariableType.ImplicitGlobalVariable, def };
case DefinitionType.ImportBinding:
return { type: VariableType.ImportBinding, def };
case DefinitionType.Parameter:
return { type: VariableType.Parameter, def };
case DefinitionType.TSEnumName:
return { type: VariableType.TSEnumName, def };
case DefinitionType.TSEnumMember:
return { type: VariableType.TSEnumMember, def };
case DefinitionType.TSModuleName:
return { type: VariableType.TSModuleName, def };
case DefinitionType.Type:
return { type: VariableType.Type, def };
case 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: VariableType): {
pattern: string | undefined;
variableDescription: string;
} {
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: ScopeVariable,
): Record<string, unknown> {
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: ScopeVariable,
): Record<string, unknown> {
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: ScopeVariable,
variableType: VariableType,
): Record<string, unknown> {
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(): ScopeVariable[] {
/**
* 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: TSESTree.Node): boolean {
return (
node.type === AST_NODE_TYPES.Property &&
node.parent.type === AST_NODE_TYPES.ObjectPattern &&
node.parent.properties[node.parent.properties.length - 1].type ===
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: ScopeVariable): boolean {
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: ScopeVariable): boolean {
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 = collectVariables(context);
const variables = [
...Array.from(analysisResults.unusedVariables, variable => ({
used: false,
variable,
})),
...Array.from(analysisResults.usedVariables, variable => ({
used: true,
variable,
})),
];
const unusedVariablesReturn: ScopeVariable[] = [];
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 === 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 === AST_NODE_TYPES.ArrayPattern,
);
// skip elements of array destructuring patterns
if (
(def.name.parent.type === AST_NODE_TYPES.ArrayPattern ||
refUsedInArrayPatterns) &&
def.name.type === 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 === TSESLint.Scope.DefinitionType.ClassName) {
const hasStaticBlock = def.node.body.body.some(
node => node.type === AST_NODE_TYPES.StaticBlock,
);
if (options.ignoreClassWithStaticInitBlock && hasStaticBlock) {
continue;
}
}
// skip catch variables
if (def.type === 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 === TSESLint.Scope.DefinitionType.Parameter) {
// if "args" option is "none", skip any parameter
if (options.args === 'none') {
continue;
}
// skip ignored parameters
if (
def.name.type === 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' &&
isFunction(def.name.parent) &&
!isAfterLastUsedArg(variable)
) {
continue;
}
}
// skip ignored variables
else if (
def.name.type === 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 !== TSESLint.Scope.DefinitionType.TSEnumMember
) {
report(variable, {
messageId: 'usedIgnoredVar',
data: getUsedIgnoredMessageData(variable, VariableType.Variable),
});
}
continue;
}
if (
def.type === 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(AST_NODE_TYPES.Program)](
node: DeclarationSelectorNode,
): void {
if (!isDefinitionFile(context.filename)) {
return;
}
const moduleDecl = nullThrows(
node.parent,
NullThrowsReasons.MissingParent,
) as TSESTree.Program;
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: DeclarationSelectorNode): void {
const moduleDecl = nullThrows(
node.parent.parent,
NullThrowsReasons.MissingParent,
) as ModuleDeclarationWithBody;
if (checkForOverridingExportStatements(moduleDecl)) {
return;
}
markDeclarationChildAsUsed(node);
},
// declared namespace handling
[ambientDeclarationSelector(
'TSModuleDeclaration[declare = true] > TSModuleBlock',
)](node: DeclarationSelectorNode): void {
const moduleDecl = nullThrows(
node.parent.parent,
NullThrowsReasons.MissingParent,
) as ModuleDeclarationWithBody;
if (checkForOverridingExportStatements(moduleDecl)) {
return;
}
markDeclarationChildAsUsed(node);
},
// namespace handling in definition files
[ambientDeclarationSelector('TSModuleDeclaration > TSModuleBlock')](
node: DeclarationSelectorNode,
): void {
if (!isDefinitionFile(context.filename)) {
return;
}
const moduleDecl = nullThrows(
node.parent.parent,
NullThrowsReasons.MissingParent,
) as ModuleDeclarationWithBody;
if (checkForOverridingExportStatements(moduleDecl)) {
return;
}
markDeclarationChildAsUsed(node);
},
// collect
'Program:exit'(programNode): void {
const unusedVars = collectUnusedVariables();
for (const unusedVar of unusedVars) {
// Report the first declaration.
if (unusedVar.defs.length > 0) {
const usedOnlyAsType = unusedVar.references.some(
ref =>
referenceContainsTypeQuery(ref.identifier) ||
referenceContainsTypePredicate(ref.identifier),
);
const messageId = usedOnlyAsType ? 'usedOnlyAsType' : 'unusedVar';
const isImportUsedOnlyAsType =
usedOnlyAsType &&
unusedVar.defs.some(
def => def.type === 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: getNameLocationInGlobalDirectiveComment(
context.sourceCode,
directiveComment,
unusedVar.name,
),
node: programNode,
messageId: 'unusedVar',
data: getDefinedMessageData(unusedVar),
});
}
}
},
};
function checkForOverridingExportStatements(
node: ModuleDeclarationWithBody | TSESTree.Program,
): boolean {
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;
}
type DeclarationSelectorNode =
| TSESTree.ClassDeclaration
| TSESTree.FunctionDeclaration
| TSESTree.TSDeclareFunction
| TSESTree.TSEnumDeclaration
| TSESTree.TSInterfaceDeclaration
| TSESTree.TSModuleDeclaration
| TSESTree.TSTypeAliasDeclaration
| TSESTree.VariableDeclaration;
function ambientDeclarationSelector(parent: string): string {
return [
// Types are ambiently exported
`${parent} > :matches(${[
AST_NODE_TYPES.TSInterfaceDeclaration,
AST_NODE_TYPES.TSTypeAliasDeclaration,
].join(', ')})`,
// Value things are ambiently exported if they are "declare"d
`${parent} > :matches(${[
AST_NODE_TYPES.ClassDeclaration,
AST_NODE_TYPES.TSDeclareFunction,
AST_NODE_TYPES.TSEnumDeclaration,
AST_NODE_TYPES.TSModuleDeclaration,
AST_NODE_TYPES.VariableDeclaration,
].join(', ')})`,
].join(', ');
}
function markDeclarationChildAsUsed(node: DeclarationSelectorNode): void {
const identifiers: TSESTree.Identifier[] = [];
switch (node.type) {
case AST_NODE_TYPES.TSInterfaceDeclaration:
case AST_NODE_TYPES.TSTypeAliasDeclaration:
case AST_NODE_TYPES.ClassDeclaration:
case AST_NODE_TYPES.FunctionDeclaration:
case AST_NODE_TYPES.TSDeclareFunction:
case AST_NODE_TYPES.TSEnumDeclaration:
case AST_NODE_TYPES.TSModuleDeclaration:
if (node.id?.type === AST_NODE_TYPES.Identifier) {
identifiers.push(node.id);
}
break;
case 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 = [
AST_NODE_TYPES.TSDeclareFunction,
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: TSESTree.Node,
cb: (node: TSESTree.Identifier) => void,
): void {
const visitor = new PatternVisitor({}, node, cb);
visitor.visit(node);
}
}
isCommaToken.predicate(token: TSESTree.Token): boolean¶
Parameters:
tokenTSESTree.Token
Returns: boolean
Code
isLeftCurlyToken.predicate(token: TSESTree.Token): boolean¶
Parameters:
tokenTSESTree.Token
Returns: boolean
Code
isRightCurlyToken.predicate(token: TSESTree.Token): boolean¶
Parameters:
tokenTSESTree.Token
Returns: boolean
Code
assertToken({ predicate, tokenChar }: Predicate, token: TSESTree.Token | null): TSESTree.Token¶
Parameters:
{ predicate, tokenChar }PredicatetokenTSESTree.Token | null
Returns: TSESTree.Token
Calls:
predicate
Code
function assertToken(
{ predicate, tokenChar }: Predicate,
token: TSESTree.Token | null,
): TSESTree.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;
}
hasOverridingExportStatement(body: TSESTree.ProgramStatement[]): boolean¶
Parameters:
bodyTSESTree.ProgramStatement[]
Returns: boolean
Code
function hasOverridingExportStatement(
body: TSESTree.ProgramStatement[],
): boolean {
for (const statement of body) {
if (
(statement.type === AST_NODE_TYPES.ExportNamedDeclaration &&
statement.declaration == null) ||
statement.type === AST_NODE_TYPES.ExportAllDeclaration ||
statement.type === AST_NODE_TYPES.TSExportAssignment
) {
return true;
}
if (
statement.type === AST_NODE_TYPES.ExportDefaultDeclaration &&
statement.declaration.type === AST_NODE_TYPES.Identifier
) {
return true;
}
}
return false;
}
getStatementsOfNode(block: ModuleDeclarationWithBody | TSESTree.Pr…): TSESTree.ProgramStatement[]¶
Parameters:
blockModuleDeclarationWithBody | TSESTree.Program
Returns: TSESTree.ProgramStatement[]
Code
Internal helpers¶
Declared inside another function in this file.
areAllSpecifiersUnused(decl: TSESTree.ImportDeclaration): boolean¶
Parameters:
declTSESTree.ImportDeclaration
Returns: boolean
Calls:
context.sourceCode.getDeclaredVariables(decl).everyreportedUnusedVariables.has
Code
report(unusedVar: ScopeVariable, opts: { data: TSESLint.ReportDescriptorMessag…): void¶
Parameters:
unusedVarScopeVariableopts{ data: TSESLint.ReportDescriptorMessageData; messageId: MessageIds; node?: TSESTree.Node; }
Returns: void
Calls:
reportedUnusedVariables.addunusedVar.references.filterref.isWritecomplex_call_9876complex_call_9932defToVariableTypegetImportFixercontext.report
Internal Comments:
// 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.
// TODO(bradzacher) -- this would be really easy to implement and (x5)
// is side-effect free! (x5)
// 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.
// (x4)
// Perhaps as an always-suggestion fixer...? (x3)
// We don't report these via this code path, so no fixer is possible
// 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.
// We don't report unused enum members so no fixer is ever possible
// 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.
// TODO(bradzacher) -- this would be really easy to implement
Code
(
unusedVar: ScopeVariable,
opts: {
data: TSESLint.ReportDescriptorMessageData;
messageId: MessageIds;
node?: TSESTree.Node;
},
) => {
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 } = ((): {
fix?: TSESLint.ReportFixFunction;
messageId?: MessageIds;
useAutofix?: boolean;
} => {
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' as const),
data,
fix,
},
],
};
})();
context.report({
...opts,
...fixer,
loc,
node: id,
});
}
removeNodeWithTrailingNewline(fixer: TSESLint.RuleFixer, node: TSESTree.Node): TSESLint.RuleFix¶
Parameters:
fixerTSESLint.RuleFixernodeTSESTree.Node
Returns: TSESLint.RuleFix
Calls:
sourceCode.getIndexFromLocsourceCode.getTextsourceCode.text.slice(lineRangeStart, lineRangeEnd).trimfixer.removeRangefixer.remove
Internal Comments:
// Expand range: start of first line to start of next line (or EOF) (x2)
// If node is the only non-whitespace on its line(s), remove full line(s)
Code
function removeNodeWithTrailingNewline(
fixer: TSESLint.RuleFixer,
node: TSESTree.Node,
): TSESLint.RuleFix {
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);
}
getImportFixer(def: ImportBindingDefinition): { fix: TSESLint.ReportFixFunction; messageId: MessageIds; }¶
Parameters:
defImportBindingDefinition
Returns: {
fix: TSESLint.ReportFixFunction;
messageId: MessageIds;
}
Calls:
removeNodeWithTrailingNewlineareAllSpecifiersUnusednullThrows (from ../util)context.sourceCode.getTokenAfterNullThrowsReasons.MissingTokenassertTokenfixer.removeRangeMath.minMath.maxcontext.sourceCode .getDeclaredVariables(importDecl) .map(variable => { if (reportedUnusedVariables.has(variable)) { return null; } const specifier = variable.defs[0].node; if (specifier.type !== AST_NODE_TYPES.ImportSpecifier) { return null; } return specifier; }) .filtercontext.sourceCode.getFirstTokencontext.sourceCode.getTokenBeforeisCommaToken.predicate
Internal Comments:
// import equals declarations can only have one binding -- so we can
// just remove entire import declaration
// all specifiers are unused -- so we can just remove entire import (x2)
// declaration (x2)
// in this branch we know the following things: (x3)
// 1) there is at least one specifier that is used (x3)
// 2) the default specifier is unused
// (x26)
// 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.
// guaranteed to NOT be in an export statement as we're inspecting an (x2)
// import (x2)
// 2) all named specifiers are unused (x2)
// by process of elimination we can deduce that there is a (x2)
// default specifier and it is the only used specifier (x2)
// i.e. the code must be import Used, { Unused, ... } from (x2)
// 'module'; (x2)
// So we can just remove the entire curly content and the comma (x2)
// before, eg import Used, { Unused, ... } from 'module'; (x2)
// ^^^^^^^^^^^^^^^^^ remove this (x2)
// in this branch we know there is at least one used named (x2)
// specifier which means we have to remove each unused specifier (x2)
// in isolation. (x2)
// there's 3 possible cases to care about: import { Unused, (x2)
// Used... } from 'module'; import { ...Used, Unused } from (x2)
// 'module'; import { ...Used, Unused, } from 'module'; (x2)
// Note that because of the above usedNamedSpecifiers check we (x2)
// know that we don't have one of these cases: import { Unused } (x2)
// from 'module'; import { Unused, Unused... } from 'module'; (x2)
// import { ...Unused, Unused, } from 'module'; (x2)
// The result is that we know that there _must_ be a comma that (x2)
// needs cleaning up (x2)
// try to remove the leading comma first as it leads to a nicer (x2)
// fix output in most cases (x2)
// leading preferred: import { Used, Unused, Used } from 'module'; (x2)
// ^^^^^^^^ remove import { Used, Used } from 'module'; (x2)
// trailing preferred: import { Used, Unused, Used } from (x2)
// 'module'; ^^^^^^^ remove import { Used, Used } from (x2)
// 'module'; ^^ ugly double space (x2)
// But we need to still fallback to the trailing comma for cases (x2)
// where the unused specifier is the first in the import eg: (x2)
// import { Unused, Used } from 'module'; (x2)
// namespace specifiers cannot be used with any other specifier -- so (x2)
// we can just remove entire import declaration (x2)
Code
function getImportFixer(def: ImportBindingDefinition): {
fix: TSESLint.ReportFixFunction;
messageId: MessageIds;
} {
switch (def.node.type) {
case 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 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 = nullThrows(
context.sourceCode.getTokenAfter(def.node),
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 AST_NODE_TYPES.ImportSpecifier: {
// guaranteed to NOT be in an export statement as we're inspecting an
// import
const importDecl = def.node.parent as Exclude<
typeof def.node.parent,
TSESTree.ExportAllDeclaration | TSESTree.ExportNamedDeclaration
>;
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 !== 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 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),
};
}
}
}
getImportFixer.fix(fixer: any): RuleFix¶
Parameters:
fixerany
Returns: RuleFix
Calls:
removeNodeWithTrailingNewline
getImportFixer.fix(fixer: any): RuleFix¶
Parameters:
fixerany
Returns: RuleFix
Calls:
removeNodeWithTrailingNewline
getImportFixer.fix(fixer: any): any¶
Parameters:
fixerany
Returns: any
Calls:
nullThrows (from ../util)context.sourceCode.getTokenAfterNullThrowsReasons.MissingTokenassertTokenfixer.removeRangeMath.minMath.max
Code
getImportFixer.fix(fixer: any): RuleFix¶
Parameters:
fixerany
Returns: RuleFix
Calls:
removeNodeWithTrailingNewline
getImportFixer.fix(fixer: any): any¶
Parameters:
fixerany
Returns: any
Calls:
context.sourceCode .getDeclaredVariables(importDecl) .map(variable => { if (reportedUnusedVariables.has(variable)) { return null; } const specifier = variable.defs[0].node; if (specifier.type !== AST_NODE_TYPES.ImportSpecifier) { return null; } return specifier; }) .filterassertTokencontext.sourceCode.getFirstTokencontext.sourceCode.getTokenBeforefixer.removeRangeisCommaToken.predicatecontext.sourceCode.getTokenAfterMath.minMath.max
Internal Comments:
// in this branch we know the following things: (x2)
// 1) there is at least one specifier that is used (x2)
// 2) all named specifiers are unused (x2)
// (x20)
// by process of elimination we can deduce that there is a (x2)
// default specifier and it is the only used specifier (x2)
// i.e. the code must be import Used, { Unused, ... } from (x2)
// 'module'; (x2)
// So we can just remove the entire curly content and the comma (x2)
// before, eg import Used, { Unused, ... } from 'module'; (x2)
// ^^^^^^^^^^^^^^^^^ remove this (x2)
// in this branch we know there is at least one used named (x2)
// specifier which means we have to remove each unused specifier (x2)
// in isolation. (x2)
// there's 3 possible cases to care about: import { Unused, (x2)
// Used... } from 'module'; import { ...Used, Unused } from (x2)
// 'module'; import { ...Used, Unused, } from 'module'; (x2)
// Note that because of the above usedNamedSpecifiers check we (x2)
// know that we don't have one of these cases: import { Unused } (x2)
// from 'module'; import { Unused, Unused... } from 'module'; (x2)
// import { ...Unused, Unused, } from 'module'; (x2)
// The result is that we know that there _must_ be a comma that (x2)
// needs cleaning up (x2)
// try to remove the leading comma first as it leads to a nicer (x2)
// fix output in most cases (x2)
// leading preferred: import { Used, Unused, Used } from 'module'; (x2)
// ^^^^^^^^ remove import { Used, Used } from 'module'; (x2)
// trailing preferred: import { Used, Unused, Used } from (x2)
// 'module'; ^^^^^^^ remove import { Used, Used } from (x2)
// 'module'; ^^ ugly double space (x2)
// But we need to still fallback to the trailing comma for cases (x2)
// where the unused specifier is the first in the import eg: (x2)
// import { Unused, Used } from 'module'; (x2)
Code
fixer => {
const usedNamedSpecifiers = context.sourceCode
.getDeclaredVariables(importDecl)
.map(variable => {
if (reportedUnusedVariables.has(variable)) {
return null;
}
const specifier = variable.defs[0].node;
if (specifier.type !== 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]),
]);
}
getImportFixer.fix(fixer: any): RuleFix¶
Parameters:
fixerany
Returns: RuleFix
Calls:
removeNodeWithTrailingNewline
defToVariableType(def: Definition): DefToVariableType¶
Determines what variable type a def is.
Parameters:
defany: the declaration to check
Returns: undefined
a simple name for the types of variables that this rule supports
Raw JSDoc
Internal Comments:
/*
* 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.
*/
Code
function defToVariableType(def: Definition): DefToVariableType {
/*
* 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 === AST_NODE_TYPES.ArrayPattern
) {
return { type: VariableType.ArrayDestructure, def };
}
switch (def.type) {
case DefinitionType.CatchClause:
return { type: VariableType.CatchClause, def };
case DefinitionType.ClassName:
return { type: VariableType.ClassName, def };
case DefinitionType.FunctionName:
return { type: VariableType.FunctionName, def };
case DefinitionType.ImplicitGlobalVariable:
return { type: VariableType.ImplicitGlobalVariable, def };
case DefinitionType.ImportBinding:
return { type: VariableType.ImportBinding, def };
case DefinitionType.Parameter:
return { type: VariableType.Parameter, def };
case DefinitionType.TSEnumName:
return { type: VariableType.TSEnumName, def };
case DefinitionType.TSEnumMember:
return { type: VariableType.TSEnumMember, def };
case DefinitionType.TSModuleName:
return { type: VariableType.TSModuleName, def };
case DefinitionType.Type:
return { type: VariableType.Type, def };
case DefinitionType.Variable:
return { type: VariableType.Variable, def };
}
}
getVariableDescription(variableType: VariableType): { pattern: string | undefined; variableDescription: string;…¶
Gets a given variable's description and configured ignore pattern based on the provided variableType
Parameters:
variableTypeany: a simple name for the types of variables that this rule supports
Returns: undefined
the given variable's description and
ignore pattern
Raw JSDoc
Calls:
options.destructuredArrayIgnorePattern?.toStringoptions.caughtErrorsIgnorePattern?.toStringoptions.argsIgnorePattern?.toStringoptions.varsIgnorePattern?.toString
Code
function getVariableDescription(variableType: VariableType): {
pattern: string | undefined;
variableDescription: string;
} {
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',
};
}
}
getDefinedMessageData(unusedVar: ScopeVariable): Record<string, unknown>¶
Generates the message data about the variable being defined and unused, including the ignore pattern if configured.
Parameters:
unusedVarany: eslint-scope variable object.
Returns: undefined
The message data to be used with this unused variable.
Raw JSDoc
Calls:
unusedVar.defs.atgetVariableDescriptiondefToVariableType
Code
function getDefinedMessageData(
unusedVar: ScopeVariable,
): Record<string, unknown> {
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,
};
}
getAssignedMessageData(unusedVar: ScopeVariable): Record<string, unknown>¶
Generate the warning message about the variable being assigned and unused, including the ignore pattern if configured.
Parameters:
unusedVarany: eslint-scope variable object.
Returns: undefined
The message data to be used with this unused variable.
Raw JSDoc
Calls:
unusedVar.defs.atgetVariableDescriptiondefToVariableType
Code
function getAssignedMessageData(
unusedVar: ScopeVariable,
): Record<string, unknown> {
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,
};
}
getUsedIgnoredMessageData(variable: ScopeVariable, variableType: VariableType): Record<string, unknown>¶
Generate the warning message about a variable being used even though it is marked as being ignored.
Parameters:
variableany: eslint-scope variable objectvariableTypeany: a simple name for the types of variables that this rule supports
Returns: undefined
The message data to be used with this used ignored variable.
Raw JSDoc
/**
* 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.
*/
Calls:
getVariableDescription
Code
function getUsedIgnoredMessageData(
variable: ScopeVariable,
variableType: VariableType,
): Record<string, unknown> {
const { pattern, variableDescription } =
getVariableDescription(variableType);
let additionalMessageData = '';
if (pattern && variableDescription) {
additionalMessageData = `. Used ${variableDescription} must not match ${pattern}`;
}
return {
additional: additionalMessageData,
varName: variable.name,
};
}
collectUnusedVariables(): ScopeVariable[]¶
Returns: ScopeVariable[]
Calls:
variable.defs.somehasRestSiblingvariable.references.somecontext.sourceCode.getDeclaredVariablesparams.sliceparams.indexOfposteriorParams.somecollectVariables (from ../util)Array.fromunusedVariablesReturn.pushoptions.destructuredArrayIgnorePattern?.testreportgetUsedIgnoredMessageDatadef.node.body.body.someoptions.caughtErrorsIgnorePattern?.testoptions.argsIgnorePattern?.testisFunction (from ../util)isAfterLastUsedArgoptions.varsIgnorePattern?.testhasRestSpreadSibling
Internal Comments:
/**
* 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.
*/
/**
* 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.
*/
/**
* 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.
*/
// If any used parameters occur after this parameter, do not report.
// explicit global variables don't have definitions.
// skip variables in the global scope if configured to
// skip elements of array destructuring patterns
// skip catch variables
// skip ignored parameters (x2)
// if "args" option is "none", skip any parameter
// if "args" option is "after-used", skip used variables
/* 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. */ (x3)
// in case another rule has run and used the collectUnusedVariables,
// we want to ensure our selectors that marked variables as used are respected
Code
function collectUnusedVariables(): ScopeVariable[] {
/**
* 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: TSESTree.Node): boolean {
return (
node.type === AST_NODE_TYPES.Property &&
node.parent.type === AST_NODE_TYPES.ObjectPattern &&
node.parent.properties[node.parent.properties.length - 1].type ===
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: ScopeVariable): boolean {
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: ScopeVariable): boolean {
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 = collectVariables(context);
const variables = [
...Array.from(analysisResults.unusedVariables, variable => ({
used: false,
variable,
})),
...Array.from(analysisResults.usedVariables, variable => ({
used: true,
variable,
})),
];
const unusedVariablesReturn: ScopeVariable[] = [];
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 === 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 === AST_NODE_TYPES.ArrayPattern,
);
// skip elements of array destructuring patterns
if (
(def.name.parent.type === AST_NODE_TYPES.ArrayPattern ||
refUsedInArrayPatterns) &&
def.name.type === 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 === TSESLint.Scope.DefinitionType.ClassName) {
const hasStaticBlock = def.node.body.body.some(
node => node.type === AST_NODE_TYPES.StaticBlock,
);
if (options.ignoreClassWithStaticInitBlock && hasStaticBlock) {
continue;
}
}
// skip catch variables
if (def.type === 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 === TSESLint.Scope.DefinitionType.Parameter) {
// if "args" option is "none", skip any parameter
if (options.args === 'none') {
continue;
}
// skip ignored parameters
if (
def.name.type === 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' &&
isFunction(def.name.parent) &&
!isAfterLastUsedArg(variable)
) {
continue;
}
}
// skip ignored variables
else if (
def.name.type === 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 !== TSESLint.Scope.DefinitionType.TSEnumMember
) {
report(variable, {
messageId: 'usedIgnoredVar',
data: getUsedIgnoredMessageData(variable, VariableType.Variable),
});
}
continue;
}
if (
def.type === 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;
}
hasRestSibling(node: TSESTree.Node): boolean¶
Checks whether a node is a sibling of the rest property or not.
Parameters:
nodeany: a node to check
Returns: undefined
True if the node is a sibling of the rest property, otherwise false.
Raw JSDoc
Code
hasRestSpreadSibling(variable: ScopeVariable): boolean¶
Determines if a variable has a sibling rest property
Parameters:
variableany: eslint-scope variable object.
Returns: undefined
True if the variable is exported, false if not.
Raw JSDoc
Calls:
variable.defs.somehasRestSiblingvariable.references.some
Code
function hasRestSpreadSibling(variable: ScopeVariable): boolean {
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;
}
isAfterLastUsedArg(variable: ScopeVariable): boolean¶
Checks whether the given variable is after the last used parameter.
Parameters:
variableany: The variable to check.
Returns: undefined
true if the variable is defined after the last used parameter.
Raw JSDoc
Calls:
context.sourceCode.getDeclaredVariablesparams.sliceparams.indexOfposteriorParams.some
Internal Comments:
Code
function isAfterLastUsedArg(variable: ScopeVariable): boolean {
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,
);
}
checkForOverridingExportStatements(node: ModuleDeclarationWithBody | TSESTree.Pr…): boolean¶
Parameters:
nodeModuleDeclarationWithBody | TSESTree.Program
Returns: boolean
Calls:
MODULE_DECL_CACHE.getgetStatementsOfNodehasOverridingExportStatementMODULE_DECL_CACHE.set
Code
function checkForOverridingExportStatements(
node: ModuleDeclarationWithBody | TSESTree.Program,
): boolean {
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;
}
ambientDeclarationSelector(parent: string): string¶
Parameters:
parentstring
Returns: string
Calls:
[ // Types are ambiently exported${parent} > :matches(${[ AST_NODE_TYPES.TSInterfaceDeclaration, AST_NODE_TYPES.TSTypeAliasDeclaration, ].join(', ')}), // Value things are ambiently exported if they are "declare"d${parent} > :matches(${[ AST_NODE_TYPES.ClassDeclaration, AST_NODE_TYPES.TSDeclareFunction, AST_NODE_TYPES.TSEnumDeclaration, AST_NODE_TYPES.TSModuleDeclaration, AST_NODE_TYPES.VariableDeclaration, ].join(', ')}), ].join[ AST_NODE_TYPES.TSInterfaceDeclaration, AST_NODE_TYPES.TSTypeAliasDeclaration, ].join[ AST_NODE_TYPES.ClassDeclaration, AST_NODE_TYPES.TSDeclareFunction, AST_NODE_TYPES.TSEnumDeclaration, AST_NODE_TYPES.TSModuleDeclaration, AST_NODE_TYPES.VariableDeclaration, ].join
Internal Comments:
// Types are ambiently exported (x2)
// Value things are ambiently exported if they are "declare"d (x2)
Code
function ambientDeclarationSelector(parent: string): string {
return [
// Types are ambiently exported
`${parent} > :matches(${[
AST_NODE_TYPES.TSInterfaceDeclaration,
AST_NODE_TYPES.TSTypeAliasDeclaration,
].join(', ')})`,
// Value things are ambiently exported if they are "declare"d
`${parent} > :matches(${[
AST_NODE_TYPES.ClassDeclaration,
AST_NODE_TYPES.TSDeclareFunction,
AST_NODE_TYPES.TSEnumDeclaration,
AST_NODE_TYPES.TSModuleDeclaration,
AST_NODE_TYPES.VariableDeclaration,
].join(', ')})`,
].join(', ');
}
markDeclarationChildAsUsed(node: DeclarationSelectorNode): void¶
Parameters:
nodeDeclarationSelectorNode
Returns: void
Calls:
identifiers.pushvisitPatterncontext.sourceCode.getScope[ AST_NODE_TYPES.TSDeclareFunction, AST_NODE_TYPES.TSModuleDeclaration, ].includesscope.set.get
Code
function markDeclarationChildAsUsed(node: DeclarationSelectorNode): void {
const identifiers: TSESTree.Identifier[] = [];
switch (node.type) {
case AST_NODE_TYPES.TSInterfaceDeclaration:
case AST_NODE_TYPES.TSTypeAliasDeclaration:
case AST_NODE_TYPES.ClassDeclaration:
case AST_NODE_TYPES.FunctionDeclaration:
case AST_NODE_TYPES.TSDeclareFunction:
case AST_NODE_TYPES.TSEnumDeclaration:
case AST_NODE_TYPES.TSModuleDeclaration:
if (node.id?.type === AST_NODE_TYPES.Identifier) {
identifiers.push(node.id);
}
break;
case 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 = [
AST_NODE_TYPES.TSDeclareFunction,
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;
}
}
}
visitPattern(node: TSESTree.Node, cb: (node: TSESTree.Identifier) => void): void¶
Parameters:
nodeTSESTree.Nodecb(node: TSESTree.Identifier) => void
Returns: void
Calls:
visitor.visit
Code
Interfaces¶
TranslatedOptions¶
Interface Code
interface TranslatedOptions {
args: 'after-used' | 'all' | 'none';
argsIgnorePattern?: RegExp;
caughtErrors: 'all' | 'none';
caughtErrorsIgnorePattern?: RegExp;
destructuredArrayIgnorePattern?: RegExp;
enableAutofixRemoval: {
imports: boolean;
};
ignoreClassWithStaticInitBlock: boolean;
ignoreRestSiblings: boolean;
ignoreUsingDeclarations: boolean;
reportUsedIgnorePattern: boolean;
vars: 'all' | 'local';
varsIgnorePattern?: RegExp;
}
Properties¶
| Name | Type | Optional | Description |
|---|---|---|---|
args |
'after-used' \| 'all' \| 'none' |
✗ | not shown |
argsIgnorePattern |
RegExp |
✓ | not shown |
caughtErrors |
'all' \| 'none' |
✗ | not shown |
caughtErrorsIgnorePattern |
RegExp |
✓ | not shown |
destructuredArrayIgnorePattern |
RegExp |
✓ | not shown |
enableAutofixRemoval |
{ imports: boolean; } |
✗ | not shown |
ignoreClassWithStaticInitBlock |
boolean |
✗ | not shown |
ignoreRestSiblings |
boolean |
✗ | not shown |
ignoreUsingDeclarations |
boolean |
✗ | not shown |
reportUsedIgnorePattern |
boolean |
✗ | not shown |
vars |
'all' \| 'local' |
✗ | not shown |
varsIgnorePattern |
RegExp |
✓ | not shown |
Predicate¶
Interface Code
Properties¶
| Name | Type | Optional | Description |
|---|---|---|---|
predicate |
(token: TSESTree.Token) => boolean |
✗ | not shown |
tokenChar |
string |
✗ | not shown |
Type Aliases¶
MessageIds¶
type MessageIds = | 'removeUnusedImportDeclaration'
| 'removeUnusedVar'
| 'unusedVar'
| 'usedIgnoredVar'
| 'usedOnlyAsType';
Options¶
type Options = [
| 'all'
| 'local'
| {
args?: 'after-used' | 'all' | 'none';
argsIgnorePattern?: string;
caughtErrors?: 'all' | 'none';
caughtErrorsIgnorePattern?: string;
destructuredArrayIgnorePattern?: string;
enableAutofixRemoval?: {
imports?: boolean;
};
ignoreClassWithStaticInitBlock?: boolean;
ignoreRestSiblings?: boolean;
ignoreUsingDeclarations?: boolean;
reportUsedIgnorePattern?: boolean;
vars?: 'all' | 'local';
varsIgnorePattern?: string;
},
];
DefToVariableType¶
type DefToVariableType = | {
type: VariableType.ArrayDestructure;
def: Definition;
}
| {
type: VariableType.CatchClause;
def: Definition & { type: DefinitionType.CatchClause };
}
| {
type: VariableType.ClassName;
def: Definition & { type: DefinitionType.ClassName };
}
| {
type: VariableType.FunctionName;
def: Definition & { type: DefinitionType.FunctionName };
}
| {
type: VariableType.ImplicitGlobalVariable;
def: Definition & { type: DefinitionType.ImplicitGlobalVariable };
}
| {
type: VariableType.ImportBinding;
def: Definition & { type: DefinitionType.ImportBinding };
}
| {
type: VariableType.Parameter;
def: Definition & { type: DefinitionType.Parameter };
}
| {
type: VariableType.TSEnumMember;
def: Definition & { type: DefinitionType.TSEnumMember };
}
| {
type: VariableType.TSEnumName;
def: Definition & { type: DefinitionType.TSEnumName };
}
| {
type: VariableType.TSModuleName;
def: Definition & { type: DefinitionType.TSModuleName };
}
| {
type: VariableType.Type;
def: Definition & { type: DefinitionType.Type };
}
| {
type: VariableType.Variable;
def: Definition & { type: DefinitionType.Variable };
};
ModuleDeclarationWithBody¶
DeclarationSelectorNode¶
type DeclarationSelectorNode = | TSESTree.ClassDeclaration
| TSESTree.FunctionDeclaration
| TSESTree.TSDeclareFunction
| TSESTree.TSEnumDeclaration
| TSESTree.TSInterfaceDeclaration
| TSESTree.TSModuleDeclaration
| TSESTree.TSTypeAliasDeclaration
| TSESTree.VariableDeclaration;
Enums¶
enum VariableType¶
Enum Code
Members¶
| Name | Value | Description |
|---|---|---|
ArrayDestructure |
auto | not shown |
CatchClause |
auto | not shown |
ClassName |
auto | not shown |
FunctionName |
auto | not shown |
ImportBinding |
auto | not shown |
ImplicitGlobalVariable |
auto | not shown |
Parameter |
auto | not shown |
TSEnumMember |
auto | not shown |
TSEnumName |
auto | not shown |
TSModuleName |
auto | not shown |
Type |
auto | not shown |
Variable |
auto | not shown |
Generated by Syntax Scribe