1
+ /* @internal */
2
+ namespace ts . OrganizeImports {
3
+ export function organizeImports (
4
+ sourceFile : SourceFile ,
5
+ formatContext : formatting . FormatContext ,
6
+ host : LanguageServiceHost ) {
7
+
8
+ // TODO (https://github.com/Microsoft/TypeScript/issues/10020): sort *within* ambient modules (find using isAmbientModule)
9
+
10
+ // All of the old ImportDeclarations in the file, in syntactic order.
11
+ const oldImportDecls = sourceFile . statements . filter ( isImportDeclaration ) ;
12
+
13
+ if ( oldImportDecls . length === 0 ) {
14
+ return [ ] ;
15
+ }
16
+
17
+ const oldValidImportDecls = oldImportDecls . filter ( importDecl => getExternalModuleName ( importDecl . moduleSpecifier ) ) ;
18
+ const oldInvalidImportDecls = oldImportDecls . filter ( importDecl => ! getExternalModuleName ( importDecl . moduleSpecifier ) ) ;
19
+
20
+ // All of the new ImportDeclarations in the file, in sorted order.
21
+ const newImportDecls = coalesceImports ( sortImports ( removeUnusedImports ( oldValidImportDecls ) ) ) . concat ( oldInvalidImportDecls ) ;
22
+
23
+ const changeTracker = textChanges . ChangeTracker . fromContext ( { host, formatContext } ) ;
24
+
25
+ // Delete or replace the first import.
26
+ if ( newImportDecls . length === 0 ) {
27
+ changeTracker . deleteNode ( sourceFile , oldImportDecls [ 0 ] ) ;
28
+ }
29
+ else {
30
+ // Note: Delete the surrounding trivia because it will have been retained in newImportDecls.
31
+ changeTracker . replaceNodeWithNodes ( sourceFile , oldImportDecls [ 0 ] , newImportDecls , {
32
+ useNonAdjustedStartPosition : false ,
33
+ useNonAdjustedEndPosition : false ,
34
+ suffix : getNewLineOrDefaultFromHost ( host , formatContext . options ) ,
35
+ } ) ;
36
+ }
37
+
38
+ // Delete any subsequent imports.
39
+ for ( let i = 1 ; i < oldImportDecls . length ; i ++ ) {
40
+ changeTracker . deleteNode ( sourceFile , oldImportDecls [ i ] ) ;
41
+ }
42
+
43
+ return changeTracker . getChanges ( ) ;
44
+ }
45
+
46
+ function removeUnusedImports ( oldImports : ReadonlyArray < ImportDeclaration > ) {
47
+ return oldImports ; // TODO (https://github.com/Microsoft/TypeScript/issues/10020)
48
+ }
49
+
50
+ /* @internal */ // Internal for testing
51
+ export function sortImports ( oldImports : ReadonlyArray < ImportDeclaration > ) {
52
+ return stableSort ( oldImports , ( import1 , import2 ) => {
53
+ const name1 = getExternalModuleName ( import1 . moduleSpecifier ) ;
54
+ const name2 = getExternalModuleName ( import2 . moduleSpecifier ) ;
55
+ Debug . assert ( name1 !== undefined ) ;
56
+ Debug . assert ( name2 !== undefined ) ;
57
+ return compareBooleans ( isExternalModuleNameRelative ( name1 ) , isExternalModuleNameRelative ( name2 ) ) ||
58
+ compareStringsCaseSensitive ( name1 , name2 ) ;
59
+ } ) ;
60
+ }
61
+
62
+ function getExternalModuleName ( specifier : Expression ) {
63
+ return isStringLiteral ( specifier ) || isNoSubstitutionTemplateLiteral ( specifier )
64
+ ? specifier . text
65
+ : undefined ;
66
+ }
67
+
68
+ /**
69
+ * @param sortedImports a non-empty list of ImportDeclarations, sorted by module name.
70
+ */
71
+ function groupSortedImports ( sortedImports : ReadonlyArray < ImportDeclaration > ) : ReadonlyArray < ReadonlyArray < ImportDeclaration > > {
72
+ Debug . assert ( length ( sortedImports ) > 0 ) ;
73
+
74
+ const groups : ImportDeclaration [ ] [ ] = [ ] ;
75
+
76
+ let groupName : string | undefined = getExternalModuleName ( sortedImports [ 0 ] . moduleSpecifier ) ;
77
+ Debug . assert ( groupName !== undefined ) ;
78
+ let group : ImportDeclaration [ ] = [ ] ;
79
+
80
+ for ( const importDeclaration of sortedImports ) {
81
+ const moduleName = getExternalModuleName ( importDeclaration . moduleSpecifier ) ;
82
+ Debug . assert ( moduleName !== undefined ) ;
83
+ if ( moduleName === groupName ) {
84
+ group . push ( importDeclaration ) ;
85
+ }
86
+ else if ( group . length ) {
87
+ groups . push ( group ) ;
88
+
89
+ groupName = moduleName ;
90
+ group = [ importDeclaration ] ;
91
+ }
92
+ }
93
+
94
+ if ( group . length ) {
95
+ groups . push ( group ) ;
96
+ }
97
+
98
+ return groups ;
99
+ }
100
+
101
+ /* @internal */ // Internal for testing
102
+ /**
103
+ * @param sortedImports a list of ImportDeclarations, sorted by module name.
104
+ */
105
+ export function coalesceImports ( sortedImports : ReadonlyArray < ImportDeclaration > ) {
106
+ if ( sortedImports . length === 0 ) {
107
+ return sortedImports ;
108
+ }
109
+
110
+ const coalescedImports : ImportDeclaration [ ] = [ ] ;
111
+
112
+ const groupedImports = groupSortedImports ( sortedImports ) ;
113
+ for ( const importGroup of groupedImports ) {
114
+
115
+ const { importWithoutClause, defaultImports, namespaceImports, namedImports } = getImportParts ( importGroup ) ;
116
+
117
+ if ( importWithoutClause ) {
118
+ coalescedImports . push ( importWithoutClause ) ;
119
+ }
120
+
121
+ // Normally, we don't combine default and namespace imports, but it would be silly to
122
+ // produce two import declarations in this special case.
123
+ if ( defaultImports . length === 1 && namespaceImports . length === 1 && namedImports . length === 0 ) {
124
+ // Add the namespace import to the existing default ImportDeclaration.
125
+ const defaultImportClause = defaultImports [ 0 ] . parent as ImportClause ;
126
+ coalescedImports . push (
127
+ updateImportDeclarationAndClause ( defaultImportClause , defaultImportClause . name , namespaceImports [ 0 ] ) ) ;
128
+
129
+ continue ;
130
+ }
131
+
132
+ const sortedNamespaceImports = stableSort ( namespaceImports , ( n1 , n2 ) => compareIdentifiers ( n1 . name , n2 . name ) ) ;
133
+
134
+ for ( const namespaceImport of sortedNamespaceImports ) {
135
+ // Drop the name, if any
136
+ coalescedImports . push (
137
+ updateImportDeclarationAndClause ( namespaceImport . parent , /*name*/ undefined , namespaceImport ) ) ;
138
+ }
139
+
140
+ if ( defaultImports . length === 0 && namedImports . length === 0 ) {
141
+ continue ;
142
+ }
143
+
144
+ let newDefaultImport : Identifier | undefined ;
145
+ const newImportSpecifiers : ImportSpecifier [ ] = [ ] ;
146
+ if ( defaultImports . length === 1 ) {
147
+ newDefaultImport = defaultImports [ 0 ] ;
148
+ }
149
+ else {
150
+ for ( const defaultImport of defaultImports ) {
151
+ newImportSpecifiers . push (
152
+ createImportSpecifier ( createIdentifier ( "default" ) , defaultImport ) ) ;
153
+ }
154
+ }
155
+
156
+ newImportSpecifiers . push ( ...flatMap ( namedImports , n => n . elements ) ) ;
157
+
158
+ const sortedImportSpecifiers = stableSort ( newImportSpecifiers , ( s1 , s2 ) =>
159
+ compareIdentifiers ( s1 . propertyName || s1 . name , s2 . propertyName || s2 . name ) ||
160
+ compareIdentifiers ( s1 . name , s2 . name ) ) ;
161
+
162
+ const importClause = defaultImports . length > 0
163
+ ? defaultImports [ 0 ] . parent as ImportClause
164
+ : namedImports [ 0 ] . parent ;
165
+
166
+ const newNamedImports = sortedImportSpecifiers . length === 0
167
+ ? undefined
168
+ : namedImports . length === 0
169
+ ? createNamedImports ( sortedImportSpecifiers )
170
+ : updateNamedImports ( namedImports [ 0 ] , sortedImportSpecifiers ) ;
171
+
172
+ coalescedImports . push (
173
+ updateImportDeclarationAndClause ( importClause , newDefaultImport , newNamedImports ) ) ;
174
+ }
175
+
176
+ return coalescedImports ;
177
+
178
+ function getImportParts ( importGroup : ReadonlyArray < ImportDeclaration > ) {
179
+ let importWithoutClause : ImportDeclaration | undefined ;
180
+ const defaultImports : Identifier [ ] = [ ] ;
181
+ const namespaceImports : NamespaceImport [ ] = [ ] ;
182
+ const namedImports : NamedImports [ ] = [ ] ;
183
+
184
+ for ( const importDeclaration of importGroup ) {
185
+ if ( importDeclaration . importClause === undefined ) {
186
+ // Only the first such import is interesting - the others are redundant.
187
+ // Note: Unfortunately, we will lose trivia that was on this node.
188
+ importWithoutClause = importWithoutClause || importDeclaration ;
189
+ continue ;
190
+ }
191
+
192
+ const { name, namedBindings } = importDeclaration . importClause ;
193
+
194
+ if ( name ) {
195
+ defaultImports . push ( name ) ;
196
+ }
197
+
198
+ if ( namedBindings ) {
199
+ if ( isNamespaceImport ( namedBindings ) ) {
200
+ namespaceImports . push ( namedBindings ) ;
201
+ }
202
+ else {
203
+ namedImports . push ( namedBindings ) ;
204
+ }
205
+ }
206
+ }
207
+
208
+ return {
209
+ importWithoutClause,
210
+ defaultImports,
211
+ namespaceImports,
212
+ namedImports,
213
+ } ;
214
+ }
215
+
216
+ function compareIdentifiers ( s1 : Identifier , s2 : Identifier ) {
217
+ return compareStringsCaseSensitive ( s1 . text , s2 . text ) ;
218
+ }
219
+
220
+ function updateImportDeclarationAndClause (
221
+ importClause : ImportClause ,
222
+ name : Identifier | undefined ,
223
+ namedBindings : NamedImportBindings | undefined ) {
224
+
225
+ const importDeclaration = importClause . parent ;
226
+ return updateImportDeclaration (
227
+ importDeclaration ,
228
+ importDeclaration . decorators ,
229
+ importDeclaration . modifiers ,
230
+ updateImportClause ( importClause , name , namedBindings ) ,
231
+ importDeclaration . moduleSpecifier ) ;
232
+ }
233
+ }
234
+ }
0 commit comments