Skip to content

Commit 4848c84

Browse files
clydinfilipesilva
authored andcommitted
feat(@ngtools/webpack): support default/namespace import eliding
1 parent 18da707 commit 4848c84

File tree

1 file changed

+65
-90
lines changed

1 file changed

+65
-90
lines changed
+65-90
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,7 @@
1-
// @ignoreDep typescript
21
import * as ts from 'typescript';
3-
4-
import { collectDeepNodes } from './ast_helpers';
52
import { RemoveNodeOperation, TransformOperation } from './interfaces';
63

74

8-
interface RemovedSymbol {
9-
symbol: ts.Symbol;
10-
importDecl: ts.ImportDeclaration;
11-
importSpec: ts.ImportSpecifier;
12-
singleImport: boolean;
13-
removed: ts.Identifier[];
14-
all: ts.Identifier[];
15-
}
16-
175
// Remove imports for which all identifiers have been removed.
186
// Needs type checker, and works even if it's not the first transformer.
197
// Works by removing imports for symbols whose identifiers have all been removed.
@@ -31,95 +19,82 @@ export function elideImports(
3119
return [];
3220
}
3321

34-
// Get all children identifiers inside the removed nodes.
35-
const removedIdentifiers = removedNodes
36-
.map((node) => collectDeepNodes<ts.Identifier>(node, ts.SyntaxKind.Identifier))
37-
.reduce((prev, curr) => prev.concat(curr), [])
38-
// Also add the top level nodes themselves if they are identifiers.
39-
.concat(removedNodes.filter((node) =>
40-
node.kind === ts.SyntaxKind.Identifier) as ts.Identifier[]);
22+
const typeChecker = getTypeChecker();
4123

42-
if (removedIdentifiers.length === 0) {
24+
// Collect all imports and used identifiers
25+
const exportSpecifiers = new Set<string>();
26+
const usedSymbols = new Set<ts.Symbol>();
27+
const imports = new Array<ts.ImportDeclaration>();
28+
ts.forEachChild(sourceFile, function visit(node) {
29+
// Skip removed nodes
30+
if (removedNodes.includes(node)) {
31+
return;
32+
}
33+
34+
// Record import and skip
35+
if (ts.isImportDeclaration(node)) {
36+
imports.push(node);
37+
return;
38+
}
39+
40+
if (ts.isIdentifier(node)) {
41+
usedSymbols.add(typeChecker.getSymbolAtLocation(node));
42+
} else if (ts.isExportSpecifier(node)) {
43+
// Export specifiers return the non-local symbol from the above
44+
// so check the name string instead
45+
exportSpecifiers.add((node.propertyName || node.name).text);
46+
return;
47+
}
48+
49+
ts.forEachChild(node, visit);
50+
});
51+
52+
if (imports.length === 0) {
4353
return [];
4454
}
4555

46-
// Get all imports in the source file.
47-
const allImports = collectDeepNodes<ts.ImportDeclaration>(
48-
sourceFile, ts.SyntaxKind.ImportDeclaration);
56+
const isUnused = (node: ts.Identifier) => {
57+
if (exportSpecifiers.has(node.text)) {
58+
return false;
59+
}
4960

50-
if (allImports.length === 0) {
51-
return [];
52-
}
61+
const symbol = typeChecker.getSymbolAtLocation(node);
5362

54-
const removedSymbolMap: Map<string, RemovedSymbol> = new Map();
55-
const typeChecker = getTypeChecker();
63+
return symbol && !usedSymbols.has(symbol);
64+
};
65+
66+
for (const node of imports) {
67+
if (!node.importClause) {
68+
// "import 'abc';"
69+
continue;
70+
}
5671

57-
// Find all imports that use a removed identifier and add them to the map.
58-
allImports
59-
.filter((node: ts.ImportDeclaration) => {
60-
// TODO: try to support removing `import * as X from 'XYZ'`.
61-
// Filter out import statements that are either `import 'XYZ'` or `import * as X from 'XYZ'`.
62-
const clause = node.importClause as ts.ImportClause;
63-
if (!clause || clause.name || !clause.namedBindings) {
64-
return false;
72+
if (node.importClause.name) {
73+
// "import XYZ from 'abc';"
74+
if (isUnused(node.importClause.name)) {
75+
ops.push(new RemoveNodeOperation(sourceFile, node));
6576
}
66-
return clause.namedBindings.kind == ts.SyntaxKind.NamedImports;
67-
})
68-
.forEach((importDecl: ts.ImportDeclaration) => {
69-
const importClause = importDecl.importClause as ts.ImportClause;
70-
const namedImports = importClause.namedBindings as ts.NamedImports;
71-
72-
namedImports.elements.forEach((importSpec: ts.ImportSpecifier) => {
73-
const importId = importSpec.name;
74-
const symbol = typeChecker.getSymbolAtLocation(importId);
75-
76-
const removedNodesForImportId = removedIdentifiers.filter((id) =>
77-
id.text === importId.text && typeChecker.getSymbolAtLocation(id) === symbol);
78-
79-
if (removedNodesForImportId.length > 0) {
80-
removedSymbolMap.set(importId.text, {
81-
symbol,
82-
importDecl,
83-
importSpec,
84-
singleImport: namedImports.elements.length === 1,
85-
removed: removedNodesForImportId,
86-
all: []
87-
});
77+
} else if (ts.isNamespaceImport(node.importClause.namedBindings)) {
78+
// "import * as XYZ from 'abc';"
79+
if (isUnused(node.importClause.namedBindings.name)) {
80+
ops.push(new RemoveNodeOperation(sourceFile, node));
81+
}
82+
} else if (ts.isNamedImports(node.importClause.namedBindings)) {
83+
// "import { XYZ, ... } from 'abc';"
84+
const specifierOps = [];
85+
for (const specifier of node.importClause.namedBindings.elements) {
86+
if (isUnused(specifier.propertyName || specifier.name)) {
87+
specifierOps.push(new RemoveNodeOperation(sourceFile, specifier));
8888
}
89-
});
90-
});
91-
92-
93-
if (removedSymbolMap.size === 0) {
94-
return [];
95-
}
89+
}
9690

97-
// Find all identifiers in the source file that have a removed symbol, and add them to the map.
98-
collectDeepNodes<ts.Identifier>(sourceFile, ts.SyntaxKind.Identifier)
99-
.forEach((id) => {
100-
if (removedSymbolMap.has(id.text)) {
101-
const symbol = removedSymbolMap.get(id.text);
102-
103-
// Check if the symbol is the same or if it is a named export.
104-
// Named exports don't have the same symbol but will have the same name.
105-
if ((id.parent && id.parent.kind === ts.SyntaxKind.ExportSpecifier)
106-
|| typeChecker.getSymbolAtLocation(id) === symbol.symbol) {
107-
symbol.all.push(id);
108-
}
91+
if (specifierOps.length === node.importClause.namedBindings.elements.length) {
92+
ops.push(new RemoveNodeOperation(sourceFile, node));
93+
} else {
94+
ops.push(...specifierOps);
10995
}
110-
});
111-
112-
Array.from(removedSymbolMap.values())
113-
.filter((symbol) => {
114-
// If the number of removed imports plus one (the import specifier) is equal to the total
115-
// number of identifiers for that symbol, it's safe to remove the import.
116-
return symbol.removed.length + 1 === symbol.all.length;
117-
})
118-
.forEach((symbol) => {
119-
// Remove the whole declaration if it's a single import.
120-
const nodeToRemove = symbol.singleImport ? symbol.importDecl : symbol.importSpec;
121-
ops.push(new RemoveNodeOperation(sourceFile, nodeToRemove));
122-
});
96+
}
97+
}
12398

12499
return ops;
125100
}

0 commit comments

Comments
 (0)