@@ -27,11 +27,198 @@ function checkImports(imported, context) {
27
27
message,
28
28
} ) ;
29
29
}
30
+
31
+ }
32
+ }
33
+ }
34
+
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
+ }
30
59
}
31
60
}
32
61
}
33
62
34
- function getFix ( first , rest , sourceCode , context ) {
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
@@ -112,26 +299,18 @@ function getFix(first, rest, sourceCode, context) {
112
299
isPunctuator ( sourceCode . getTokenBefore ( closeBrace ) , ',' ) ;
113
300
const firstIsEmpty = ! hasSpecifiers ( first ) ;
114
301
115
- const [ specifiersText ] = specifiers . reduce (
116
- ( [ result , needsComma ] , specifier ) => {
117
- const isTypeSpecifier = specifier . importNode . importKind === 'type' ;
118
-
119
- const preferInline = context . options [ 0 ] && context . options [ 0 ] [ 'prefer-inline' ] ;
120
- // 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.
121
- if ( preferInline && ( ! typescriptPkg || ! semver . satisfies ( typescriptPkg . version , '>= 4.5' ) ) ) {
122
- throw new Error ( 'Your version of TypeScript does not support inline type imports.' ) ;
123
- }
124
-
125
- const insertText = `${ preferInline && isTypeSpecifier ? 'type ' : '' } ${ specifier . text } ` ;
126
- return [
127
- needsComma && ! specifier . isEmpty
128
- ? `${ result } ,${ insertText } `
129
- : `${ result } ${ insertText } ` ,
130
- specifier . isEmpty ? needsComma : true ,
131
- ] ;
132
- } ,
133
- [ '' , ! firstHasTrailingComma && ! firstIsEmpty ] ,
134
- ) ;
302
+ const [ specifiersText ] = specifiers
303
+ . reduce (
304
+ ( [ result , needsComma ] , specifier ) => {
305
+ return [
306
+ needsComma && ! specifier . isEmpty
307
+ ? `${ result } ,${ specifier . text } `
308
+ : `${ result } ${ specifier . text } ` ,
309
+ specifier . isEmpty ? needsComma : true ,
310
+ ] ;
311
+ } ,
312
+ [ '' , ! firstHasTrailingComma && ! firstIsEmpty ] ,
313
+ ) ;
135
314
136
315
const fixes = [ ] ;
137
316
@@ -158,7 +337,7 @@ function getFix(first, rest, sourceCode, context) {
158
337
// `import def from './foo'` → `import def, {...} from './foo'`
159
338
fixes . push ( fixer . insertTextAfter ( first . specifiers [ 0 ] , `, {${ specifiersText } }` ) ) ;
160
339
}
161
- } else if ( ! shouldAddDefault && openBrace != null && closeBrace != null ) {
340
+ } else if ( ! shouldAddDefault && openBrace != null && closeBrace != null && specifiersText ) {
162
341
// `import {...} './foo'` → `import {..., ...} from './foo'`
163
342
fixes . push ( fixer . insertTextBefore ( closeBrace , specifiersText ) ) ;
164
343
}
@@ -303,14 +482,18 @@ module.exports = {
303
482
nsImported : new Map ( ) ,
304
483
defaultTypesImported : new Map ( ) ,
305
484
namedTypesImported : new Map ( ) ,
485
+ inlineTypesImported : new Map ( ) ,
306
486
} ) ;
307
487
}
308
488
const map = moduleMaps . get ( n . parent ) ;
309
489
if ( n . importKind === 'type' ) {
490
+ // import type Foo | import type { foo }
310
491
return n . specifiers . length > 0 && n . specifiers [ 0 ] . type === 'ImportDefaultSpecifier' ? map . defaultTypesImported : map . namedTypesImported ;
311
492
}
493
+
312
494
if ( n . specifiers . some ( ( spec ) => spec . importKind === 'type' ) ) {
313
- return map . namedTypesImported ;
495
+ // import { type foo }
496
+ return map . inlineTypesImported ;
314
497
}
315
498
316
499
return hasNamespace ( n ) ? map . nsImported : map . imported ;
@@ -335,6 +518,26 @@ module.exports = {
335
518
checkImports ( map . nsImported , context ) ;
336
519
checkImports ( map . defaultTypesImported , context ) ;
337
520
checkImports ( map . namedTypesImported , context ) ;
521
+
522
+ const duplicatedImports = new Map ( [ ...map . inlineTypesImported ] ) ;
523
+ map . imported . forEach ( ( value , key ) => {
524
+ if ( duplicatedImports . has ( key ) ) {
525
+ duplicatedImports . get ( key ) . push ( ...value ) ;
526
+ } else {
527
+ duplicatedImports . set ( key , [ value ] ) ;
528
+ }
529
+ } ) ;
530
+ checkInlineTypeImports ( duplicatedImports , context ) ;
531
+
532
+ const duplicatedTypeImports = new Map ( [ ...map . inlineTypesImported ] ) ;
533
+ map . namedTypesImported . forEach ( ( value , key ) => {
534
+ if ( duplicatedTypeImports . has ( key ) ) {
535
+ duplicatedTypeImports . get ( key ) . push ( ...value ) ;
536
+ } else {
537
+ duplicatedTypeImports . set ( key , value ) ;
538
+ }
539
+ } ) ;
540
+ checkTypeImports ( duplicatedTypeImports , context ) ;
338
541
}
339
542
} ,
340
543
} ;
0 commit comments