1
- // @ignoreDep typescript
2
1
import * as ts from 'typescript' ;
3
-
4
- import { collectDeepNodes } from './ast_helpers' ;
5
2
import { RemoveNodeOperation , TransformOperation } from './interfaces' ;
6
3
7
4
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
-
17
5
// Remove imports for which all identifiers have been removed.
18
6
// Needs type checker, and works even if it's not the first transformer.
19
7
// Works by removing imports for symbols whose identifiers have all been removed.
@@ -31,95 +19,82 @@ export function elideImports(
31
19
return [ ] ;
32
20
}
33
21
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 ( ) ;
41
23
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 ) {
43
53
return [ ] ;
44
54
}
45
55
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
+ }
49
60
50
- if ( allImports . length === 0 ) {
51
- return [ ] ;
52
- }
61
+ const symbol = typeChecker . getSymbolAtLocation ( node ) ;
53
62
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
+ }
56
71
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 ) ) ;
65
76
}
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 ) ) ;
88
88
}
89
- } ) ;
90
- } ) ;
91
-
92
-
93
- if ( removedSymbolMap . size === 0 ) {
94
- return [ ] ;
95
- }
89
+ }
96
90
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 ) ;
109
95
}
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
+ }
123
98
124
99
return ops ;
125
100
}
0 commit comments