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