Skip to content

Commit

Permalink
Add autofixer for order rule (#908)
Browse files Browse the repository at this point in the history
  • Loading branch information
tihonove authored and benmosher committed Mar 28, 2018
1 parent e215b61 commit b34d9ff
Show file tree
Hide file tree
Showing 4 changed files with 717 additions and 26 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
This change log adheres to standards from [Keep a CHANGELOG](http://keepachangelog.com).

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

## [2.9.0] - 2018-02-21
### Added
Expand Down Expand Up @@ -679,6 +680,7 @@ for info on changes for earlier releases.
[@mplewis]: https://github.com/mplewis
[@rosswarren]: https://github.com/rosswarren
[@alexgorbatchev]: https://github.com/alexgorbatchev
[@tihonove]: https://github.com/tihonove
[@robertrossmann]: https://github.com/robertrossmann
[@isiahmeadows]: https://github.com/isiahmeadows
[@graingert]: https://github.com/graingert
Expand Down
4 changes: 3 additions & 1 deletion docs/rules/order.md
@@ -1,6 +1,8 @@
# import/order: Enforce a convention in module import order

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

```js
// 1. node "builtin" modules
Expand Down
247 changes: 230 additions & 17 deletions src/rules/order.js
Expand Up @@ -6,7 +6,7 @@ import docsUrl from '../docsUrl'

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

// REPORTING
// REPORTING AND FIXING

function reverse(array) {
return array.map(function (v) {
Expand All @@ -18,6 +18,60 @@ function reverse(array) {
}).reverse()
}

function getTokensOrCommentsAfter(sourceCode, node, count) {
let currentNodeOrToken = node
const result = []
for (let i = 0; i < count; i++) {
currentNodeOrToken = sourceCode.getTokenOrCommentAfter(currentNodeOrToken)
if (currentNodeOrToken == null) {
break
}
result.push(currentNodeOrToken)
}
return result
}

function getTokensOrCommentsBefore(sourceCode, node, count) {
let currentNodeOrToken = node
const result = []
for (let i = 0; i < count; i++) {
currentNodeOrToken = sourceCode.getTokenOrCommentBefore(currentNodeOrToken)
if (currentNodeOrToken == null) {
break
}
result.push(currentNodeOrToken)
}
return result.reverse()
}

function takeTokensAfterWhile(sourceCode, node, condition) {
const tokens = getTokensOrCommentsAfter(sourceCode, node, 100)
const result = []
for (let i = 0; i < tokens.length; i++) {
if (condition(tokens[i])) {
result.push(tokens[i])
}
else {
break
}
}
return result
}

function takeTokensBeforeWhile(sourceCode, node, condition) {
const tokens = getTokensOrCommentsBefore(sourceCode, node, 100)
const result = []
for (let i = tokens.length - 1; i >= 0; i--) {
if (condition(tokens[i])) {
result.push(tokens[i])
}
else {
break
}
}
return result.reverse()
}

function findOutOfOrder(imported) {
if (imported.length === 0) {
return []
Expand All @@ -32,13 +86,141 @@ function findOutOfOrder(imported) {
})
}

function findRootNode(node) {
let parent = node
while (parent.parent != null && parent.parent.body == null) {
parent = parent.parent
}
return parent
}

function findEndOfLineWithComments(sourceCode, node) {
const tokensToEndOfLine = takeTokensAfterWhile(sourceCode, node, commentOnSameLineAs(node))
let endOfTokens = tokensToEndOfLine.length > 0
? tokensToEndOfLine[tokensToEndOfLine.length - 1].end
: node.end
let result = endOfTokens
for (let i = endOfTokens; i < sourceCode.text.length; i++) {
if (sourceCode.text[i] === '\n') {
result = i + 1
break
}
if (sourceCode.text[i] !== ' ' && sourceCode.text[i] !== '\t' && sourceCode.text[i] !== '\r') {
break
}
result = i + 1
}
return result
}

function commentOnSameLineAs(node) {
return token => (token.type === 'Block' || token.type === 'Line') &&
token.loc.start.line === token.loc.end.line &&
token.loc.end.line === node.loc.end.line
}

function findStartOfLineWithComments(sourceCode, node) {
const tokensToEndOfLine = takeTokensBeforeWhile(sourceCode, node, commentOnSameLineAs(node))
let startOfTokens = tokensToEndOfLine.length > 0 ? tokensToEndOfLine[0].start : node.start
let result = startOfTokens
for (let i = startOfTokens - 1; i > 0; i--) {
if (sourceCode.text[i] !== ' ' && sourceCode.text[i] !== '\t') {
break
}
result = i
}
return result
}

function isPlainRequireModule(node) {
if (node.type !== 'VariableDeclaration') {
return false
}
if (node.declarations.length !== 1) {
return false
}
const decl = node.declarations[0]
const result = (decl.id != null && decl.id.type === 'Identifier') &&
decl.init != null &&
decl.init.type === 'CallExpression' &&
decl.init.callee != null &&
decl.init.callee.name === 'require' &&
decl.init.arguments != null &&
decl.init.arguments.length === 1 &&
decl.init.arguments[0].type === 'Literal'
return result
}

function isPlainImportModule(node) {
return node.type === 'ImportDeclaration' && node.specifiers != null && node.specifiers.length > 0
}

function canCrossNodeWhileReorder(node) {
return isPlainRequireModule(node) || isPlainImportModule(node)
}

function canReorderItems(firstNode, secondNode) {
const parent = firstNode.parent
const firstIndex = parent.body.indexOf(firstNode)
const secondIndex = parent.body.indexOf(secondNode)
const nodesBetween = parent.body.slice(firstIndex, secondIndex + 1)
for (var nodeBetween of nodesBetween) {
if (!canCrossNodeWhileReorder(nodeBetween)) {
return false
}
}
return true
}

function fixOutOfOrder(context, firstNode, secondNode, order) {
const sourceCode = context.getSourceCode()

const firstRoot = findRootNode(firstNode.node)
let firstRootStart = findStartOfLineWithComments(sourceCode, firstRoot)
const firstRootEnd = findEndOfLineWithComments(sourceCode, firstRoot)

const secondRoot = findRootNode(secondNode.node)
let secondRootStart = findStartOfLineWithComments(sourceCode, secondRoot)
let secondRootEnd = findEndOfLineWithComments(sourceCode, secondRoot)
const canFix = canReorderItems(firstRoot, secondRoot)

let newCode = sourceCode.text.substring(secondRootStart, secondRootEnd)
if (newCode[newCode.length - 1] !== '\n') {
newCode = newCode + '\n'
}

const message = '`' + secondNode.name + '` import should occur ' + order +
' import of `' + firstNode.name + '`'

if (order === 'before') {
context.report({
node: secondNode.node,
message: message,
fix: canFix && (fixer =>
fixer.replaceTextRange(
[firstRootStart, secondRootEnd],
newCode + sourceCode.text.substring(firstRootStart, secondRootStart)
)),
})
} else if (order === 'after') {
context.report({
node: secondNode.node,
message: message,
fix: canFix && (fixer =>
fixer.replaceTextRange(
[secondRootStart, firstRootEnd],
sourceCode.text.substring(secondRootEnd, firstRootEnd) + newCode
)),
})
}
}

function reportOutOfOrder(context, imported, outOfOrder, order) {
outOfOrder.forEach(function (imp) {
const found = imported.find(function hasHigherRank(importedItem) {
return importedItem.rank > imp.rank
})
context.report(imp.node, '`' + imp.name + '` import should occur ' + order +
' import of `' + found.name + '`')
fixOutOfOrder(context, found, imp, order)
})
}

Expand Down Expand Up @@ -109,6 +291,32 @@ function convertGroupsToRanks(groups) {
}, rankObject)
}

function fixNewLineAfterImport(context, previousImport) {
const prevRoot = findRootNode(previousImport.node)
const tokensToEndOfLine = takeTokensAfterWhile(
context.getSourceCode(), prevRoot, commentOnSameLineAs(prevRoot))

let endOfLine = prevRoot.end
if (tokensToEndOfLine.length > 0) {
endOfLine = tokensToEndOfLine[tokensToEndOfLine.length - 1].end
}
return (fixer) => fixer.insertTextAfterRange([prevRoot.start, endOfLine], '\n')
}

function removeNewLineAfterImport(context, currentImport, previousImport) {
const sourceCode = context.getSourceCode()
const prevRoot = findRootNode(previousImport.node)
const currRoot = findRootNode(currentImport.node)
const rangeToRemove = [
findEndOfLineWithComments(sourceCode, prevRoot),
findStartOfLineWithComments(sourceCode, currRoot),
]
if (/^\s*$/.test(sourceCode.text.substring(rangeToRemove[0], rangeToRemove[1]))) {
return (fixer) => fixer.removeRange(rangeToRemove)
}
return undefined
}

function makeNewlinesBetweenReport (context, imported, newlinesBetweenImports) {
const getNumberOfEmptyLinesBetween = (currentImport, previousImport) => {
const linesBetweenImports = context.getSourceCode().lines.slice(
Expand All @@ -125,23 +333,27 @@ function makeNewlinesBetweenReport (context, imported, newlinesBetweenImports) {

if (newlinesBetweenImports === 'always'
|| newlinesBetweenImports === 'always-and-inside-groups') {
if (currentImport.rank !== previousImport.rank && emptyLinesBetween === 0)
{
context.report(
previousImport.node, 'There should be at least one empty line between import groups'
)
if (currentImport.rank !== previousImport.rank && emptyLinesBetween === 0) {
context.report({
node: previousImport.node,
message: 'There should be at least one empty line between import groups',
fix: fixNewLineAfterImport(context, previousImport, currentImport),
})
} else if (currentImport.rank === previousImport.rank
&& emptyLinesBetween > 0
&& newlinesBetweenImports !== 'always-and-inside-groups')
{
context.report(
previousImport.node, 'There should be no empty line within import group'
)
}
} else {
if (emptyLinesBetween > 0) {
context.report(previousImport.node, 'There should be no empty line between import groups')
&& newlinesBetweenImports !== 'always-and-inside-groups') {
context.report({
node: previousImport.node,
message: 'There should be no empty line within import group',
fix: removeNewLineAfterImport(context, currentImport, previousImport),
})
}
} else if (emptyLinesBetween > 0) {
context.report({
node: previousImport.node,
message: 'There should be no empty line between import groups',
fix: removeNewLineAfterImport(context, currentImport, previousImport),
})
}

previousImport = currentImport
Expand All @@ -154,6 +366,7 @@ module.exports = {
url: docsUrl('order'),
},

fixable: 'code',
schema: [
{
type: 'object',
Expand Down

0 comments on commit b34d9ff

Please sign in to comment.