blob: fbb7b031b82284d7455cf10daa564db43cc80f9a [file] [log] [blame]
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
const experimental_utils_1 = require("@typescript-eslint/experimental-utils");
const scope_manager_1 = require("@typescript-eslint/scope-manager");
const no_unused_vars_1 = __importDefault(require("eslint/lib/rules/no-unused-vars"));
const util = __importStar(require("../util"));
exports.default = util.createRule({
name: 'no-unused-vars',
meta: {
type: 'problem',
docs: {
description: 'Disallow unused variables',
category: 'Variables',
recommended: 'warn',
extendsBaseRule: true,
},
schema: no_unused_vars_1.default.meta.schema,
messages: (_a = no_unused_vars_1.default.meta.messages) !== null && _a !== void 0 ? _a : {
unusedVar: "'{{varName}}' is {{action}} but never used{{additional}}.",
},
},
defaultOptions: [{}],
create(context) {
const rules = no_unused_vars_1.default.create(context);
const filename = context.getFilename();
const MODULE_DECL_CACHE = new Map();
/**
* Gets a list of TS module definitions for a specified variable.
* @param variable eslint-scope variable object.
*/
function getModuleNameDeclarations(variable) {
const moduleDeclarations = [];
variable.defs.forEach(def => {
if (def.type === 'TSModuleName') {
moduleDeclarations.push(def.node);
}
});
return moduleDeclarations;
}
/**
* Determine if an identifier is referencing an enclosing name.
* This only applies to declarations that create their own scope (modules, functions, classes)
* @param ref The reference to check.
* @param nodes The candidate function nodes.
* @returns True if it's a self-reference, false if not.
*/
function isBlockSelfReference(ref, nodes) {
let scope = ref.from;
while (scope) {
if (nodes.indexOf(scope.block) >= 0) {
return true;
}
scope = scope.upper;
}
return false;
}
function isExported(variable, target) {
// TS will require that all merged namespaces/interfaces are exported, so we only need to find one
return variable.defs.some(def => {
var _a, _b;
return def.node.type === target &&
(((_a = def.node.parent) === null || _a === void 0 ? void 0 : _a.type) === experimental_utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
((_b = def.node.parent) === null || _b === void 0 ? void 0 : _b.type) === experimental_utils_1.AST_NODE_TYPES.ExportDefaultDeclaration);
});
}
return Object.assign(Object.assign({}, rules), { 'TSCallSignatureDeclaration, TSConstructorType, TSConstructSignatureDeclaration, TSDeclareFunction, TSEmptyBodyFunctionExpression, TSFunctionType, TSMethodSignature'(node) {
// function type signature params create variables because they can be referenced within the signature,
// but they obviously aren't unused variables for the purposes of this rule.
for (const param of node.params) {
visitPattern(param, name => {
context.markVariableAsUsed(name.name);
});
}
},
TSEnumDeclaration() {
// enum members create variables because they can be referenced within the enum,
// but they obviously aren't unused variables for the purposes of this rule.
const scope = context.getScope();
for (const variable of scope.variables) {
context.markVariableAsUsed(variable.name);
}
},
TSMappedType(node) {
// mapped types create a variable for their type name, but it's not necessary to reference it,
// so we shouldn't consider it as unused for the purpose of this rule.
context.markVariableAsUsed(node.typeParameter.name.name);
},
TSModuleDeclaration() {
const childScope = context.getScope();
const scope = util.nullThrows(context.getScope().upper, util.NullThrowsReasons.MissingToken(childScope.type, 'upper scope'));
for (const variable of scope.variables) {
const moduleNodes = getModuleNameDeclarations(variable);
if (moduleNodes.length === 0 ||
// ignore unreferenced module definitions, as the base rule will report on them
variable.references.length === 0 ||
// ignore exported nodes
isExported(variable, experimental_utils_1.AST_NODE_TYPES.TSModuleDeclaration)) {
continue;
}
// check if the only reference to a module's name is a self-reference in its body
// this won't be caught by the base rule because it doesn't understand TS modules
const isOnlySelfReferenced = variable.references.every(ref => {
return isBlockSelfReference(ref, moduleNodes);
});
if (isOnlySelfReferenced) {
context.report({
node: variable.identifiers[0],
messageId: 'unusedVar',
data: {
varName: variable.name,
action: 'defined',
additional: '',
},
});
}
}
},
[[
'TSParameterProperty > AssignmentPattern > Identifier.left',
'TSParameterProperty > Identifier.parameter',
].join(', ')](node) {
// just assume parameter properties are used as property usage tracking is beyond the scope of this rule
context.markVariableAsUsed(node.name);
},
':matches(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression) > Identifier[name="this"].params'(node) {
// this parameters should always be considered used as they're pseudo-parameters
context.markVariableAsUsed(node.name);
},
'TSInterfaceDeclaration, TSTypeAliasDeclaration'(node) {
const variable = context.getScope().set.get(node.id.name);
if (!variable) {
return;
}
if (variable.references.length === 0 ||
// ignore exported nodes
isExported(variable, node.type)) {
return;
}
// check if the type is only self-referenced
// this won't be caught by the base rule because it doesn't understand self-referencing types
const isOnlySelfReferenced = variable.references.every(ref => {
if (ref.identifier.range[0] >= node.range[0] &&
ref.identifier.range[1] <= node.range[1]) {
return true;
}
return false;
});
if (isOnlySelfReferenced) {
context.report({
node: variable.identifiers[0],
messageId: 'unusedVar',
data: {
varName: variable.name,
action: 'defined',
additional: '',
},
});
}
},
// declaration file handling
[ambientDeclarationSelector(experimental_utils_1.AST_NODE_TYPES.Program, true)](node) {
if (!util.isDefinitionFile(filename)) {
return;
}
markDeclarationChildAsUsed(node);
},
// global augmentation can be in any file, and they do not need exports
'TSModuleDeclaration[declare = true][global = true]'() {
context.markVariableAsUsed('global');
},
// children of a namespace that is a child of a declared namespace are auto-exported
[ambientDeclarationSelector('TSModuleDeclaration[declare = true] > TSModuleBlock TSModuleDeclaration > TSModuleBlock', false)](node) {
markDeclarationChildAsUsed(node);
},
// declared namespace handling
[ambientDeclarationSelector('TSModuleDeclaration[declare = true] > TSModuleBlock', false)](node) {
var _a;
const moduleDecl = util.nullThrows((_a = node.parent) === null || _a === void 0 ? void 0 : _a.parent, util.NullThrowsReasons.MissingParent);
// declared ambient modules with an `export =` statement will only export that one thing
// all other statements are not automatically exported in this case
if (moduleDecl.id.type === experimental_utils_1.AST_NODE_TYPES.Literal &&
checkModuleDeclForExportEquals(moduleDecl)) {
return;
}
markDeclarationChildAsUsed(node);
} });
function checkModuleDeclForExportEquals(node) {
var _a, _b;
const cached = MODULE_DECL_CACHE.get(node);
if (cached != null) {
return cached;
}
for (const statement of (_b = (_a = node.body) === null || _a === void 0 ? void 0 : _a.body) !== null && _b !== void 0 ? _b : []) {
if (statement.type === experimental_utils_1.AST_NODE_TYPES.TSExportAssignment) {
MODULE_DECL_CACHE.set(node, true);
return true;
}
}
MODULE_DECL_CACHE.set(node, false);
return false;
}
function ambientDeclarationSelector(parent, childDeclare) {
return [
// Types are ambiently exported
`${parent} > :matches(${[
experimental_utils_1.AST_NODE_TYPES.TSInterfaceDeclaration,
experimental_utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration,
].join(', ')})`,
// Value things are ambiently exported if they are "declare"d
`${parent} > :matches(${[
experimental_utils_1.AST_NODE_TYPES.ClassDeclaration,
experimental_utils_1.AST_NODE_TYPES.TSDeclareFunction,
experimental_utils_1.AST_NODE_TYPES.TSEnumDeclaration,
experimental_utils_1.AST_NODE_TYPES.TSModuleDeclaration,
experimental_utils_1.AST_NODE_TYPES.VariableDeclaration,
].join(', ')})${childDeclare ? '[declare = true]' : ''}`,
].join(', ');
}
function markDeclarationChildAsUsed(node) {
var _a;
const identifiers = [];
switch (node.type) {
case experimental_utils_1.AST_NODE_TYPES.TSInterfaceDeclaration:
case experimental_utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration:
case experimental_utils_1.AST_NODE_TYPES.ClassDeclaration:
case experimental_utils_1.AST_NODE_TYPES.FunctionDeclaration:
case experimental_utils_1.AST_NODE_TYPES.TSDeclareFunction:
case experimental_utils_1.AST_NODE_TYPES.TSEnumDeclaration:
case experimental_utils_1.AST_NODE_TYPES.TSModuleDeclaration:
if (((_a = node.id) === null || _a === void 0 ? void 0 : _a.type) === experimental_utils_1.AST_NODE_TYPES.Identifier) {
identifiers.push(node.id);
}
break;
case experimental_utils_1.AST_NODE_TYPES.VariableDeclaration:
for (const declaration of node.declarations) {
visitPattern(declaration, pattern => {
identifiers.push(pattern);
});
}
break;
}
const scope = context.getScope();
const { variableScope } = scope;
if (variableScope !== scope) {
for (const id of identifiers) {
const superVar = variableScope.set.get(id.name);
if (superVar) {
superVar.eslintUsed = true;
}
}
}
else {
for (const id of identifiers) {
context.markVariableAsUsed(id.name);
}
}
}
function visitPattern(node, cb) {
const visitor = new scope_manager_1.PatternVisitor({}, node, cb);
visitor.visit(node);
}
},
});
/*
###### 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
) {}
*/
/*
###### TODO ######
We currently extend base `no-unused-vars` implementation because it's easier and lighter-weight.
Because of this, there are a few false-negatives which won't get caught.
We could fix these if we fork the base rule; but that's a lot of code (~650 lines) to add in.
I didn't want to do that just yet without some real-world issues, considering these are pretty rare edge-cases.
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;
*/
//# sourceMappingURL=no-unused-vars.js.map