1- // @ignoreDep typescript
21import * as ts from 'typescript' ;
3-
4- import { collectDeepNodes } from './ast_helpers' ;
52import { 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