alm
Version:
The best IDE for TypeScript
175 lines (174 loc) • 7.36 kB
JavaScript
;
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;
}