1036 lines
50 KiB
Plaintext
1036 lines
50 KiB
Plaintext
"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 referenceContainsTypePredicate_1 = require("../util/referenceContainsTypePredicate");
|
|
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 `...` destructuring.',
|
|
},
|
|
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) ||
|
|
(0, referenceContainsTypePredicate_1.referenceContainsTypePredicate)(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';
|
|
}
|
|
}
|
|
type T = Test.Foo; // Error: Namespace 'Test' has no exported member 'Foo'.
|
|
|
|
|
|
namespace Test {
|
|
export interface Foo {
|
|
a: string;
|
|
}
|
|
namespace Foo { // Foo should be unused here
|
|
export type T = 'b';
|
|
}
|
|
}
|
|
type T = Test.Foo.T; // Error: Namespace 'Test' has no exported member 'Foo'.
|
|
|
|
---
|
|
|
|
These cases are mishandled because the base rule assumes that each variable has one def, but type-value shadowing
|
|
creates a variable with two defs
|
|
|
|
--- type-only or value-only references to type/value shadowed variables ---
|
|
--- NOTE - TS gets these cases wrong
|
|
|
|
type T = 1;
|
|
const T = 2; // this T should be unused
|
|
|
|
type U = T; // this U should be unused
|
|
const U = 3;
|
|
|
|
const _V = U;
|
|
|
|
|
|
--- partially exported type/value shadowed variables ---
|
|
--- NOTE - TS gets these cases wrong
|
|
|
|
export interface Foo {}
|
|
const Foo = 1; // this Foo should be unused
|
|
|
|
interface Bar {} // this Bar should be unused
|
|
export const Bar = 1;
|
|
|
|
*/
|