@@ -23,6 +23,59 @@ function checkImports(imported, context) {
2323 fix, // Attach the autofix (if any) to the first import.
2424 } ) ;
2525
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+
2679 for ( const node of rest ) {
2780 context . report ( {
2881 node : node . source ,
@@ -33,7 +86,141 @@ function checkImports(imported, context) {
3386 }
3487}
3588
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 ) {
37224 // Sorry ESLint <= 3 users, no autofix for you. Autofixing duplicate imports
38225 // requires multiple `fixer.whatever()` calls in the `fix`: We both need to
39226 // update the first one, and remove the rest. Support for multiple
@@ -115,22 +302,13 @@ function getFix(first, rest, sourceCode, context) {
115302
116303 const [ specifiersText ] = specifiers . reduce (
117304 ( [ 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-
126305 // 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
127306 const [ specifierText , updatedExistingIdentifiers ] = specifier . identifiers . reduce ( ( [ text , set ] , cur ) => {
128307 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 ;
130308 if ( existingIdentifiers . has ( trimmed ) ) {
131309 return [ text , set ] ;
132310 }
133- return [ text . length > 0 ? `${ text } ,${ curWithType } ` : curWithType , set . add ( trimmed ) ] ;
311+ return [ text . length > 0 ? `${ text } ,${ cur } ` : cur , set . add ( trimmed ) ] ;
134312 } , [ '' , existingIdentifiers ] ) ;
135313
136314 return [
@@ -169,7 +347,7 @@ function getFix(first, rest, sourceCode, context) {
169347 // `import def from './foo'` → `import def, {...} from './foo'`
170348 fixes . push ( fixer . insertTextAfter ( first . specifiers [ 0 ] , `, {${ specifiersText } }` ) ) ;
171349 }
172- } else if ( ! shouldAddDefault && openBrace != null && closeBrace != null ) {
350+ } else if ( ! shouldAddDefault && openBrace != null && closeBrace != null && specifiersText ) {
173351 // `import {...} './foo'` → `import {..., ...} from './foo'`
174352 fixes . push ( fixer . insertTextBefore ( closeBrace , specifiersText ) ) ;
175353 }
@@ -314,15 +492,18 @@ module.exports = {
314492 nsImported : new Map ( ) ,
315493 defaultTypesImported : new Map ( ) ,
316494 namedTypesImported : new Map ( ) ,
495+ inlineTypesImported : new Map ( ) ,
317496 } ) ;
318497 }
319498 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 }
322501 return n . specifiers . length > 0 && n . specifiers [ 0 ] . type === 'ImportDefaultSpecifier' ? map . defaultTypesImported : map . namedTypesImported ;
323502 }
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 ;
326507 }
327508
328509 return hasNamespace ( n ) ? map . nsImported : map . imported ;
@@ -347,6 +528,26 @@ module.exports = {
347528 checkImports ( map . nsImported , context ) ;
348529 checkImports ( map . defaultTypesImported , context ) ;
349530 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 ) ;
350551 }
351552 } ,
352553 } ;
0 commit comments