Skip to content

Commit b34d9ff

Browse files
tihonovebenmosher
authored andcommitted
Add autofixer for order rule (#908)
1 parent e215b61 commit b34d9ff

File tree

4 files changed

+717
-26
lines changed

4 files changed

+717
-26
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
44
This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com).
55

66
## [Unreleased]
7+
- Autofixer for [`order`] rule ([#711], thanks [@tihonove])
78

89
## [2.9.0] - 2018-02-21
910
### Added
@@ -679,6 +680,7 @@ for info on changes for earlier releases.
679680
[@mplewis]: https://github.com/mplewis
680681
[@rosswarren]: https://github.com/rosswarren
681682
[@alexgorbatchev]: https://github.com/alexgorbatchev
683+
[@tihonove]: https://github.com/tihonove
682684
[@robertrossmann]: https://github.com/robertrossmann
683685
[@isiahmeadows]: https://github.com/isiahmeadows
684686
[@graingert]: https://github.com/graingert

docs/rules/order.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# import/order: Enforce a convention in module import order
22

3-
Enforce a convention in the order of `require()` / `import` statements. The order is as shown in the following example:
3+
Enforce a convention in the order of `require()` / `import` statements.
4+
+(fixable) The `--fix` option on the [command line] automatically fixes problems reported by this rule.
5+
The order is as shown in the following example:
46

57
```js
68
// 1. node "builtin" modules

src/rules/order.js

Lines changed: 230 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import docsUrl from '../docsUrl'
66

77
const defaultGroups = ['builtin', 'external', 'parent', 'sibling', 'index']
88

9-
// REPORTING
9+
// REPORTING AND FIXING
1010

1111
function 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+
2175
function 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+
35218
function 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+
112320
function 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

Comments
 (0)