tslint-etc
Version:
More rules for TSLint
379 lines (378 loc) • 15.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Walker = exports.Rule = void 0;
const Lint = require("tslint");
const tsutils = require("tsutils");
const ts = require("typescript");
class Rule extends Lint.Rules.TypedRule {
applyWithProgram(sourceFile, program) {
return this.applyWithWalker(new Walker(sourceFile, this.getOptions(), program));
}
}
exports.Rule = Rule;
Rule.metadata = {
description: "Disallows unused declarations.",
options: {
properties: {
declarations: { type: "boolean" },
ignored: { type: "object" },
imports: { type: "boolean" },
},
type: "object",
},
optionsDescription: Lint.Utils.dedent `
An optional object with optional \`imports\`, \`declarations\` and \`ignored\` properties.
The \`imports\` and \`declarations\` properties are booleans and determine whether or not unused imports or declarations are allowed.
They default to \`true\`.
The \`ignored\` property is an object containing keys that are regular expressions
and values that are booleans - indicating whether or not matches are ignored.`,
requiresTypeInfo: true,
ruleName: "no-unused-declaration",
type: "functionality",
typescriptOnly: true,
};
Rule.FAILURE_STRING = "Unused declarations are forbidden";
class Walker extends Lint.ProgramAwareRuleWalker {
constructor(sourceFile, rawOptions, program) {
super(sourceFile, rawOptions, program);
this._associationsByIdentifier = new Map();
this._declarationsByIdentifier = new Map();
this._deletes = new Set();
this._ignored = [];
this._scopes = [new Map()];
this._withoutDeclarations = new Set();
this._usageByIdentifier = new Map();
this._validate = {
declarations: true,
imports: true,
};
const [options] = this.getOptions();
if (options) {
Object.entries(options.ignored || {}).forEach(([key, value]) => {
if (value !== false) {
this._ignored.push(new RegExp(key));
}
});
this._validate = { ...this._validate, ...options };
}
}
visitClassDeclaration(node) {
if (this._validate.declarations) {
const { name } = node;
if (!tsutils.hasModifier(node.modifiers, ts.SyntaxKind.ExportKeyword)) {
this.declared(node, name);
this.setScopedIdentifier(name);
}
}
super.visitClassDeclaration(node);
}
visitEnumDeclaration(node) {
if (this._validate.declarations) {
const { name } = node;
if (!tsutils.hasModifier(node.modifiers, ts.SyntaxKind.ExportKeyword)) {
this.declared(node, name);
this.setScopedIdentifier(name);
}
}
super.visitEnumDeclaration(node);
}
visitFunctionDeclaration(node) {
if (this._validate.declarations) {
const { body, name } = node;
if (body &&
name &&
!tsutils.hasModifier(node.modifiers, ts.SyntaxKind.ExportKeyword)) {
this.declared(node, name);
this.setScopedIdentifier(name, true);
}
}
super.visitFunctionDeclaration(node);
}
visitIdentifier(node) {
const { _usageByIdentifier, _withoutDeclarations } = this;
if (tsutils.isExportSpecifier(node.parent)) {
this.seen(node.getText());
return;
}
if (tsutils.isPropertyAssignment(node.parent) &&
node === node.parent.name) {
return;
}
const isDeclaration = _usageByIdentifier.has(node);
if (!isDeclaration &&
(!tsutils.isReassignmentTarget(node) || isUnaryPrefixOrPostfix(node))) {
let hasDeclarations = false;
const typeChecker = this.getTypeChecker();
const symbol = typeChecker.getSymbolAtLocation(node);
if (symbol) {
const declarations = symbol.getDeclarations();
if (declarations) {
declarations.forEach((declaration) => {
const identifier = getIdentifier(declaration);
this.seen(identifier);
});
hasDeclarations = true;
}
}
if (!hasDeclarations) {
_withoutDeclarations.add(node.getText());
}
}
super.visitIdentifier(node);
}
visitImportDeclaration(node) {
const { importClause } = node;
if (this._validate.imports && importClause) {
const { name } = node.importClause;
if (name) {
this.declared(node, name);
this.setScopedIdentifier(name);
}
}
super.visitImportDeclaration(node);
}
visitImportEqualsDeclaration(node) {
const { name } = node;
if (this._validate.imports && name) {
this.declared(node, name);
this.setScopedIdentifier(name);
}
super.visitImportEqualsDeclaration(node);
}
visitInterfaceDeclaration(node) {
if (this._validate.declarations) {
const { name } = node;
if (!tsutils.hasModifier(node.modifiers, ts.SyntaxKind.ExportKeyword)) {
this.declared(node, name);
this.setScopedIdentifier(name);
}
}
super.visitInterfaceDeclaration(node);
}
visitJsxSelfClosingElement(node) {
this.seenJsx();
super.visitJsxSelfClosingElement(node);
}
visitJsxElement(node) {
this.seenJsx();
super.visitJsxElement(node);
}
visitNamedImports(node) {
if (this._validate.imports) {
node.elements.forEach((element) => {
const { name, propertyName } = element;
this.declared(node, name);
if (propertyName) {
this.seen(propertyName);
}
this.setScopedIdentifier(name);
});
}
super.visitNamedImports(node);
}
visitNamespaceImport(node) {
if (this._validate.imports) {
const { name } = node;
this.declared(node, name);
this.setScopedIdentifier(name);
}
super.visitNamespaceImport(node);
}
visitNode(node) {
const isScopeBoundary = tsutils.isBlock(node) ||
tsutils.isArrowFunction(node) ||
tsutils.isConstructorDeclaration(node) ||
tsutils.isFunctionDeclaration(node) ||
tsutils.isGetAccessorDeclaration(node) ||
tsutils.isMethodDeclaration(node) ||
tsutils.isSetAccessorDeclaration(node);
const { _scopes } = this;
if (isScopeBoundary) {
_scopes.push(new Map());
}
super.visitNode(node);
if (isScopeBoundary) {
_scopes.pop();
}
if (tsutils.isSourceFile(node)) {
this.onSourceFileEnd();
}
}
visitObjectLiteralExpression(node) {
node.properties.forEach((property) => {
if (tsutils.isShorthandPropertyAssignment(property)) {
const text = property.name.getText();
const identifier = this.getScopedIdentifier(text);
if (identifier) {
this.seen(identifier);
}
else {
this._withoutDeclarations.add(text);
}
}
});
super.visitObjectLiteralExpression(node);
}
visitTypeAliasDeclaration(node) {
if (this._validate.declarations) {
const { name } = node;
if (!tsutils.hasModifier(node.modifiers, ts.SyntaxKind.ExportKeyword)) {
this.declared(node, name);
this.setScopedIdentifier(name);
}
}
super.visitTypeAliasDeclaration(node);
}
visitVariableStatement(node) {
if (this._validate.declarations) {
if (!tsutils.hasModifier(node.modifiers, ts.SyntaxKind.ExportKeyword)) {
const names = [];
tsutils.forEachDeclaredVariable(node.declarationList, (declaration) => {
const { name } = declaration;
if (tsutils.isBindingElement(declaration) &&
declaration.dotDotDotToken) {
this.associate(name, names);
}
else {
names.push(name);
}
this.declared(node, name);
this.setScopedIdentifier(name);
});
}
}
super.visitVariableStatement(node);
}
associate(name, names) {
const { _associationsByIdentifier } = this;
_associationsByIdentifier.set(name, names);
}
declared(declaration, name) {
const { _declarationsByIdentifier, _usageByIdentifier } = this;
_declarationsByIdentifier.set(name, declaration);
const usage = _usageByIdentifier.get(name);
_usageByIdentifier.set(name, usage === "seen" ? "used" : "declared");
}
getFix(identifier, declaration) {
const { _deletes } = this;
if (tsutils.isImportDeclaration(declaration) ||
tsutils.isImportEqualsDeclaration(declaration)) {
_deletes.add(declaration);
return Lint.Replacement.deleteFromTo(getStart(declaration), declaration.getFullStart() + declaration.getFullWidth());
}
else if (tsutils.isNamedImports(declaration)) {
const { _usageByIdentifier } = this;
const { elements } = declaration;
if (elements.every((element) => _usageByIdentifier.get(element.name) === "declared")) {
const importClause = declaration.parent;
const importDeclaration = importClause.parent;
if (_deletes.has(importDeclaration)) {
return undefined;
}
const { name } = importClause;
if (name && this.used(name)) {
_deletes.add(declaration);
return Lint.Replacement.deleteFromTo(name.getFullStart() + name.getFullWidth(), declaration.getFullStart() + declaration.getFullWidth());
}
_deletes.add(importDeclaration);
return Lint.Replacement.deleteFromTo(getStart(importDeclaration), importDeclaration.getFullStart() + importDeclaration.getFullWidth());
}
const index = elements.findIndex((element) => element.name === identifier);
const from = index === 0
? elements[index].getFullStart()
: elements[index - 1].getFullStart() +
elements[index - 1].getFullWidth();
const to = index === 0
? elements[index + 1].getFullStart()
: elements[index].getFullStart() + elements[index].getFullWidth();
return Lint.Replacement.deleteFromTo(from, to);
}
else if (tsutils.isNamespaceImport(declaration)) {
const importClause = declaration.parent;
const importDeclaration = importClause.parent;
_deletes.add(importDeclaration);
return Lint.Replacement.deleteFromTo(getStart(importDeclaration), importDeclaration.getFullStart() + importDeclaration.getFullWidth());
}
return undefined;
function getStart(importDeclaration) {
return importDeclaration.getFullStart() || importDeclaration.getStart();
}
}
getScopedIdentifier(name) {
const { _scopes } = this;
for (let s = _scopes.length - 1; s >= 0; --s) {
const scope = _scopes[s];
if (scope.has(name)) {
return scope.get(name);
}
}
return undefined;
}
onSourceFileEnd() {
const { _declarationsByIdentifier, _usageByIdentifier, _withoutDeclarations, } = this;
_usageByIdentifier.forEach((usage, identifier) => {
if (this._ignored.some((regExp) => regExp.test(identifier.getText()))) {
return;
}
if (usage === "declared" &&
!_withoutDeclarations.has(identifier.getText())) {
const declaration = _declarationsByIdentifier.get(identifier);
const fix = this.getFix(identifier, declaration);
this.addFailureAtNode(identifier, Rule.FAILURE_STRING, fix);
}
});
}
seen(name) {
const { _associationsByIdentifier, _usageByIdentifier } = this;
if (typeof name === "string") {
_usageByIdentifier.forEach((value, key) => {
if (key.getText() === name) {
_usageByIdentifier.set(key, value === "declared" ? "used" : "seen");
const associatedNames = _associationsByIdentifier.get(key);
if (associatedNames) {
associatedNames.forEach((associatedName) => this.seen(associatedName));
}
}
});
}
else {
const usage = _usageByIdentifier.get(name);
_usageByIdentifier.set(name, usage === "declared" ? "used" : "seen");
const associatedNames = _associationsByIdentifier.get(name);
if (associatedNames) {
associatedNames.forEach((associatedName) => this.seen(associatedName));
}
}
}
seenJsx() {
const jsxFactory = this.getProgram().getCompilerOptions().jsxFactory ||
"React.createElement";
const index = jsxFactory.indexOf(".");
this.seen(index === -1 ? jsxFactory : jsxFactory.substring(0, index));
}
setScopedIdentifier(identifier, parent = false) {
const { _scopes } = this;
const scope = _scopes[_scopes.length - (parent ? 2 : 1)];
scope.set(identifier.getText(), identifier);
}
used(name) {
const { _usageByIdentifier } = this;
const text = typeof name === "string" ? name : name.getText();
let used = false;
_usageByIdentifier.forEach((usage, identifier) => {
if (usage === "used" && identifier.getText() === text) {
used = true;
}
});
return used;
}
}
exports.Walker = Walker;
function getIdentifier(node) {
return tsutils.isIdentifier(node) ? node : node["name"];
}
function isUnaryPrefixOrPostfix(node) {
const { parent } = node;
return (tsutils.isPrefixUnaryExpression(parent) ||
tsutils.isPostfixUnaryExpression(parent));
}