Skip to content

Commit 1561b35

Browse files
committed
Add prefer-t-throws rule
Fixes: #156
1 parent 33dbbc7 commit 1561b35

File tree

5 files changed

+267
-0
lines changed

5 files changed

+267
-0
lines changed

docs/rules/prefer-t-throws.md

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Prefer using `t.throws()` or `t.throwsAsync()` over try/catch
2+
3+
This rule will enforce the use of `t.throws()` or `t.throwsAsync()` when possible.
4+
5+
## Fail
6+
7+
```js
8+
const test = require('ava');
9+
10+
test('some test', async t => {
11+
try {
12+
await throwingFunction();
13+
t.fail();
14+
} catch (error) {
15+
t.is(error.message, 'Unicorn overload');
16+
}
17+
});
18+
```
19+
20+
```js
21+
const test = require('ava');
22+
23+
test('some test', async t => {
24+
try {
25+
await potentiallyThrowingFunction();
26+
await anotherPromise;
27+
await timeout(100, 'Unicorn timeout');
28+
t.fail();
29+
} catch (error) {
30+
t.ok(error.message.startsWith('Unicorn'));
31+
}
32+
});
33+
```
34+
35+
```js
36+
const test = require('ava');
37+
38+
test('some test', async t => {
39+
try {
40+
synchronousThrowingFunction();
41+
t.fail();
42+
} catch (error) {
43+
t.is(error.message, 'Missing Unicorn argument');
44+
}
45+
});
46+
```
47+
48+
## Pass
49+
50+
```js
51+
const test = require('ava');
52+
53+
test('some test', async t => {
54+
await t.throwsAsync(asyncThrowingFunction(), {message: 'Unicorn overload'});
55+
});
56+
```

index.js

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ module.exports = {
4141
'ava/prefer-async-await': 'error',
4242
'ava/prefer-power-assert': 'off',
4343
'ava/prefer-t-regex': 'error',
44+
'ava/prefer-t-throws': 'error',
4445
'ava/test-title': 'error',
4546
'ava/test-title-format': 'off',
4647
'ava/use-t-well': 'error',

readme.md

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ Configure it in `package.json`.
5656
"ava/prefer-async-await": "error",
5757
"ava/prefer-power-assert": "off",
5858
"ava/prefer-t-regex": "error",
59+
"ava/prefer-t-throws": "error",
5960
"ava/test-title": "error",
6061
"ava/test-title-format": "off",
6162
"ava/use-t": "error",
@@ -92,6 +93,7 @@ The rules will only activate in test files.
9293
- [prefer-async-await](docs/rules/prefer-async-await.md) - Prefer using async/await instead of returning a Promise.
9394
- [prefer-power-assert](docs/rules/prefer-power-assert.md) - Allow only use of the asserts that have no [power-assert](https://github.com/power-assert-js/power-assert) alternative.
9495
- [prefer-t-regex](docs/rules/prefer-t-regex.md) - Prefer using `t.regex()` to test regular expressions. *(fixable)*
96+
- [prefer-t-throws](docs/rules/prefer-t-throws.md) - Prefer using `t.throws()` or `t.throwsAsync()` over try/catch.
9597
- [test-title](docs/rules/test-title.md) - Ensure tests have a title.
9698
- [test-title-format](docs/rules/test-title-format.md) - Ensure test titles have a certain format.
9799
- [use-t](docs/rules/use-t.md) - Ensure test functions use `t` as their parameter.

rules/prefer-t-throws.js

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
'use strict';
2+
3+
const {visitIf} = require('enhance-visitors');
4+
const createAvaRule = require('../create-ava-rule');
5+
const util = require('../util');
6+
7+
// This function checks if there is an AwaitExpression, which is not inside another function.
8+
//
9+
// TODO: find a simpler way to do this
10+
function hasAwaitExpression(nodes) {
11+
if (!nodes) {
12+
return false;
13+
}
14+
15+
for (const node of nodes) {
16+
if (!node) {
17+
continue;
18+
}
19+
20+
if (node.type === 'ExpressionStatement' && hasAwaitExpression([node.expression])) {
21+
return true;
22+
}
23+
24+
if (node.type === 'AwaitExpression') {
25+
return true;
26+
}
27+
28+
if (node.expressions && hasAwaitExpression(node.expressions)) {
29+
return true;
30+
}
31+
32+
if (node.type === 'BlockStatement' && hasAwaitExpression(node.body)) {
33+
return true;
34+
}
35+
36+
if (node.type === 'MemberExpression' && hasAwaitExpression([node.object, node.property])) {
37+
return true;
38+
}
39+
40+
if ((node.type === 'CallExpression' || node.type === 'NewExpression')
41+
&& hasAwaitExpression([...node.arguments, node.callee])) {
42+
return true;
43+
}
44+
45+
if (node.left && node.right && hasAwaitExpression([node.left, node.right])) {
46+
return true;
47+
}
48+
49+
if (node.type === 'SequenceExpression' && hasAwaitExpression(node.expressions)) {
50+
return true;
51+
}
52+
53+
if (node.type === 'VariableDeclaration'
54+
&& hasAwaitExpression(node.declarations.map(declaration => declaration.init))) {
55+
return true;
56+
}
57+
58+
if (node.type === 'ThrowStatement' && hasAwaitExpression([node.argument])) {
59+
return true;
60+
}
61+
62+
if (node.type === 'IfStatement' && hasAwaitExpression([node.test, node.consequent, node.alternate])) {
63+
return true;
64+
}
65+
66+
if (node.type === 'SwitchStatement'
67+
// eslint-disable-next-line unicorn/prefer-spread
68+
&& hasAwaitExpression([node.discriminant, ...node.cases.flatMap(caseNode => [caseNode.test].concat(caseNode.consequent))])) {
69+
return true;
70+
}
71+
72+
if (node.type.endsWith('WhileStatement') && hasAwaitExpression([node.test, node.body])) {
73+
return true;
74+
}
75+
76+
if (node.type === 'ForStatement' && hasAwaitExpression([node.init, node.test, node.update, node.body])) {
77+
return true;
78+
}
79+
80+
if (node.type === 'ForInStatement' && hasAwaitExpression([node.right, node.body])) {
81+
return true;
82+
}
83+
84+
if (node.type === 'ForOfStatement' && (node.await || hasAwaitExpression([node.right, node.body]))) {
85+
return true;
86+
}
87+
88+
if (node.type === 'WithStatement' && hasAwaitExpression([node.object, node.body])) {
89+
return true;
90+
}
91+
}
92+
93+
return false;
94+
}
95+
96+
const create = context => {
97+
const ava = createAvaRule();
98+
99+
return ava.merge({
100+
TryStatement: visitIf([
101+
ava.isInTestFile,
102+
ava.isInTestNode,
103+
])(node => {
104+
const nodes = node.block.body;
105+
if (nodes.length < 2) {
106+
return;
107+
}
108+
109+
const tFailIndex = [...nodes].reverse().findIndex(node => node.type === 'ExpressionStatement'
110+
&& node.expression.type === 'CallExpression'
111+
&& node.expression.callee.object
112+
&& node.expression.callee.object.name === 't'
113+
&& node.expression.callee.property
114+
&& node.expression.callee.property.name === 'fail');
115+
116+
// Return if there is no t.fail() or if it's the first node
117+
if (tFailIndex === -1 || tFailIndex === nodes.length - 1) {
118+
return;
119+
}
120+
121+
const beforeNodes = nodes.slice(0, nodes.length - 1 - tFailIndex);
122+
123+
context.report({
124+
node,
125+
message: `Prefer using the \`t.throws${hasAwaitExpression(beforeNodes) ? 'Async' : ''}()\` assertion.`,
126+
});
127+
}),
128+
});
129+
};
130+
131+
module.exports = {
132+
create,
133+
meta: {
134+
type: 'suggestion',
135+
docs: {
136+
url: util.getDocsUrl(__filename),
137+
},
138+
schema: [],
139+
},
140+
};

test/prefer-t-throws.js

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use strict';
2+
3+
const test = require('ava');
4+
const avaRuleTester = require('eslint-ava-rule-tester');
5+
const rule = require('../rules/prefer-t-throws');
6+
7+
const ruleTester = avaRuleTester(test, {
8+
parserOptions: {
9+
ecmaVersion: 'latest',
10+
},
11+
});
12+
13+
const header = 'const test = require(\'ava\');\n';
14+
15+
ruleTester.run('prefer-t-throws', rule, {
16+
valid: [
17+
`${header}test(async t => { const error = await t.throwsAsync(promise); t.is(error, 'error'); });`,
18+
`${header}test(t => { const error = t.throws(fn()); t.is(error, 'error'); });`,
19+
`${header}test(async t => { try { t.fail(); unicorn(); } catch (error) { t.is(error, 'error'); } });`,
20+
`${header}test(async t => { try { await promise; } catch (error) { t.is(error, 'error'); } });`,
21+
],
22+
invalid: [
23+
{
24+
code: `${header}test(async t => { try { async function unicorn() { throw await Promise.resolve('error') }; unicorn(); t.fail(); } catch (error) { t.is(error, 'error'); } });`,
25+
errors: [{message: 'Prefer using the `t.throws()` assertion.'}],
26+
},
27+
{
28+
code: `${header}test(async t => { try { await Promise.reject('error'); t.fail(); } catch (error) { t.is(error, 'error'); } });`,
29+
errors: [{message: 'Prefer using the `t.throwsAsync()` assertion.'}],
30+
},
31+
{
32+
code: `${header}test(async t => { try { if (await promise); t.fail(); } catch (error) { t.is(error, 'error'); } });`,
33+
errors: [{message: 'Prefer using the `t.throwsAsync()` assertion.'}],
34+
},
35+
{
36+
code: `${header}test(async t => { try { (await 1) > 2; t.fail(); } catch (error) { t.is(error, 'error'); } });`,
37+
errors: [{message: 'Prefer using the `t.throwsAsync()` assertion.'}],
38+
},
39+
{
40+
code: `${header}test(async t => { try { (await getArray())[0]; t.fail(); } catch (error) { t.is(error, 'error'); } });`,
41+
errors: [{message: 'Prefer using the `t.throwsAsync()` assertion.'}],
42+
},
43+
{
44+
code: `${header}test(async t => { try { getArraySync(await 20)[0]; t.fail(); } catch (error) { t.is(error, 'error'); } });`,
45+
errors: [{message: 'Prefer using the `t.throwsAsync()` assertion.'}],
46+
},
47+
{
48+
code: `${header}test(async t => { try { getArraySync()[await 0]; t.fail(); } catch (error) { t.is(error, 'error'); } });`,
49+
errors: [{message: 'Prefer using the `t.throwsAsync()` assertion.'}],
50+
},
51+
{
52+
code: `${header}test(async t => { try { new (await cl())(1); t.fail(); } catch (error) { t.is(error, 'error'); } });`,
53+
errors: [{message: 'Prefer using the `t.throwsAsync()` assertion.'}],
54+
},
55+
{
56+
code: `${header}test(async t => { try { if (false) { await promise; }; t.fail(); } catch (error) { t.is(error, 'error'); } });`,
57+
errors: [{message: 'Prefer using the `t.throwsAsync()` assertion.'}],
58+
},
59+
{
60+
code: `${header}test(t => { try { undefined(); t.fail(); } catch (error) { t.ok(error instanceof TypeError); } });`,
61+
errors: [{message: 'Prefer using the `t.throws()` assertion.'}],
62+
},
63+
{
64+
code: `${header}test(async t => { try { undefined(); t.fail(); } catch (error) { t.ok(error instanceof TypeError); } });`,
65+
errors: [{message: 'Prefer using the `t.throws()` assertion.'}],
66+
},
67+
],
68+
});

0 commit comments

Comments
 (0)