@@ -27,11 +27,198 @@ function checkImports(imported, context) {
27
27
message,
28
28
} ) ;
29
29
}
30
+
30
31
}
31
32
}
32
33
}
33
34
34
- function getFix ( first , rest , sourceCode , context ) {
35
+ function checkTypeImports ( imported , context ) {
36
+ for ( const [ module , nodes ] of imported . entries ( ) ) {
37
+ const typeImports = nodes . filter ( ( node ) => node . importKind === 'type' ) ;
38
+ if ( nodes . length > 1 ) {
39
+ const someInlineTypeImports = nodes . filter ( ( node ) => node . specifiers . some ( ( spec ) => spec . importKind === 'type' ) ) ;
40
+ if ( typeImports . length > 0 && someInlineTypeImports . length > 0 ) {
41
+ const message = `'${ module } ' imported multiple times.` ;
42
+ const sourceCode = context . getSourceCode ( ) ;
43
+ const fix = getTypeFix ( nodes , sourceCode , context ) ;
44
+
45
+ const [ first , ...rest ] = nodes ;
46
+ context . report ( {
47
+ node : first . source ,
48
+ message,
49
+ fix, // Attach the autofix (if any) to the first import.
50
+ } ) ;
51
+
52
+ for ( const node of rest ) {
53
+ context . report ( {
54
+ node : node . source ,
55
+ message,
56
+ } ) ;
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ function checkInlineTypeImports ( imported , context ) {
64
+ for ( const [ module , nodes ] of imported . entries ( ) ) {
65
+ if ( nodes . length > 1 ) {
66
+ const message = `'${ module } ' imported multiple times.` ;
67
+ const sourceCode = context . getSourceCode ( ) ;
68
+ const fix = getInlineTypeFix ( nodes , sourceCode ) ;
69
+
70
+ const [ first , ...rest ] = nodes ;
71
+ context . report ( {
72
+ node : first . source ,
73
+ message,
74
+ fix, // Attach the autofix (if any) to the first import.
75
+ } ) ;
76
+
77
+ for ( const node of rest ) {
78
+ context . report ( {
79
+ node : node . source ,
80
+ message,
81
+ } ) ;
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ function isComma ( token ) {
88
+ return token . type === 'Punctuator' && token . value === ',' ;
89
+ }
90
+
91
+ function getInlineTypeFix ( nodes , sourceCode ) {
92
+ return fixer => {
93
+ const fixes = [ ] ;
94
+
95
+ // if (!semver.satisfies(typescriptPkg.version, '>= 4.5')) {
96
+ // throw new Error('Your version of TypeScript does not support inline type imports.');
97
+ // }
98
+
99
+ // push to first import
100
+ let [ firstImport , ...rest ] = nodes ;
101
+ const valueImport = nodes . find ( ( n ) => n . specifiers . every ( ( spec ) => spec . importKind === 'value' ) ) || nodes . find ( ( n ) => n . specifiers . some ( ( spec ) => spec . type === 'ImportDefaultSpecifier' ) ) ;
102
+ if ( valueImport ) {
103
+ firstImport = valueImport ;
104
+ rest = nodes . filter ( ( n ) => n !== firstImport ) ;
105
+ }
106
+
107
+ const nodeTokens = sourceCode . getTokens ( firstImport ) ;
108
+ // we are moving the rest of the Type or Inline Type imports here.
109
+ const nodeClosingBrace = nodeTokens . find ( token => isPunctuator ( token , '}' ) ) ;
110
+ // const preferInline = context.options[0] && context.options[0]['prefer-inline'];
111
+ if ( nodeClosingBrace ) {
112
+ for ( const node of rest ) {
113
+ // these will be all Type imports, no Value specifiers
114
+ // then add inline type specifiers to importKind === 'type' import
115
+ for ( const specifier of node . specifiers ) {
116
+ if ( specifier . importKind === 'type' ) {
117
+ fixes . push ( fixer . insertTextBefore ( nodeClosingBrace , `, type ${ specifier . local . name } ` ) ) ;
118
+ } else {
119
+ fixes . push ( fixer . insertTextBefore ( nodeClosingBrace , `, ${ specifier . local . name } ` ) ) ;
120
+ }
121
+ }
122
+
123
+ fixes . push ( fixer . remove ( node ) ) ;
124
+ }
125
+ } else {
126
+ // we have a default import only
127
+ const defaultSpecifier = firstImport . specifiers . find ( ( spec ) => spec . type === 'ImportDefaultSpecifier' ) ;
128
+ const inlineTypeImports = [ ] ;
129
+ for ( const node of rest ) {
130
+ // these will be all Type imports, no Value specifiers
131
+ // then add inline type specifiers to importKind === 'type' import
132
+ for ( const specifier of node . specifiers ) {
133
+ if ( specifier . importKind === 'type' ) {
134
+ inlineTypeImports . push ( `type ${ specifier . local . name } ` ) ;
135
+ } else {
136
+ inlineTypeImports . push ( specifier . local . name ) ;
137
+ }
138
+ }
139
+
140
+ fixes . push ( fixer . remove ( node ) ) ;
141
+ }
142
+
143
+ fixes . push ( fixer . insertTextAfter ( defaultSpecifier , `, {${ inlineTypeImports . join ( ', ' ) } }` ) ) ;
144
+ }
145
+
146
+ return fixes ;
147
+ } ;
148
+ }
149
+
150
+ function getTypeFix ( nodes , sourceCode , context ) {
151
+ return fixer => {
152
+ const fixes = [ ] ;
153
+
154
+ const preferInline = context . options [ 0 ] && context . options [ 0 ] [ 'prefer-inline' ] ;
155
+
156
+ if ( preferInline ) {
157
+ if ( ! semver . satisfies ( typescriptPkg . version , '>= 4.5' ) ) {
158
+ throw new Error ( 'Your version of TypeScript does not support inline type imports.' ) ;
159
+ }
160
+
161
+ // collapse all type imports to the inline type import
162
+ const typeImports = nodes . filter ( ( node ) => node . importKind === 'type' ) ;
163
+ const someInlineTypeImports = nodes . filter ( ( node ) => node . specifiers . some ( ( spec ) => spec . importKind === 'type' ) ) ;
164
+ // push to first import
165
+ const firstImport = someInlineTypeImports [ 0 ] ;
166
+
167
+ if ( firstImport ) {
168
+ const nodeTokens = sourceCode . getTokens ( firstImport ) ;
169
+ // we are moving the rest of the Type imports here
170
+ const nodeClosingBrace = nodeTokens . find ( token => isPunctuator ( token , '}' ) ) ;
171
+
172
+ for ( const node of typeImports ) {
173
+ for ( const specifier of node . specifiers ) {
174
+ fixes . push ( fixer . insertTextBefore ( nodeClosingBrace , `, type ${ specifier . local . name } ` ) ) ;
175
+ }
176
+
177
+ fixes . push ( fixer . remove ( node ) ) ;
178
+ }
179
+ }
180
+ } else {
181
+ // move inline types to type imports
182
+ const typeImports = nodes . filter ( ( node ) => node . importKind === 'type' ) ;
183
+ const someInlineTypeImports = nodes . filter ( ( node ) => node . specifiers . some ( ( spec ) => spec . importKind === 'type' ) ) ;
184
+
185
+ const firstImport = typeImports [ 0 ] ;
186
+
187
+ if ( firstImport ) {
188
+ const nodeTokens = sourceCode . getTokens ( firstImport ) ;
189
+ // we are moving the rest of the Type imports here
190
+ const nodeClosingBrace = nodeTokens . find ( token => isPunctuator ( token , '}' ) ) ;
191
+
192
+ for ( const node of someInlineTypeImports ) {
193
+ for ( const specifier of node . specifiers ) {
194
+ if ( specifier . importKind === 'type' ) {
195
+ fixes . push ( fixer . insertTextBefore ( nodeClosingBrace , `, ${ specifier . local . name } ` ) ) ;
196
+ }
197
+ }
198
+
199
+ if ( node . specifiers . every ( ( spec ) => spec . importKind === 'type' ) ) {
200
+ fixes . push ( fixer . remove ( node ) ) ;
201
+ } else {
202
+ for ( const specifier of node . specifiers ) {
203
+ if ( specifier . importKind === 'type' ) {
204
+ const maybeComma = sourceCode . getTokenAfter ( specifier ) ;
205
+ if ( isComma ( maybeComma ) ) {
206
+ fixes . push ( fixer . remove ( maybeComma ) ) ;
207
+ }
208
+ // TODO: remove `type`?
209
+ fixes . push ( fixer . remove ( specifier ) ) ;
210
+ }
211
+ }
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ return fixes ;
218
+ } ;
219
+ }
220
+
221
+ function getFix ( first , rest , sourceCode ) {
35
222
// Sorry ESLint <= 3 users, no autofix for you. Autofixing duplicate imports
36
223
// requires multiple `fixer.whatever()` calls in the `fix`: We both need to
37
224
// update the first one, and remove the rest. Support for multiple
@@ -119,22 +306,13 @@ function getFix(first, rest, sourceCode, context) {
119
306
120
307
const [ specifiersText ] = specifiers . reduce (
121
308
( [ result , needsComma , existingIdentifiers ] , specifier ) => {
122
- const isTypeSpecifier = specifier . importNode . importKind === 'type' ;
123
-
124
- const preferInline = context . options [ 0 ] && context . options [ 0 ] [ 'prefer-inline' ] ;
125
- // a user might set prefer-inline but not have a supporting TypeScript version. Flow does not support inline types so this should fail in that case as well.
126
- if ( preferInline && ( ! typescriptPkg || ! semver . satisfies ( typescriptPkg . version , '>= 4.5' ) ) ) {
127
- throw new Error ( 'Your version of TypeScript does not support inline type imports.' ) ;
128
- }
129
-
130
309
// Add *only* the new identifiers that don't already exist, and track any new identifiers so we don't add them again in the next loop
131
310
const [ specifierText , updatedExistingIdentifiers ] = specifier . identifiers . reduce ( ( [ text , set ] , cur ) => {
132
311
const trimmed = cur . trim ( ) ; // Trim whitespace before/after to compare to our set of existing identifiers
133
- const curWithType = trimmed . length > 0 && preferInline && isTypeSpecifier ? `type ${ cur } ` : cur ;
134
312
if ( existingIdentifiers . has ( trimmed ) ) {
135
313
return [ text , set ] ;
136
314
}
137
- return [ text . length > 0 ? `${ text } ,${ curWithType } ` : curWithType , set . add ( trimmed ) ] ;
315
+ return [ text . length > 0 ? `${ text } ,${ cur } ` : cur , set . add ( trimmed ) ] ;
138
316
} , [ '' , existingIdentifiers ] ) ;
139
317
140
318
return [
@@ -173,7 +351,7 @@ function getFix(first, rest, sourceCode, context) {
173
351
// `import def from './foo'` → `import def, {...} from './foo'`
174
352
fixes . push ( fixer . insertTextAfter ( first . specifiers [ 0 ] , `, {${ specifiersText } }` ) ) ;
175
353
}
176
- } else if ( ! shouldAddDefault && openBrace != null && closeBrace != null ) {
354
+ } else if ( ! shouldAddDefault && openBrace != null && closeBrace != null && specifiersText ) {
177
355
// `import {...} './foo'` → `import {..., ...} from './foo'`
178
356
fixes . push ( fixer . insertTextBefore ( closeBrace , specifiersText ) ) ;
179
357
}
@@ -318,14 +496,18 @@ module.exports = {
318
496
nsImported : new Map ( ) ,
319
497
defaultTypesImported : new Map ( ) ,
320
498
namedTypesImported : new Map ( ) ,
499
+ inlineTypesImported : new Map ( ) ,
321
500
} ) ;
322
501
}
323
502
const map = moduleMaps . get ( n . parent ) ;
324
503
if ( n . importKind === 'type' ) {
504
+ // import type Foo | import type { foo }
325
505
return n . specifiers . length > 0 && n . specifiers [ 0 ] . type === 'ImportDefaultSpecifier' ? map . defaultTypesImported : map . namedTypesImported ;
326
506
}
507
+
327
508
if ( n . specifiers . some ( ( spec ) => spec . importKind === 'type' ) ) {
328
- return map . namedTypesImported ;
509
+ // import { type foo }
510
+ return map . inlineTypesImported ;
329
511
}
330
512
331
513
return hasNamespace ( n ) ? map . nsImported : map . imported ;
@@ -350,6 +532,26 @@ module.exports = {
350
532
checkImports ( map . nsImported , context ) ;
351
533
checkImports ( map . defaultTypesImported , context ) ;
352
534
checkImports ( map . namedTypesImported , context ) ;
535
+
536
+ const duplicatedImports = new Map ( [ ...map . inlineTypesImported ] ) ;
537
+ map . imported . forEach ( ( value , key ) => {
538
+ if ( duplicatedImports . has ( key ) ) {
539
+ duplicatedImports . get ( key ) . push ( ...value ) ;
540
+ } else {
541
+ duplicatedImports . set ( key , [ value ] ) ;
542
+ }
543
+ } ) ;
544
+ checkInlineTypeImports ( duplicatedImports , context ) ;
545
+
546
+ const duplicatedTypeImports = new Map ( [ ...map . inlineTypesImported ] ) ;
547
+ map . namedTypesImported . forEach ( ( value , key ) => {
548
+ if ( duplicatedTypeImports . has ( key ) ) {
549
+ duplicatedTypeImports . get ( key ) . push ( ...value ) ;
550
+ } else {
551
+ duplicatedTypeImports . set ( key , value ) ;
552
+ }
553
+ } ) ;
554
+ checkTypeImports ( duplicatedTypeImports , context ) ;
353
555
}
354
556
} ,
355
557
} ;
0 commit comments