UNPKG

tslint-etc

Version:
379 lines (378 loc) 15.3 kB
"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)); }