-
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.
Fixes: #156
- Loading branch information
Showing
5 changed files
with
267 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,56 @@ | ||
# Prefer using `t.throws()` or `t.throwsAsync()` over try/catch | ||
|
||
This rule will enforce the use of `t.throws()` or `t.throwsAsync()` when possible. | ||
|
||
## Fail | ||
|
||
```js | ||
const test = require('ava'); | ||
|
||
test('some test', async t => { | ||
try { | ||
await throwingFunction(); | ||
t.fail(); | ||
} catch (error) { | ||
t.is(error.message, 'Unicorn overload'); | ||
} | ||
}); | ||
``` | ||
|
||
```js | ||
const test = require('ava'); | ||
|
||
test('some test', async t => { | ||
try { | ||
await potentiallyThrowingFunction(); | ||
await anotherPromise; | ||
await timeout(100, 'Unicorn timeout'); | ||
t.fail(); | ||
} catch (error) { | ||
t.ok(error.message.startsWith('Unicorn')); | ||
} | ||
}); | ||
``` | ||
|
||
```js | ||
const test = require('ava'); | ||
|
||
test('some test', async t => { | ||
try { | ||
synchronousThrowingFunction(); | ||
t.fail(); | ||
} catch (error) { | ||
t.is(error.message, 'Missing Unicorn argument'); | ||
} | ||
}); | ||
``` | ||
|
||
## Pass | ||
|
||
```js | ||
const test = require('ava'); | ||
|
||
test('some test', async t => { | ||
await t.throwsAsync(asyncThrowingFunction(), {message: 'Unicorn overload'}); | ||
}); | ||
``` |
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,140 @@ | ||
'use strict'; | ||
|
||
const {visitIf} = require('enhance-visitors'); | ||
const createAvaRule = require('../create-ava-rule'); | ||
const util = require('../util'); | ||
|
||
// This function checks if there is an AwaitExpression, which is not inside another function. | ||
// | ||
// TODO: find a simpler way to do this | ||
function hasAwaitExpression(nodes) { | ||
if (!nodes) { | ||
return false; | ||
} | ||
|
||
for (const node of nodes) { | ||
if (!node) { | ||
continue; | ||
} | ||
|
||
if (node.type === 'ExpressionStatement' && hasAwaitExpression([node.expression])) { | ||
return true; | ||
} | ||
|
||
if (node.type === 'AwaitExpression') { | ||
return true; | ||
} | ||
|
||
if (node.expressions && hasAwaitExpression(node.expressions)) { | ||
return true; | ||
} | ||
|
||
if (node.type === 'BlockStatement' && hasAwaitExpression(node.body)) { | ||
return true; | ||
} | ||
|
||
if (node.type === 'MemberExpression' && hasAwaitExpression([node.object, node.property])) { | ||
return true; | ||
} | ||
|
||
if ((node.type === 'CallExpression' || node.type === 'NewExpression') | ||
&& hasAwaitExpression([...node.arguments, node.callee])) { | ||
return true; | ||
} | ||
|
||
if (node.left && node.right && hasAwaitExpression([node.left, node.right])) { | ||
return true; | ||
} | ||
|
||
if (node.type === 'SequenceExpression' && hasAwaitExpression(node.expressions)) { | ||
return true; | ||
} | ||
|
||
if (node.type === 'VariableDeclaration' | ||
&& hasAwaitExpression(node.declarations.map(declaration => declaration.init))) { | ||
return true; | ||
} | ||
|
||
if (node.type === 'ThrowStatement' && hasAwaitExpression([node.argument])) { | ||
return true; | ||
} | ||
|
||
if (node.type === 'IfStatement' && hasAwaitExpression([node.test, node.consequent, node.alternate])) { | ||
return true; | ||
} | ||
|
||
if (node.type === 'SwitchStatement' | ||
// eslint-disable-next-line unicorn/prefer-spread | ||
&& hasAwaitExpression([node.discriminant, ...node.cases.flatMap(caseNode => [caseNode.test].concat(caseNode.consequent))])) { | ||
return true; | ||
} | ||
|
||
if (node.type.endsWith('WhileStatement') && hasAwaitExpression([node.test, node.body])) { | ||
return true; | ||
} | ||
|
||
if (node.type === 'ForStatement' && hasAwaitExpression([node.init, node.test, node.update, node.body])) { | ||
return true; | ||
} | ||
|
||
if (node.type === 'ForInStatement' && hasAwaitExpression([node.right, node.body])) { | ||
return true; | ||
} | ||
|
||
if (node.type === 'ForOfStatement' && (node.await || hasAwaitExpression([node.right, node.body]))) { | ||
return true; | ||
} | ||
|
||
if (node.type === 'WithStatement' && hasAwaitExpression([node.object, node.body])) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
const create = context => { | ||
const ava = createAvaRule(); | ||
|
||
return ava.merge({ | ||
TryStatement: visitIf([ | ||
ava.isInTestFile, | ||
ava.isInTestNode, | ||
])(node => { | ||
const nodes = node.block.body; | ||
if (nodes.length < 2) { | ||
return; | ||
} | ||
|
||
const tFailIndex = [...nodes].reverse().findIndex(node => node.type === 'ExpressionStatement' | ||
&& node.expression.type === 'CallExpression' | ||
&& node.expression.callee.object | ||
&& node.expression.callee.object.name === 't' | ||
&& node.expression.callee.property | ||
&& node.expression.callee.property.name === 'fail'); | ||
|
||
// Return if there is no t.fail() or if it's the first node | ||
if (tFailIndex === -1 || tFailIndex === nodes.length - 1) { | ||
return; | ||
} | ||
|
||
const beforeNodes = nodes.slice(0, nodes.length - 1 - tFailIndex); | ||
|
||
context.report({ | ||
node, | ||
message: `Prefer using the \`t.throws${hasAwaitExpression(beforeNodes) ? 'Async' : ''}()\` assertion.`, | ||
}); | ||
}), | ||
}); | ||
}; | ||
|
||
module.exports = { | ||
create, | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
url: util.getDocsUrl(__filename), | ||
}, | ||
schema: [], | ||
}, | ||
}; |
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,68 @@ | ||
'use strict'; | ||
|
||
const test = require('ava'); | ||
const avaRuleTester = require('eslint-ava-rule-tester'); | ||
const rule = require('../rules/prefer-t-throws'); | ||
|
||
const ruleTester = avaRuleTester(test, { | ||
parserOptions: { | ||
ecmaVersion: 'latest', | ||
}, | ||
}); | ||
|
||
const header = 'const test = require(\'ava\');\n'; | ||
|
||
ruleTester.run('prefer-t-throws', rule, { | ||
valid: [ | ||
`${header}test(async t => { const error = await t.throwsAsync(promise); t.is(error, 'error'); });`, | ||
`${header}test(t => { const error = t.throws(fn()); t.is(error, 'error'); });`, | ||
`${header}test(async t => { try { t.fail(); unicorn(); } catch (error) { t.is(error, 'error'); } });`, | ||
`${header}test(async t => { try { await promise; } catch (error) { t.is(error, 'error'); } });`, | ||
], | ||
invalid: [ | ||
{ | ||
code: `${header}test(async t => { try { async function unicorn() { throw await Promise.resolve('error') }; unicorn(); t.fail(); } catch (error) { t.is(error, 'error'); } });`, | ||
errors: [{message: 'Prefer using the `t.throws()` assertion.'}], | ||
}, | ||
{ | ||
code: `${header}test(async t => { try { await Promise.reject('error'); t.fail(); } catch (error) { t.is(error, 'error'); } });`, | ||
errors: [{message: 'Prefer using the `t.throwsAsync()` assertion.'}], | ||
}, | ||
{ | ||
code: `${header}test(async t => { try { if (await promise); t.fail(); } catch (error) { t.is(error, 'error'); } });`, | ||
errors: [{message: 'Prefer using the `t.throwsAsync()` assertion.'}], | ||
}, | ||
{ | ||
code: `${header}test(async t => { try { (await 1) > 2; t.fail(); } catch (error) { t.is(error, 'error'); } });`, | ||
errors: [{message: 'Prefer using the `t.throwsAsync()` assertion.'}], | ||
}, | ||
{ | ||
code: `${header}test(async t => { try { (await getArray())[0]; t.fail(); } catch (error) { t.is(error, 'error'); } });`, | ||
errors: [{message: 'Prefer using the `t.throwsAsync()` assertion.'}], | ||
}, | ||
{ | ||
code: `${header}test(async t => { try { getArraySync(await 20)[0]; t.fail(); } catch (error) { t.is(error, 'error'); } });`, | ||
errors: [{message: 'Prefer using the `t.throwsAsync()` assertion.'}], | ||
}, | ||
{ | ||
code: `${header}test(async t => { try { getArraySync()[await 0]; t.fail(); } catch (error) { t.is(error, 'error'); } });`, | ||
errors: [{message: 'Prefer using the `t.throwsAsync()` assertion.'}], | ||
}, | ||
{ | ||
code: `${header}test(async t => { try { new (await cl())(1); t.fail(); } catch (error) { t.is(error, 'error'); } });`, | ||
errors: [{message: 'Prefer using the `t.throwsAsync()` assertion.'}], | ||
}, | ||
{ | ||
code: `${header}test(async t => { try { if (false) { await promise; }; t.fail(); } catch (error) { t.is(error, 'error'); } });`, | ||
errors: [{message: 'Prefer using the `t.throwsAsync()` assertion.'}], | ||
}, | ||
{ | ||
code: `${header}test(t => { try { undefined(); t.fail(); } catch (error) { t.ok(error instanceof TypeError); } });`, | ||
errors: [{message: 'Prefer using the `t.throws()` assertion.'}], | ||
}, | ||
{ | ||
code: `${header}test(async t => { try { undefined(); t.fail(); } catch (error) { t.ok(error instanceof TypeError); } });`, | ||
errors: [{message: 'Prefer using the `t.throws()` assertion.'}], | ||
}, | ||
], | ||
}); |