@@ -6,7 +6,7 @@ import docsUrl from '../docsUrl'
66
77const defaultGroups = [ 'builtin' , 'external' , 'parent' , 'sibling' , 'index' ]
88
9- // REPORTING
9+ // REPORTING AND FIXING
1010
1111function reverse ( array ) {
1212 return array . map ( function ( v ) {
@@ -18,6 +18,60 @@ function reverse(array) {
1818 } ) . reverse ( )
1919}
2020
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+
2175function findOutOfOrder ( imported ) {
2276 if ( imported . length === 0 ) {
2377 return [ ]
@@ -32,13 +86,141 @@ function findOutOfOrder(imported) {
3286 } )
3387}
3488
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+
35218function reportOutOfOrder ( context , imported , outOfOrder , order ) {
36219 outOfOrder . forEach ( function ( imp ) {
37220 const found = imported . find ( function hasHigherRank ( importedItem ) {
38221 return importedItem . rank > imp . rank
39222 } )
40- context . report ( imp . node , '`' + imp . name + '` import should occur ' + order +
41- ' import of `' + found . name + '`' )
223+ fixOutOfOrder ( context , found , imp , order )
42224 } )
43225}
44226
@@ -109,6 +291,32 @@ function convertGroupsToRanks(groups) {
109291 } , rankObject )
110292}
111293
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+
112320function makeNewlinesBetweenReport ( context , imported , newlinesBetweenImports ) {
113321 const getNumberOfEmptyLinesBetween = ( currentImport , previousImport ) => {
114322 const linesBetweenImports = context . getSourceCode ( ) . lines . slice (
@@ -125,23 +333,27 @@ function makeNewlinesBetweenReport (context, imported, newlinesBetweenImports) {
125333
126334 if ( newlinesBetweenImports === 'always'
127335 || 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+ } )
133342 } else if ( currentImport . rank === previousImport . rank
134343 && 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+ } )
144350 }
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+ } )
145357 }
146358
147359 previousImport = currentImport
@@ -154,6 +366,7 @@ module.exports = {
154366 url : docsUrl ( 'order' ) ,
155367 } ,
156368
369+ fixable : 'code' ,
157370 schema : [
158371 {
159372 type : 'object' ,
0 commit comments