@@ -2,21 +2,193 @@ import resolve from 'eslint-module-utils/resolve'
2
2
import docsUrl from '../docsUrl'
3
3
4
4
function checkImports ( imported , context ) {
5
- for ( let [ module , nodes ] of imported . entries ( ) ) {
6
- if ( nodes . size > 1 ) {
7
- for ( let node of nodes ) {
8
- context . report ( node , `'${ module } ' imported multiple times.` )
5
+ for ( const [ module , nodes ] of imported . entries ( ) ) {
6
+ if ( nodes . length > 1 ) {
7
+ const message = `'${ module } ' imported multiple times.`
8
+ const [ first , ...rest ] = nodes
9
+ const sourceCode = context . getSourceCode ( )
10
+ const fix = getFix ( first , rest , sourceCode )
11
+
12
+ context . report ( {
13
+ node : first . source ,
14
+ message,
15
+ fix, // Attach the autofix (if any) to the first import.
16
+ } )
17
+
18
+ for ( const node of rest ) {
19
+ context . report ( {
20
+ node : node . source ,
21
+ message,
22
+ } )
23
+ }
24
+ }
25
+ }
26
+ }
27
+
28
+ function getFix ( first , rest , sourceCode ) {
29
+ const defaultImportNames = new Set (
30
+ [ first , ...rest ] . map ( getDefaultImportName ) . filter ( Boolean )
31
+ )
32
+
33
+ // Bail if there are multiple different default import names – it's up to the
34
+ // user to choose which one to keep.
35
+ if ( defaultImportNames . size > 1 ) {
36
+ return undefined
37
+ }
38
+
39
+ // It's not obvious what the user wants to do with comments associated with
40
+ // duplicate imports, so skip imports with comments when autofixing.
41
+ const restWithoutComments = rest . filter ( node => ! (
42
+ hasCommentBefore ( node , sourceCode ) ||
43
+ hasCommentAfter ( node , sourceCode ) ||
44
+ hasCommentInsideNonSpecifiers ( node , sourceCode )
45
+ ) )
46
+
47
+ const specifiers = restWithoutComments
48
+ . map ( node => {
49
+ const tokens = sourceCode . getTokens ( node )
50
+ const openBrace = tokens . find ( token => isPunctuator ( token , '{' ) )
51
+ const closeBrace = tokens . find ( token => isPunctuator ( token , '}' ) )
52
+
53
+ if ( openBrace == null || closeBrace == null ) {
54
+ return undefined
55
+ }
56
+
57
+ return {
58
+ importNode : node ,
59
+ text : sourceCode . text . slice ( openBrace . range [ 1 ] , closeBrace . range [ 0 ] ) ,
60
+ hasTrailingComma : isPunctuator ( sourceCode . getTokenBefore ( closeBrace ) , ',' ) ,
61
+ isEmpty : ! hasSpecifiers ( node ) ,
62
+ }
63
+ } )
64
+ . filter ( Boolean )
65
+
66
+ const unnecessaryImports = restWithoutComments . filter ( node =>
67
+ ! hasSpecifiers ( node ) &&
68
+ ! specifiers . some ( specifier => specifier . importNode === node )
69
+ )
70
+
71
+ const shouldAddDefault = getDefaultImportName ( first ) == null && defaultImportNames . size === 1
72
+ const shouldAddSpecifiers = specifiers . length > 0
73
+ const shouldRemoveUnnecessary = unnecessaryImports . length > 0
74
+
75
+ if ( ! ( shouldAddDefault || shouldAddSpecifiers || shouldRemoveUnnecessary ) ) {
76
+ return undefined
77
+ }
78
+
79
+ return function * ( fixer ) {
80
+ const tokens = sourceCode . getTokens ( first )
81
+ const openBrace = tokens . find ( token => isPunctuator ( token , '{' ) )
82
+ const closeBrace = tokens . find ( token => isPunctuator ( token , '}' ) )
83
+ const firstToken = sourceCode . getFirstToken ( first )
84
+ const [ defaultImportName ] = defaultImportNames
85
+
86
+ const firstHasTrailingComma =
87
+ closeBrace != null &&
88
+ isPunctuator ( sourceCode . getTokenBefore ( closeBrace ) , ',' )
89
+ const firstIsEmpty = ! hasSpecifiers ( first )
90
+
91
+ const [ specifiersText ] = specifiers . reduce (
92
+ ( [ result , needsComma ] , specifier ) => {
93
+ return [
94
+ needsComma && ! specifier . isEmpty
95
+ ? `${ result } ,${ specifier . text } `
96
+ : `${ result } ${ specifier . text } ` ,
97
+ specifier . isEmpty ? needsComma : true ,
98
+ ]
99
+ } ,
100
+ [ '' , ! firstHasTrailingComma && ! firstIsEmpty ]
101
+ )
102
+
103
+ if ( shouldAddDefault && openBrace == null && shouldAddSpecifiers ) {
104
+ // `import './foo'` → `import def, {...} from './foo'`
105
+ yield fixer . insertTextAfter ( firstToken , ` ${ defaultImportName } , {${ specifiersText } } from` )
106
+ } else if ( shouldAddDefault && openBrace == null && ! shouldAddSpecifiers ) {
107
+ // `import './foo'` → `import def from './foo'`
108
+ yield fixer . insertTextAfter ( firstToken , ` ${ defaultImportName } from` )
109
+ } else if ( shouldAddDefault && openBrace != null && closeBrace != null ) {
110
+ // `import {...} from './foo'` → `import def, {...} from './foo'`
111
+ yield fixer . insertTextAfter ( firstToken , ` ${ defaultImportName } ,` )
112
+ if ( shouldAddSpecifiers ) {
113
+ // `import def, {...} from './foo'` → `import def, {..., ...} from './foo'`
114
+ yield fixer . insertTextBefore ( closeBrace , specifiersText )
9
115
}
116
+ } else if ( ! shouldAddDefault && openBrace == null && shouldAddSpecifiers ) {
117
+ // `import './foo'` → `import {...} from './foo'`
118
+ yield fixer . insertTextAfter ( firstToken , ` {${ specifiersText } } from` )
119
+ } else if ( ! shouldAddDefault && openBrace != null && closeBrace != null ) {
120
+ // `import {...} './foo'` → `import {..., ...} from './foo'`
121
+ yield fixer . insertTextBefore ( closeBrace , specifiersText )
122
+ }
123
+
124
+ // Remove imports whose specifiers have been moved into the first import.
125
+ for ( const specifier of specifiers ) {
126
+ yield fixer . remove ( specifier . importNode )
127
+ }
128
+
129
+ // Remove imports whose default import has been moved to the first import,
130
+ // and side-effect-only imports that are unnecessary due to the first
131
+ // import.
132
+ for ( const node of unnecessaryImports ) {
133
+ yield fixer . remove ( node )
10
134
}
11
135
}
12
136
}
13
137
138
+ function isPunctuator ( node , value ) {
139
+ return node . type === 'Punctuator' && node . value === value
140
+ }
141
+
142
+ // Get the name of the default import of `node`, if any.
143
+ function getDefaultImportName ( node ) {
144
+ const defaultSpecifier = node . specifiers
145
+ . find ( specifier => specifier . type === 'ImportDefaultSpecifier' )
146
+ return defaultSpecifier != null ? defaultSpecifier . local . name : undefined
147
+ }
148
+
149
+ // Checks whether `node` has any non-default specifiers.
150
+ function hasSpecifiers ( node ) {
151
+ const specifiers = node . specifiers
152
+ . filter ( specifier => specifier . type === 'ImportSpecifier' )
153
+ return specifiers . length > 0
154
+ }
155
+
156
+ // Checks whether `node` has a comment (that ends) on the previous line or on
157
+ // the same line as `node` (starts).
158
+ function hasCommentBefore ( node , sourceCode ) {
159
+ return sourceCode . getCommentsBefore ( node )
160
+ . some ( comment => comment . loc . end . line >= node . loc . start . line - 1 )
161
+ }
162
+
163
+ // Checks whether `node` has a comment (that starts) on the same line as `node`
164
+ // (ends).
165
+ function hasCommentAfter ( node , sourceCode ) {
166
+ return sourceCode . getCommentsAfter ( node )
167
+ . some ( comment => comment . loc . start . line === node . loc . end . line )
168
+ }
169
+
170
+ // Checks whether `node` has any comments _inside,_ except inside the `{...}`
171
+ // part (if any).
172
+ function hasCommentInsideNonSpecifiers ( node , sourceCode ) {
173
+ const tokens = sourceCode . getTokens ( node )
174
+ const openBraceIndex = tokens . findIndex ( token => isPunctuator ( token , '{' ) )
175
+ const closeBraceIndex = tokens . findIndex ( token => isPunctuator ( token , '}' ) )
176
+ // Slice away the first token, since we're no looking for comments _before_
177
+ // `node` (only inside). If there's a `{...}` part, look for comments before
178
+ // the `{`, but not before the `}` (hence the `+1`s).
179
+ const someTokens = openBraceIndex >= 0 && closeBraceIndex >= 0
180
+ ? tokens . slice ( 1 , openBraceIndex + 1 ) . concat ( tokens . slice ( closeBraceIndex + 1 ) )
181
+ : tokens . slice ( 1 )
182
+ return someTokens . some ( token => sourceCode . getCommentsBefore ( token ) . length > 0 )
183
+ }
184
+
14
185
module . exports = {
15
186
meta : {
16
187
type : 'problem' ,
17
188
docs : {
18
189
url : docsUrl ( 'no-duplicates' ) ,
19
190
} ,
191
+ fixable : 'code' ,
20
192
} ,
21
193
22
194
create : function ( context ) {
@@ -29,9 +201,9 @@ module.exports = {
29
201
const importMap = n . importKind === 'type' ? typesImported : imported
30
202
31
203
if ( importMap . has ( resolvedPath ) ) {
32
- importMap . get ( resolvedPath ) . add ( n . source )
204
+ importMap . get ( resolvedPath ) . push ( n )
33
205
} else {
34
- importMap . set ( resolvedPath , new Set ( [ n . source ] ) )
206
+ importMap . set ( resolvedPath , [ n ] )
35
207
}
36
208
} ,
37
209
0 commit comments