UNPKG

alm

Version:

The best IDE for TypeScript

175 lines (174 loc) 7.36 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var types = require("../../../../common/types"); var utils_1 = require("../../../../common/utils"); /** * Removes unused imports (both import/require and ES6) */ exports.removeUnusedImports = function (filePath, service) { /** * Plan: * - First finds all the imports in the file * - Then checks if they have any usages (using document highlighting). * - For unused ones it removes them * - If all the ones from a ES6 Named import are unused the whole import should be removed */ var sourceFile = service.getProgram().getSourceFile(filePath); var imports = getImports(sourceFile); var unUsedImports = imports.filter(function (imp) { return !isIdentifierUsed(imp.identifier, sourceFile, service); }); // unUsedImports.forEach(ui => console.log(ui.identifier.text)) // DEBUG /** * Remove the non es6Named imports */ var refactorings = unUsedImports .filter(function (ui) { return ui.type !== 'es6NamedImport'; }) .map(function (ui) { var refactoring = { filePath: filePath, span: ui.toRemove, newText: '' }; return refactoring; }); /** * ES6 named imports */ /** Since imports are all at the root level. It is safe to assume no duplications */ var identifiersMarkedForRemoval = utils_1.createMap(unUsedImports.map(function (ui) { return ui.identifier.text; })); var wholeSectionRemovedMap = Object.create(null); unUsedImports .forEach(function (ui) { /** * Not using `Array.prototype.filter` as it doesn't work with TypeScirpt's discriminated unions * Hence this ugly `if` */ if (ui.type === 'es6NamedImport') { var siblings = ui.siblings, wholeToRemove = ui.wholeToRemove; var start_length = wholeToRemove.start + "_" + wholeToRemove.length; if (wholeSectionRemovedMap[start_length]) { // Already marked for removal. Move on return; } if (!siblings.some(function (s) { return !identifiersMarkedForRemoval[s.text]; })) { // remove all. var refactoring = { filePath: filePath, span: wholeToRemove, newText: '' }; refactorings.push(refactoring); /** Mark as analyzed */ wholeSectionRemovedMap[start_length] = true; } else { var refactoring = { filePath: filePath, span: ui.toRemove, newText: '' }; refactorings.push(refactoring); } } }); return types.getRefactoringsByFilePath(refactorings); }; function getImports(searchNode) { var results = []; ts.forEachChild(searchNode, function (node) { // Vist top-level import nodes if (node.kind === ts.SyntaxKind.ImportDeclaration) { var importDeclaration = node; var importClause = importDeclaration.importClause; var namedBindings = importClause.namedBindings; /** Is it a named import */ if (namedBindings.kind === ts.SyntaxKind.NamedImports) { var namedImports = namedBindings; var importSpecifiers_1 = namedImports.elements; /** * Also store the information about whole for potential use * if all siblings end up needing removal */ var siblings_1 = importSpecifiers_1.map(function (importSpecifier) { return importSpecifier.name; }); var wholeToRemove_1 = { start: importDeclaration.getFullStart(), length: importDeclaration.getFullWidth() }; importSpecifiers_1.forEach(function (importSpecifier, i) { var result = { type: 'es6NamedImport', /** * If "foo" then foo is name * If "foo as bar" the foo is name and bar is `propertyName` * The whole thing is `importSpecifier` * */ identifier: importSpecifier.name, toRemove: { start: importSpecifier.getFullStart(), length: importSpecifier.getFullWidth() }, siblings: siblings_1, wholeToRemove: wholeToRemove_1, }; /** Also we need to get the trailing coma if any */ if (i !== (importSpecifiers_1.length - 1)) { var next = importSpecifiers_1[i + 1]; var toRemove = result.toRemove; toRemove.length = next.getFullStart() - toRemove.start; } results.push(result); }); } else if (namedBindings.kind === ts.SyntaxKind.NamespaceImport) { var namespaceImport = namedBindings; results.push({ type: 'es6NamespaceImport', identifier: namespaceImport.name, toRemove: { start: importDeclaration.getFullStart(), length: importDeclaration.getFullWidth() }, }); } else { console.error('ERRRRRRRRR: found an unaccounted ES6 import type'); } } else if (node.kind === ts.SyntaxKind.ImportEqualsDeclaration) { var importEqual = node; results.push({ type: 'importEqual', identifier: importEqual.name, toRemove: { start: importEqual.getFullStart(), length: importEqual.getFullWidth() }, }); } }); return results; } function isIdentifierUsed(identifier, sourceFile, service) { var highlights = service.getOccurrencesAtPosition(sourceFile.fileName, identifier.getStart()) || []; // console.log({highlights: highlights.length, text: identifier.text}); // DEBUG /** * Filter out usages in imports * don't count usages that are in other imports * E.g. `import {foo}` & `import {foo as bar}` * Also makes it easy to get *only* true usages (not even a single import) count ;) */ var nodes = highlights.map(function (h) { return ts.getTokenAtPosition(sourceFile, h.textSpan.start, true); }); var trueUsages = nodes.filter(function (n) { return !isNodeInAnImport(n); }); // console.log({trueUsages: trueUsages.length, text: identifier.text}); // DEBUG return !!trueUsages.length; } function isNodeInAnImport(node) { while (node.parent) { if (node.kind === ts.SyntaxKind.ImportDeclaration || node.kind === ts.SyntaxKind.ImportEqualsDeclaration) { return true; } node = node.parent; } return false; }