@@ -6,7 +6,7 @@ import docsUrl from '../docsUrl'
6
6
7
7
const defaultGroups = [ 'builtin' , 'external' , 'parent' , 'sibling' , 'index' ]
8
8
9
- // REPORTING
9
+ // REPORTING AND FIXING
10
10
11
11
function reverse ( array ) {
12
12
return array . map ( function ( v ) {
@@ -18,6 +18,60 @@ function reverse(array) {
18
18
} ) . reverse ( )
19
19
}
20
20
21
+ function getTokensOrCommentsAfter ( sourceCode , node , count ) {
22
+ let currentNodeOrToken = node
23
+ const result = [ ]
24
+ for ( let i = 0 ; i < count ; i ++ ) {
25
+ currentNodeOrToken = sourceCode . getTokenOrCommentAfter ( currentNodeOrToken )
26
+ if ( currentNodeOrToken == null ) {
27
+ break
28
+ }
29
+ result . push ( currentNodeOrToken )
30
+ }
31
+ return result
32
+ }
33
+
34
+ function getTokensOrCommentsBefore ( sourceCode , node , count ) {
35
+ let currentNodeOrToken = node
36
+ const result = [ ]
37
+ for ( let i = 0 ; i < count ; i ++ ) {
38
+ currentNodeOrToken = sourceCode . getTokenOrCommentBefore ( currentNodeOrToken )
39
+ if ( currentNodeOrToken == null ) {
40
+ break
41
+ }
42
+ result . push ( currentNodeOrToken )
43
+ }
44
+ return result . reverse ( )
45
+ }
46
+
47
+ function takeTokensAfterWhile ( sourceCode , node , condition ) {
48
+ const tokens = getTokensOrCommentsAfter ( sourceCode , node , 100 )
49
+ const result = [ ]
50
+ for ( let i = 0 ; i < tokens . length ; i ++ ) {
51
+ if ( condition ( tokens [ i ] ) ) {
52
+ result . push ( tokens [ i ] )
53
+ }
54
+ else {
55
+ break
56
+ }
57
+ }
58
+ return result
59
+ }
60
+
61
+ function takeTokensBeforeWhile ( sourceCode , node , condition ) {
62
+ const tokens = getTokensOrCommentsBefore ( sourceCode , node , 100 )
63
+ const result = [ ]
64
+ for ( let i = tokens . length - 1 ; i >= 0 ; i -- ) {
65
+ if ( condition ( tokens [ i ] ) ) {
66
+ result . push ( tokens [ i ] )
67
+ }
68
+ else {
69
+ break
70
+ }
71
+ }
72
+ return result . reverse ( )
73
+ }
74
+
21
75
function findOutOfOrder ( imported ) {
22
76
if ( imported . length === 0 ) {
23
77
return [ ]
@@ -32,13 +86,141 @@ function findOutOfOrder(imported) {
32
86
} )
33
87
}
34
88
89
+ function findRootNode ( node ) {
90
+ let parent = node
91
+ while ( parent . parent != null && parent . parent . body == null ) {
92
+ parent = parent . parent
93
+ }
94
+ return parent
95
+ }
96
+
97
+ function findEndOfLineWithComments ( sourceCode , node ) {
98
+ const tokensToEndOfLine = takeTokensAfterWhile ( sourceCode , node , commentOnSameLineAs ( node ) )
99
+ let endOfTokens = tokensToEndOfLine . length > 0
100
+ ? tokensToEndOfLine [ tokensToEndOfLine . length - 1 ] . end
101
+ : node . end
102
+ let result = endOfTokens
103
+ for ( let i = endOfTokens ; i < sourceCode . text . length ; i ++ ) {
104
+ if ( sourceCode . text [ i ] === '\n' ) {
105
+ result = i + 1
106
+ break
107
+ }
108
+ if ( sourceCode . text [ i ] !== ' ' && sourceCode . text [ i ] !== '\t' && sourceCode . text [ i ] !== '\r' ) {
109
+ break
110
+ }
111
+ result = i + 1
112
+ }
113
+ return result
114
+ }
115
+
116
+ function commentOnSameLineAs ( node ) {
117
+ return token => ( token . type === 'Block' || token . type === 'Line' ) &&
118
+ token . loc . start . line === token . loc . end . line &&
119
+ token . loc . end . line === node . loc . end . line
120
+ }
121
+
122
+ function findStartOfLineWithComments ( sourceCode , node ) {
123
+ const tokensToEndOfLine = takeTokensBeforeWhile ( sourceCode , node , commentOnSameLineAs ( node ) )
124
+ let startOfTokens = tokensToEndOfLine . length > 0 ? tokensToEndOfLine [ 0 ] . start : node . start
125
+ let result = startOfTokens
126
+ for ( let i = startOfTokens - 1 ; i > 0 ; i -- ) {
127
+ if ( sourceCode . text [ i ] !== ' ' && sourceCode . text [ i ] !== '\t' ) {
128
+ break
129
+ }
130
+ result = i
131
+ }
132
+ return result
133
+ }
134
+
135
+ function isPlainRequireModule ( node ) {
136
+ if ( node . type !== 'VariableDeclaration' ) {
137
+ return false
138
+ }
139
+ if ( node . declarations . length !== 1 ) {
140
+ return false
141
+ }
142
+ const decl = node . declarations [ 0 ]
143
+ const result = ( decl . id != null && decl . id . type === 'Identifier' ) &&
144
+ decl . init != null &&
145
+ decl . init . type === 'CallExpression' &&
146
+ decl . init . callee != null &&
147
+ decl . init . callee . name === 'require' &&
148
+ decl . init . arguments != null &&
149
+ decl . init . arguments . length === 1 &&
150
+ decl . init . arguments [ 0 ] . type === 'Literal'
151
+ return result
152
+ }
153
+
154
+ function isPlainImportModule ( node ) {
155
+ return node . type === 'ImportDeclaration' && node . specifiers != null && node . specifiers . length > 0
156
+ }
157
+
158
+ function canCrossNodeWhileReorder ( node ) {
159
+ return isPlainRequireModule ( node ) || isPlainImportModule ( node )
160
+ }
161
+
162
+ function canReorderItems ( firstNode , secondNode ) {
163
+ const parent = firstNode . parent
164
+ const firstIndex = parent . body . indexOf ( firstNode )
165
+ const secondIndex = parent . body . indexOf ( secondNode )
166
+ const nodesBetween = parent . body . slice ( firstIndex , secondIndex + 1 )
167
+ for ( var nodeBetween of nodesBetween ) {
168
+ if ( ! canCrossNodeWhileReorder ( nodeBetween ) ) {
169
+ return false
170
+ }
171
+ }
172
+ return true
173
+ }
174
+
175
+ function fixOutOfOrder ( context , firstNode , secondNode , order ) {
176
+ const sourceCode = context . getSourceCode ( )
177
+
178
+ const firstRoot = findRootNode ( firstNode . node )
179
+ let firstRootStart = findStartOfLineWithComments ( sourceCode , firstRoot )
180
+ const firstRootEnd = findEndOfLineWithComments ( sourceCode , firstRoot )
181
+
182
+ const secondRoot = findRootNode ( secondNode . node )
183
+ let secondRootStart = findStartOfLineWithComments ( sourceCode , secondRoot )
184
+ let secondRootEnd = findEndOfLineWithComments ( sourceCode , secondRoot )
185
+ const canFix = canReorderItems ( firstRoot , secondRoot )
186
+
187
+ let newCode = sourceCode . text . substring ( secondRootStart , secondRootEnd )
188
+ if ( newCode [ newCode . length - 1 ] !== '\n' ) {
189
+ newCode = newCode + '\n'
190
+ }
191
+
192
+ const message = '`' + secondNode . name + '` import should occur ' + order +
193
+ ' import of `' + firstNode . name + '`'
194
+
195
+ if ( order === 'before' ) {
196
+ context . report ( {
197
+ node : secondNode . node ,
198
+ message : message ,
199
+ fix : canFix && ( fixer =>
200
+ fixer . replaceTextRange (
201
+ [ firstRootStart , secondRootEnd ] ,
202
+ newCode + sourceCode . text . substring ( firstRootStart , secondRootStart )
203
+ ) ) ,
204
+ } )
205
+ } else if ( order === 'after' ) {
206
+ context . report ( {
207
+ node : secondNode . node ,
208
+ message : message ,
209
+ fix : canFix && ( fixer =>
210
+ fixer . replaceTextRange (
211
+ [ secondRootStart , firstRootEnd ] ,
212
+ sourceCode . text . substring ( secondRootEnd , firstRootEnd ) + newCode
213
+ ) ) ,
214
+ } )
215
+ }
216
+ }
217
+
35
218
function reportOutOfOrder ( context , imported , outOfOrder , order ) {
36
219
outOfOrder . forEach ( function ( imp ) {
37
220
const found = imported . find ( function hasHigherRank ( importedItem ) {
38
221
return importedItem . rank > imp . rank
39
222
} )
40
- context . report ( imp . node , '`' + imp . name + '` import should occur ' + order +
41
- ' import of `' + found . name + '`' )
223
+ fixOutOfOrder ( context , found , imp , order )
42
224
} )
43
225
}
44
226
@@ -109,6 +291,32 @@ function convertGroupsToRanks(groups) {
109
291
} , rankObject )
110
292
}
111
293
294
+ function fixNewLineAfterImport ( context , previousImport ) {
295
+ const prevRoot = findRootNode ( previousImport . node )
296
+ const tokensToEndOfLine = takeTokensAfterWhile (
297
+ context . getSourceCode ( ) , prevRoot , commentOnSameLineAs ( prevRoot ) )
298
+
299
+ let endOfLine = prevRoot . end
300
+ if ( tokensToEndOfLine . length > 0 ) {
301
+ endOfLine = tokensToEndOfLine [ tokensToEndOfLine . length - 1 ] . end
302
+ }
303
+ return ( fixer ) => fixer . insertTextAfterRange ( [ prevRoot . start , endOfLine ] , '\n' )
304
+ }
305
+
306
+ function removeNewLineAfterImport ( context , currentImport , previousImport ) {
307
+ const sourceCode = context . getSourceCode ( )
308
+ const prevRoot = findRootNode ( previousImport . node )
309
+ const currRoot = findRootNode ( currentImport . node )
310
+ const rangeToRemove = [
311
+ findEndOfLineWithComments ( sourceCode , prevRoot ) ,
312
+ findStartOfLineWithComments ( sourceCode , currRoot ) ,
313
+ ]
314
+ if ( / ^ \s * $ / . test ( sourceCode . text . substring ( rangeToRemove [ 0 ] , rangeToRemove [ 1 ] ) ) ) {
315
+ return ( fixer ) => fixer . removeRange ( rangeToRemove )
316
+ }
317
+ return undefined
318
+ }
319
+
112
320
function makeNewlinesBetweenReport ( context , imported , newlinesBetweenImports ) {
113
321
const getNumberOfEmptyLinesBetween = ( currentImport , previousImport ) => {
114
322
const linesBetweenImports = context . getSourceCode ( ) . lines . slice (
@@ -125,23 +333,27 @@ function makeNewlinesBetweenReport (context, imported, newlinesBetweenImports) {
125
333
126
334
if ( newlinesBetweenImports === 'always'
127
335
|| newlinesBetweenImports === 'always-and-inside-groups' ) {
128
- if ( currentImport . rank !== previousImport . rank && emptyLinesBetween === 0 )
129
- {
130
- context . report (
131
- previousImport . node , 'There should be at least one empty line between import groups'
132
- )
336
+ if ( currentImport . rank !== previousImport . rank && emptyLinesBetween === 0 ) {
337
+ context . report ( {
338
+ node : previousImport . node ,
339
+ message : 'There should be at least one empty line between import groups' ,
340
+ fix : fixNewLineAfterImport ( context , previousImport , currentImport ) ,
341
+ } )
133
342
} else if ( currentImport . rank === previousImport . rank
134
343
&& emptyLinesBetween > 0
135
- && newlinesBetweenImports !== 'always-and-inside-groups' )
136
- {
137
- context . report (
138
- previousImport . node , 'There should be no empty line within import group'
139
- )
140
- }
141
- } else {
142
- if ( emptyLinesBetween > 0 ) {
143
- context . report ( previousImport . node , 'There should be no empty line between import groups' )
344
+ && newlinesBetweenImports !== 'always-and-inside-groups' ) {
345
+ context . report ( {
346
+ node : previousImport . node ,
347
+ message : 'There should be no empty line within import group' ,
348
+ fix : removeNewLineAfterImport ( context , currentImport , previousImport ) ,
349
+ } )
144
350
}
351
+ } else if ( emptyLinesBetween > 0 ) {
352
+ context . report ( {
353
+ node : previousImport . node ,
354
+ message : 'There should be no empty line between import groups' ,
355
+ fix : removeNewLineAfterImport ( context , currentImport , previousImport ) ,
356
+ } )
145
357
}
146
358
147
359
previousImport = currentImport
@@ -154,6 +366,7 @@ module.exports = {
154
366
url : docsUrl ( 'order' ) ,
155
367
} ,
156
368
369
+ fixable : 'code' ,
157
370
schema : [
158
371
{
159
372
type : 'object' ,
0 commit comments