1
1
/* @internal */
2
2
namespace ts . OrganizeImports {
3
+
4
+ /**
5
+ * Organize imports by:
6
+ * 1) Removing unused imports
7
+ * 2) Coalescing imports from the same module
8
+ * 3) Sorting imports
9
+ */
3
10
export function organizeImports (
4
11
sourceFile : SourceFile ,
5
12
formatContext : formatting . FormatContext ,
6
- host : LanguageServiceHost ) {
13
+ host : LanguageServiceHost ,
14
+ program : Program ) {
7
15
8
16
// TODO (https://github.com/Microsoft/TypeScript/issues/10020): sort *within* ambient modules (find using isAmbientModule)
9
17
@@ -21,7 +29,7 @@ namespace ts.OrganizeImports {
21
29
22
30
const newImportDecls = flatMap ( sortedImportGroups , importGroup =>
23
31
getExternalModuleName ( importGroup [ 0 ] . moduleSpecifier )
24
- ? coalesceImports ( removeUnusedImports ( importGroup ) )
32
+ ? coalesceImports ( removeUnusedImports ( importGroup , sourceFile , program ) )
25
33
: importGroup ) ;
26
34
27
35
const changeTracker = textChanges . ChangeTracker . fromContext ( { host, formatContext } ) ;
@@ -47,8 +55,73 @@ namespace ts.OrganizeImports {
47
55
return changeTracker . getChanges ( ) ;
48
56
}
49
57
50
- function removeUnusedImports ( oldImports : ReadonlyArray < ImportDeclaration > ) {
51
- return oldImports ; // TODO (https://github.com/Microsoft/TypeScript/issues/10020)
58
+ function removeUnusedImports ( oldImports : ReadonlyArray < ImportDeclaration > , sourceFile : SourceFile , program : Program ) {
59
+ const typeChecker = program . getTypeChecker ( ) ;
60
+ const jsxNamespace = typeChecker . getJsxNamespace ( ) ;
61
+ const jsxContext = sourceFile . languageVariant === LanguageVariant . JSX && program . getCompilerOptions ( ) . jsx ;
62
+
63
+ const usedImports : ImportDeclaration [ ] = [ ] ;
64
+
65
+ for ( const importDecl of oldImports ) {
66
+ const { importClause} = importDecl ;
67
+
68
+ if ( ! importClause ) {
69
+ // Imports without import clauses are assumed to be included for their side effects and are not removed.
70
+ usedImports . push ( importDecl ) ;
71
+ continue ;
72
+ }
73
+
74
+ let { name, namedBindings } = importClause ;
75
+
76
+ // Default import
77
+ if ( name && ! isDeclarationUsed ( name ) ) {
78
+ name = undefined ;
79
+ }
80
+
81
+ if ( namedBindings ) {
82
+ if ( isNamespaceImport ( namedBindings ) ) {
83
+ // Namespace import
84
+ if ( ! isDeclarationUsed ( namedBindings . name ) ) {
85
+ namedBindings = undefined ;
86
+ }
87
+ }
88
+ else {
89
+ // List of named imports
90
+ const newElements = namedBindings . elements . filter ( e => isDeclarationUsed ( e . propertyName || e . name ) ) ;
91
+ if ( newElements . length < namedBindings . elements . length ) {
92
+ namedBindings = newElements . length
93
+ ? updateNamedImports ( namedBindings , newElements )
94
+ : undefined ;
95
+ }
96
+ }
97
+ }
98
+
99
+ if ( name || namedBindings ) {
100
+ usedImports . push ( updateImportDeclarationAndClause ( importDecl , name , namedBindings ) ) ;
101
+ }
102
+ }
103
+
104
+ return usedImports ;
105
+
106
+ function isDeclarationUsed ( identifier : Identifier ) {
107
+ const symbol = typeChecker . getSymbolAtLocation ( identifier ) ;
108
+
109
+ // Be lenient with invalid code.
110
+ if ( symbol === undefined ) {
111
+ return true ;
112
+ }
113
+
114
+ // The JSX factory symbol is always used.
115
+ if ( jsxContext && symbol . name === jsxNamespace ) {
116
+ return true ;
117
+ }
118
+
119
+ const entries = FindAllReferences . getReferenceEntriesForNode ( identifier . pos , identifier , program , [ sourceFile ] , {
120
+ isCancellationRequested : ( ) => false ,
121
+ throwIfCancellationRequested : ( ) => { /*noop*/ } ,
122
+ } ) . filter ( e => e . type === "node" && e . node . getSourceFile ( ) === sourceFile ) ;
123
+ return entries . length > 1 ;
124
+ }
52
125
}
53
126
54
127
function getExternalModuleName ( specifier : Expression ) {
@@ -66,7 +139,7 @@ namespace ts.OrganizeImports {
66
139
return importGroup ;
67
140
}
68
141
69
- const { importWithoutClause, defaultImports, namespaceImports, namedImports } = getImportParts ( importGroup ) ;
142
+ const { importWithoutClause, defaultImports, namespaceImports, namedImports } = getCategorizedImports ( importGroup ) ;
70
143
71
144
const coalescedImports : ImportDeclaration [ ] = [ ] ;
72
145
@@ -78,19 +151,20 @@ namespace ts.OrganizeImports {
78
151
// produce two import declarations in this special case.
79
152
if ( defaultImports . length === 1 && namespaceImports . length === 1 && namedImports . length === 0 ) {
80
153
// Add the namespace import to the existing default ImportDeclaration.
81
- const defaultImportClause = defaultImports [ 0 ] . parent as ImportClause ;
154
+ const defaultImport = defaultImports [ 0 ] ;
82
155
coalescedImports . push (
83
- updateImportDeclarationAndClause ( defaultImportClause , defaultImportClause . name , namespaceImports [ 0 ] ) ) ;
156
+ updateImportDeclarationAndClause ( defaultImport , defaultImport . importClause . name , namespaceImports [ 0 ] . importClause . namedBindings ) ) ;
84
157
85
158
return coalescedImports ;
86
159
}
87
160
88
- const sortedNamespaceImports = stableSort ( namespaceImports , ( n1 , n2 ) => compareIdentifiers ( n1 . name , n2 . name ) ) ;
161
+ const sortedNamespaceImports = stableSort ( namespaceImports , ( i1 , i2 ) =>
162
+ compareIdentifiers ( ( i1 . importClause . namedBindings as NamespaceImport ) . name , ( i2 . importClause . namedBindings as NamespaceImport ) . name ) ) ;
89
163
90
164
for ( const namespaceImport of sortedNamespaceImports ) {
91
165
// Drop the name, if any
92
166
coalescedImports . push (
93
- updateImportDeclarationAndClause ( namespaceImport . parent , /*name*/ undefined , namespaceImport ) ) ;
167
+ updateImportDeclarationAndClause ( namespaceImport , /*name*/ undefined , namespaceImport . importClause . namedBindings ) ) ;
94
168
}
95
169
96
170
if ( defaultImports . length === 0 && namedImports . length === 0 ) {
@@ -100,41 +174,48 @@ namespace ts.OrganizeImports {
100
174
let newDefaultImport : Identifier | undefined ;
101
175
const newImportSpecifiers : ImportSpecifier [ ] = [ ] ;
102
176
if ( defaultImports . length === 1 ) {
103
- newDefaultImport = defaultImports [ 0 ] ;
177
+ newDefaultImport = defaultImports [ 0 ] . importClause . name ;
104
178
}
105
179
else {
106
180
for ( const defaultImport of defaultImports ) {
107
181
newImportSpecifiers . push (
108
- createImportSpecifier ( createIdentifier ( "default" ) , defaultImport ) ) ;
182
+ createImportSpecifier ( createIdentifier ( "default" ) , defaultImport . importClause . name ) ) ;
109
183
}
110
184
}
111
185
112
- newImportSpecifiers . push ( ...flatMap ( namedImports , n => n . elements ) ) ;
186
+ newImportSpecifiers . push ( ...flatMap ( namedImports , i => ( i . importClause . namedBindings as NamedImports ) . elements ) ) ;
113
187
114
188
const sortedImportSpecifiers = stableSort ( newImportSpecifiers , ( s1 , s2 ) =>
115
189
compareIdentifiers ( s1 . propertyName || s1 . name , s2 . propertyName || s2 . name ) ||
116
190
compareIdentifiers ( s1 . name , s2 . name ) ) ;
117
191
118
- const importClause = defaultImports . length > 0
119
- ? defaultImports [ 0 ] . parent as ImportClause
120
- : namedImports [ 0 ] . parent ;
192
+ const importDecl = defaultImports . length > 0
193
+ ? defaultImports [ 0 ]
194
+ : namedImports [ 0 ] ;
121
195
122
196
const newNamedImports = sortedImportSpecifiers . length === 0
123
197
? undefined
124
198
: namedImports . length === 0
125
199
? createNamedImports ( sortedImportSpecifiers )
126
- : updateNamedImports ( namedImports [ 0 ] , sortedImportSpecifiers ) ;
200
+ : updateNamedImports ( namedImports [ 0 ] . importClause . namedBindings as NamedImports , sortedImportSpecifiers ) ;
127
201
128
202
coalescedImports . push (
129
- updateImportDeclarationAndClause ( importClause , newDefaultImport , newNamedImports ) ) ;
203
+ updateImportDeclarationAndClause ( importDecl , newDefaultImport , newNamedImports ) ) ;
130
204
131
205
return coalescedImports ;
132
206
133
- function getImportParts ( importGroup : ReadonlyArray < ImportDeclaration > ) {
207
+ /*
208
+ * Returns entire import declarations because they may already have been rewritten and
209
+ * may lack parent pointers. The desired parts can easily be recovered based on the
210
+ * categorization.
211
+ *
212
+ * NB: There may be overlap between `defaultImports` and `namespaceImports`/`namedImports`.
213
+ */
214
+ function getCategorizedImports ( importGroup : ReadonlyArray < ImportDeclaration > ) {
134
215
let importWithoutClause : ImportDeclaration | undefined ;
135
- const defaultImports : Identifier [ ] = [ ] ;
136
- const namespaceImports : NamespaceImport [ ] = [ ] ;
137
- const namedImports : NamedImports [ ] = [ ] ;
216
+ const defaultImports : ImportDeclaration [ ] = [ ] ;
217
+ const namespaceImports : ImportDeclaration [ ] = [ ] ;
218
+ const namedImports : ImportDeclaration [ ] = [ ] ;
138
219
139
220
for ( const importDeclaration of importGroup ) {
140
221
if ( importDeclaration . importClause === undefined ) {
@@ -147,15 +228,15 @@ namespace ts.OrganizeImports {
147
228
const { name, namedBindings } = importDeclaration . importClause ;
148
229
149
230
if ( name ) {
150
- defaultImports . push ( name ) ;
231
+ defaultImports . push ( importDeclaration ) ;
151
232
}
152
233
153
234
if ( namedBindings ) {
154
235
if ( isNamespaceImport ( namedBindings ) ) {
155
- namespaceImports . push ( namedBindings ) ;
236
+ namespaceImports . push ( importDeclaration ) ;
156
237
}
157
238
else {
158
- namedImports . push ( namedBindings ) ;
239
+ namedImports . push ( importDeclaration ) ;
159
240
}
160
241
}
161
242
}
@@ -171,20 +252,19 @@ namespace ts.OrganizeImports {
171
252
function compareIdentifiers ( s1 : Identifier , s2 : Identifier ) {
172
253
return compareStringsCaseSensitive ( s1 . text , s2 . text ) ;
173
254
}
255
+ }
174
256
175
- function updateImportDeclarationAndClause (
176
- importClause : ImportClause ,
177
- name : Identifier | undefined ,
178
- namedBindings : NamedImportBindings | undefined ) {
179
-
180
- const importDeclaration = importClause . parent ;
181
- return updateImportDeclaration (
182
- importDeclaration ,
183
- importDeclaration . decorators ,
184
- importDeclaration . modifiers ,
185
- updateImportClause ( importClause , name , namedBindings ) ,
186
- importDeclaration . moduleSpecifier ) ;
187
- }
257
+ function updateImportDeclarationAndClause (
258
+ importDeclaration : ImportDeclaration ,
259
+ name : Identifier | undefined ,
260
+ namedBindings : NamedImportBindings | undefined ) {
261
+
262
+ return updateImportDeclaration (
263
+ importDeclaration ,
264
+ importDeclaration . decorators ,
265
+ importDeclaration . modifiers ,
266
+ updateImportClause ( importDeclaration . importClause , name , namedBindings ) ,
267
+ importDeclaration . moduleSpecifier ) ;
188
268
}
189
269
190
270
/* internal */ // Exported for testing
0 commit comments