-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
- Loading branch information
1 parent
b829dbc
commit 348c4e8
Showing
6 changed files
with
964 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
# Enforce test hook ordering | ||
|
||
Hooks should be placed before any tests and in the proper semantic order: | ||
|
||
- `test.before(…);` | ||
- `test.after(…);` | ||
- `test.after.always(…);` | ||
- `test.beforeEach(…);` | ||
- `test.afterEach(…);` | ||
- `test.afterEach.always(…);` | ||
- `test(…);` | ||
|
||
This rule is fixable as long as no other code is between the hooks that need to be reordered. | ||
|
||
|
||
## Fail | ||
|
||
```js | ||
import test from 'ava'; | ||
|
||
test.after(t => { | ||
doFoo(); | ||
}); | ||
|
||
test.before(t => { | ||
doFoo(); | ||
}); | ||
|
||
test('foo', t => { | ||
t.true(true); | ||
}); | ||
``` | ||
|
||
```js | ||
import test from 'ava'; | ||
|
||
test('foo', t => { | ||
t.true(true); | ||
}); | ||
|
||
test.before(t => { | ||
doFoo(); | ||
}); | ||
``` | ||
|
||
|
||
## Pass | ||
|
||
```js | ||
import test from 'ava'; | ||
|
||
test.before(t => { | ||
doFoo(); | ||
}); | ||
|
||
test.after(t => { | ||
doFoo(); | ||
}); | ||
|
||
test.after.always(t => { | ||
doFoo(); | ||
}); | ||
|
||
test.beforeEach(t => { | ||
doFoo(); | ||
}); | ||
|
||
test.afterEach(t => { | ||
doFoo(); | ||
}); | ||
|
||
test.afterEach.always(t => { | ||
doFoo(); | ||
}); | ||
|
||
test('foo', t => { | ||
t.true(true); | ||
}); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
'use strict'; | ||
const {visitIf} = require('enhance-visitors'); | ||
const createAvaRule = require('../create-ava-rule'); | ||
const util = require('../util'); | ||
|
||
const MESSAGE_ID = 'hooks-order'; | ||
|
||
const buildOrders = names => { | ||
const orders = {}; | ||
for (const nameLater of names) { | ||
for (const nameEarlier in orders) { | ||
if (orders[nameEarlier]) { | ||
orders[nameEarlier].push(nameLater); | ||
} | ||
} | ||
|
||
orders[nameLater] = []; | ||
} | ||
|
||
return orders; | ||
}; | ||
|
||
const buildMessage = (name, orders, visited) => { | ||
const checks = orders[name] || []; | ||
|
||
for (const check of checks) { | ||
const nodeEarlier = visited[check]; | ||
if (nodeEarlier) { | ||
return { | ||
messageId: MESSAGE_ID, | ||
data: { | ||
current: name, | ||
invalid: check | ||
}, | ||
node: nodeEarlier | ||
}; | ||
} | ||
} | ||
|
||
return null; | ||
}; | ||
|
||
const create = context => { | ||
const ava = createAvaRule(); | ||
|
||
const orders = buildOrders([ | ||
'before', | ||
'after', | ||
'after.always', | ||
'beforeEach', | ||
'afterEach', | ||
'afterEach.always', | ||
'test' | ||
]); | ||
|
||
const visited = {}; | ||
|
||
const checks = [ | ||
{ | ||
selector: 'CallExpression[callee.object.name="test"][callee.property.name="before"]', | ||
name: 'before' | ||
}, | ||
{ | ||
selector: 'CallExpression[callee.object.name="test"][callee.property.name="after"]', | ||
name: 'after' | ||
}, | ||
{ | ||
selector: 'CallExpression[callee.object.object.name="test"][callee.object.property.name="after"][callee.property.name="always"]', | ||
name: 'after.always' | ||
}, | ||
{ | ||
selector: 'CallExpression[callee.object.name="test"][callee.property.name="beforeEach"]', | ||
name: 'beforeEach' | ||
}, | ||
{ | ||
selector: 'CallExpression[callee.object.name="test"][callee.property.name="afterEach"]', | ||
name: 'afterEach' | ||
}, | ||
{ | ||
selector: 'CallExpression[callee.object.object.name="test"][callee.object.property.name="afterEach"][callee.property.name="always"]', | ||
name: 'afterEach.always' | ||
}, | ||
{ | ||
selector: 'CallExpression[callee.name="test"]', | ||
name: 'test' | ||
} | ||
]; | ||
|
||
const sourceCode = context.getSourceCode(); | ||
|
||
const selectors = checks.reduce((result, check) => { | ||
result[check.selector] = visitIf([ | ||
ava.isInTestFile, | ||
ava.isTestNode | ||
])(node => { | ||
visited[check.name] = node; | ||
|
||
const message = buildMessage(check.name, orders, visited); | ||
if (message) { | ||
const nodeEarlier = message.node; | ||
|
||
context.report({ | ||
node, | ||
messageId: message.messageId, | ||
data: message.data, | ||
fix: fixer => { | ||
const tokensBetween = sourceCode.getTokensBetween(nodeEarlier.parent, node.parent); | ||
|
||
if (tokensBetween && tokensBetween.length > 0) { | ||
return; | ||
} | ||
|
||
const source = sourceCode.getText(); | ||
let [insertStart, insertEnd] = nodeEarlier.parent.range; | ||
|
||
// Grab the node and all comments and whitespace before the node | ||
const start = nodeEarlier.parent.range[1]; | ||
const end = node.parent.range[1]; | ||
|
||
let text = sourceCode.getText().substring(start, end); | ||
|
||
// Preserve newline previously between hooks | ||
if (source.length >= (start + 1) && source[start + 1] === '\n') { | ||
text = text.substring(1) + '\n'; | ||
} | ||
|
||
// Preserve newline that was previously before hooks | ||
if ((insertStart - 1) > 0 && source[insertStart - 1] === '\n') { | ||
insertStart -= 1; | ||
} | ||
|
||
return [ | ||
fixer.insertTextBeforeRange([insertStart, insertEnd], text), | ||
fixer.removeRange([start, end]) | ||
]; | ||
} | ||
}); | ||
} | ||
}); | ||
return result; | ||
}, {}); | ||
|
||
return ava.merge(selectors); | ||
}; | ||
|
||
module.exports = { | ||
create, | ||
meta: { | ||
docs: { | ||
url: util.getDocsUrl(__filename) | ||
}, | ||
messages: { | ||
[MESSAGE_ID]: '`{{current}}` hook must come before `{{invalid}}`' | ||
}, | ||
type: 'suggestion', | ||
fixable: 'code' | ||
} | ||
}; |
Oops, something went wrong.