Skip to content

Commit

Permalink
add fixer for order rule
Browse files Browse the repository at this point in the history
  • Loading branch information
tihonove committed Aug 2, 2017
1 parent dd28130 commit 31def14
Show file tree
Hide file tree
Showing 4 changed files with 578 additions and 22 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel

## [Unreleased]

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

## [2.7.0] - 2017-07-06
### Changed
Expand Down
4 changes: 3 additions & 1 deletion docs/rules/order.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# 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 "builtins"
Expand Down
198 changes: 181 additions & 17 deletions src/rules/order.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import isStaticRequire from '../core/staticRequire'

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

// REPORTING
// REPORTING AND FIXING

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

function takeTokensAfterWhile(sourceCode, node, condition) {
const tokens = sourceCode.getTokensAfter(node, { count: 100, includeComments: true })
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 = sourceCode.getTokensBefore(node, { count: 100, includeComments: true })
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 @@ -31,13 +59,116 @@ function findOutOfOrder(imported) {
})
}

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

function findStartOfLine(text, position) {
for(let i = position; i >= 0; i--) {
if (text[i] === '\n') {
return i + 1
}
}
return 0
}

function findEndOfLine(text, position) {
for(let i = position; i < text.length; i++) {
if (text[i] === '\n') {
return i + 1
}
}
return text.length
}

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 => ['Block', 'Line'].includes(token.type) &&
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 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)

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: fixer => [
fixer.removeRange([ secondRootStart, secondRootEnd ]),
fixer.insertTextBeforeRange([firstRootStart, firstRootEnd], newCode),
],
})
} else if (order === 'after') {
context.report({
node: secondNode.node,
message: message,
fix: fixer => [
fixer.removeRange([ secondRootStart, secondRootEnd ]),
fixer.insertTextAfterRange([firstRootStart, 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 @@ -108,6 +239,34 @@ function convertGroupsToRanks(groups) {
}, rankObject)
}

function fixNewLineAfterImport(context, previousImport) {
const prevRoot = findRootNode(previousImport.node)
const tokensToEndOfLine = takeTokensAfterWhile(context, prevRoot,
x => ['Block', 'Line'].includes(x.type) &&
x.loc.start.line === x.loc.end.line &&
x.loc.end.line ===prevRoot.loc.end.line)

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 sourceCodeText = context.getSourceCode().text
const prevRoot = findRootNode(previousImport.node)
const currRoot = findRootNode(currentImport.node)
const rangeToRemove = [
findEndOfLine(sourceCodeText, prevRoot.end),
findStartOfLine(sourceCodeText, currRoot.start),
]
if (/^\s*$/.test(sourceCodeText.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 @@ -124,23 +283,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 @@ -151,6 +314,7 @@ module.exports = {
meta: {
docs: {},

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

0 comments on commit 31def14

Please sign in to comment.