Skip to content

Commit d9f9a49

Browse files
Assertion methods auto correction for use-t-well rule (#277)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent a217bee commit d9f9a49

File tree

4 files changed

+180
-87
lines changed

4 files changed

+180
-87
lines changed

docs/rules/use-t-well.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Translations: [Français](https://github.com/avajs/ava-docs/blob/master/fr_FR/re
44

55
Prevent the use of unknown assertion methods and the access to members other than the assertion methods and `.context`, as well as some known misuses of `t`.
66

7-
This rule is partly fixable. It will replace misspelled `.falsey` with `.falsy`.
7+
This rule is partly fixable. It can fix most misspelled assertion method names and incorrect usages of `.skip`.
88

99

1010
## Fail
@@ -14,12 +14,13 @@ const test = require('ava');
1414

1515
test('main', t => {
1616
t(value); // `t` is not a function
17-
t.depEqual(value, [2]); // Unknown assertion method `.depEqual`
18-
t.contxt.foo = 100; // Unknown member `.contxt`. Use `.context.contxt` instead
17+
t.depEqual(value, [2]); // Misspelled `.deepEqual` as `.depEqual`, fixable
18+
t.contxt.foo = 100; // Misspelled `.context` as `.contxt`, fixable
19+
t.deepEqual.skip.skip(); // Too many chained uses of `.skip`, fixable
20+
t.skip.deepEqual(1, 1); // `.skip` modifier should be the last in chain, fixable
1921
t.foo = 1000; // Unknown member `.foo`. Use `.context.foo` instead
2022
t.deepEqual.is(value, value); // Can't chain assertion methods
2123
t.skip(); // Missing assertion method
22-
t.deepEqual.skip.skip(); // Too many chained uses of `.skip`
2324
});
2425
```
2526

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"espree": "^6.1.2",
3636
"espurify": "^2.0.1",
3737
"import-modules": "^2.0.0",
38+
"micro-spelling-correcter": "^1.1.1",
3839
"pkg-dir": "^4.2.0",
3940
"resolve-from": "^5.0.0"
4041
},

rules/use-t-well.js

+95-74
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,29 @@
11
'use strict';
22
const {visitIf} = require('enhance-visitors');
3+
const MicroSpellingCorrecter = require('micro-spelling-correcter');
4+
35
const util = require('../util');
46
const createAvaRule = require('../create-ava-rule');
57

6-
const isMethod = name => util.executionMethods.has(name);
8+
const properties = new Set([
9+
...util.executionMethods,
10+
'context',
11+
'title',
12+
'skip'
13+
]);
14+
15+
const correcter = new MicroSpellingCorrecter([...properties]);
716

817
const isCallExpression = node =>
918
node.parent.type === 'CallExpression' &&
1019
node.parent.callee === node;
1120

12-
const getMemberStats = members => {
13-
const initial = {
14-
skip: [],
15-
falsey: [],
16-
method: [],
17-
other: []
18-
};
19-
20-
return members.reduce((res, member) => {
21-
if (member === 'skip') {
22-
res.skip.push(member);
23-
} else if (member === 'falsey') {
24-
res.falsey.push(member);
25-
} else if (isMethod(member)) {
26-
res.method.push(member);
27-
} else {
28-
res.other.push(member);
29-
}
30-
31-
return res;
32-
}, initial);
21+
const getMemberNodes = node => {
22+
if (node.object.type === 'MemberExpression') {
23+
return getMemberNodes(node.object).concat(node.property);
24+
}
25+
26+
return [node.property];
3327
};
3428

3529
const create = context => {
@@ -57,67 +51,94 @@ const create = context => {
5751
return;
5852
}
5953

60-
const members = util.getMembers(node);
61-
const stats = getMemberStats(members);
62-
63-
if (members[0] === 'context') {
64-
// Anything is fine when of the form `t.context...`
65-
if (members.length === 1 && isCallExpression(node)) {
66-
// Except `t.context()`
67-
context.report({
68-
node,
69-
message: 'Unknown assertion method `.context`.'
70-
});
54+
const members = getMemberNodes(node);
55+
56+
const skipPositions = [];
57+
let hadCall = false;
58+
for (const [i, member] of members.entries()) {
59+
const {name} = member;
60+
61+
let corrected = correcter.correct(name);
62+
63+
if (i !== 0 && (corrected === 'context' || corrected === 'title')) { // `context` and `title` can only be first
64+
corrected = undefined;
7165
}
7266

73-
return;
74-
}
67+
if (corrected !== name) {
68+
if (corrected === undefined) {
69+
if (isCallExpression(node)) {
70+
context.report({
71+
node,
72+
message: `Unknown assertion method \`.${name}\`.`
73+
});
74+
} else {
75+
context.report({
76+
node,
77+
message: `Unknown member \`.${name}\`. Use \`.context.${name}\` instead.`
78+
});
79+
}
80+
} else {
81+
context.report({
82+
node,
83+
message: `Misspelled \`.${corrected}\` as \`.${name}\`.`,
84+
fix: fixer => fixer.replaceText(member, corrected)
85+
});
86+
}
7587

76-
if (members[0] === 'title') {
77-
// Anything is fine when of the form `t.title...`
78-
if (members.length === 1 && isCallExpression(node)) {
79-
// Except `t.title()`
80-
context.report({
81-
node,
82-
message: 'Unknown assertion method `.title`.'
83-
});
88+
return; // Don't check further
8489
}
8590

86-
return;
87-
}
91+
if (name === 'context' || name === 'title') {
92+
if (members.length === 1 && isCallExpression(node)) {
93+
context.report({
94+
node,
95+
message: `Unknown assertion method \`.${name}\`.`
96+
});
97+
}
98+
99+
return; // Don't check further
100+
}
88101

89-
if (isCallExpression(node)) {
90-
if (stats.other.length > 0) {
91-
context.report({
92-
node,
93-
message: `Unknown assertion method \`.${stats.other[0]}\`.`
94-
});
95-
} else if (stats.skip.length > 1) {
96-
context.report({
97-
node,
98-
message: 'Too many chained uses of `.skip`.'
99-
});
100-
} else if (stats.falsey.length > 0) {
101-
context.report({
102-
node,
103-
message: 'Misspelled `.falsy` as `.falsey`.',
104-
fix: fixer => fixer.replaceText(node.property, 'falsy')
105-
});
106-
} else if (stats.method.length > 1) {
107-
context.report({
108-
node,
109-
message: 'Can\'t chain assertion methods.'
110-
});
111-
} else if (stats.method.length === 0) {
112-
context.report({
113-
node,
114-
message: 'Missing assertion method.'
115-
});
102+
if (name === 'skip') {
103+
skipPositions.push(i);
104+
} else {
105+
if (hadCall) {
106+
context.report({
107+
node,
108+
message: 'Can\'t chain assertion methods.'
109+
});
110+
}
111+
112+
hadCall = true;
116113
}
117-
} else if (stats.other.length > 0) {
114+
}
115+
116+
if (!hadCall) {
117+
context.report({
118+
node,
119+
message: 'Missing assertion method.'
120+
});
121+
}
122+
123+
if (skipPositions.length > 1) {
124+
context.report({
125+
node,
126+
message: 'Too many chained uses of `.skip`.',
127+
fix: fixer => {
128+
const chain = ['t', ...members.map(member => member.name).filter(name => name !== 'skip'), 'skip'];
129+
return fixer.replaceText(node, chain.join('.'));
130+
}
131+
});
132+
}
133+
134+
if (skipPositions.length === 1 && skipPositions[0] !== members.length - 1) {
118135
context.report({
119136
node,
120-
message: `Unknown member \`.${stats.other[0]}\`. Use \`.context.${stats.other[0]}\` instead.`
137+
message: '`.skip` modifier should be the last in chain.',
138+
fix: fixer => {
139+
const chain = ['t', ...members.map(member => member.name).filter(name => name !== 'skip'), 'skip'];
140+
return fixer.replaceText(node, chain.join('.'));
141+
}
121142
});
122143
}
123144
})

test/use-t-well.js

+79-9
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ ruleTester.run('use-t-well', rule, {
5050
testCase('t.snapshot(v);'),
5151
testCase('t.ifError(error);'),
5252
testCase('t.deepEqual.skip(a, a);'),
53-
testCase('t.skip.deepEqual(a, a);'),
5453
testCase('t.context.a = 1;'),
5554
testCase('t.context.foo.skip();'),
5655
testCase('console.log(t.context);'),
@@ -84,15 +83,13 @@ ruleTester.run('use-t-well', rule, {
8483
},
8584
{
8685
code: testCase('t.depEqual(a, a);'),
87-
errors: [error('Unknown assertion method `.depEqual`.')]
86+
output: testCase('t.deepEqual(a, a);'),
87+
errors: [error('Misspelled `.deepEqual` as `.depEqual`.')]
8888
},
8989
{
9090
code: testCase('t.deepEqual.skp(a, a);'),
91-
errors: [error('Unknown assertion method `.skp`.')]
92-
},
93-
{
94-
code: testCase('t.skp.deepEqual(a, a);'),
95-
errors: [error('Unknown assertion method `.skp`.')]
91+
output: testCase('t.deepEqual.skip(a, a);'),
92+
errors: [error('Misspelled `.skip` as `.skp`.')]
9693
},
9794
{
9895
code: testCase('t.context();'),
@@ -112,28 +109,101 @@ ruleTester.run('use-t-well', rule, {
112109
},
113110
{
114111
code: testCase('t.deepEqu;'),
115-
errors: [error('Unknown member `.deepEqu`. Use `.context.deepEqu` instead.')]
112+
output: testCase('t.deepEqual;'),
113+
errors: [error('Misspelled `.deepEqual` as `.deepEqu`.')]
116114
},
117115
{
118116
code: testCase('t.deepEqual.is(a, a);'),
119117
errors: [error('Can\'t chain assertion methods.')]
120118
},
121119
{
122120
code: testCase('t.paln(1);'),
123-
errors: [error('Unknown assertion method `.paln`.')]
121+
output: testCase('t.plan(1);'),
122+
errors: [error('Misspelled `.plan` as `.paln`.')]
124123
},
125124
{
126125
code: testCase('t.skip();'),
127126
errors: [error('Missing assertion method.')]
128127
},
129128
{
130129
code: testCase('t.deepEqual.skip.skip(a, a);'),
130+
output: testCase('t.deepEqual.skip(a, a);'),
131131
errors: [error('Too many chained uses of `.skip`.')]
132132
},
133133
{
134134
code: testCase('t.falsey(a);'),
135135
output: testCase('t.falsy(a);'),
136136
errors: [error('Misspelled `.falsy` as `.falsey`.')]
137+
},
138+
{
139+
code: testCase('t.truthey(a);'),
140+
output: testCase('t.truthy(a);'),
141+
errors: [error('Misspelled `.truthy` as `.truthey`.')]
142+
},
143+
{
144+
code: testCase('t.deepequal(a, {});'),
145+
output: testCase('t.deepEqual(a, {});'),
146+
errors: [error('Misspelled `.deepEqual` as `.deepequal`.')]
147+
},
148+
{
149+
code: testCase('t.contxt;'),
150+
output: testCase('t.context;'),
151+
errors: [error('Misspelled `.context` as `.contxt`.')]
152+
},
153+
{
154+
code: testCase('t.notdeepEqual(a, {});'),
155+
output: testCase('t.notDeepEqual(a, {});'),
156+
errors: [error('Misspelled `.notDeepEqual` as `.notdeepEqual`.')]
157+
},
158+
{
159+
code: testCase('t.throw(a);'),
160+
output: testCase('t.throws(a);'),
161+
errors: [error('Misspelled `.throws` as `.throw`.')]
162+
},
163+
{
164+
code: testCase('t.notThrow(a);'),
165+
output: testCase('t.notThrows(a);'),
166+
errors: [error('Misspelled `.notThrows` as `.notThrow`.')]
167+
},
168+
{
169+
code: testCase('t.throwAsync(a);'),
170+
output: testCase('t.throwsAsync(a);'),
171+
errors: [error('Misspelled `.throwsAsync` as `.throwAsync`.')]
172+
},
173+
{
174+
code: testCase('t.notthrowAsync(a);'),
175+
output: testCase('t.notThrowsAsync(a);'),
176+
errors: [error('Misspelled `.notThrowsAsync` as `.notthrowAsync`.')]
177+
},
178+
{
179+
code: testCase('t.regexp(a, /r/);'),
180+
output: testCase('t.regex(a, /r/);'),
181+
errors: [error('Misspelled `.regex` as `.regexp`.')]
182+
},
183+
{
184+
code: testCase('t.notregexp(a, /r/);'),
185+
output: testCase('t.notRegex(a, /r/);'),
186+
errors: [error('Misspelled `.notRegex` as `.notregexp`.')]
187+
},
188+
{
189+
code: testCase('t.contxt.foo = 1;'),
190+
output: testCase('t.context.foo = 1;'),
191+
errors: [error('Misspelled `.context` as `.contxt`.')]
192+
},
193+
194+
{
195+
code: testCase('t.skip.deepEqual(a, a);'),
196+
output: testCase('t.deepEqual.skip(a, a);'),
197+
errors: [error('`.skip` modifier should be the last in chain.')]
198+
},
199+
{
200+
code: testCase('t.skp.deepEqual(a, a);'),
201+
output: testCase('t.skip.deepEqual(a, a);'),
202+
errors: [error('Misspelled `.skip` as `.skp`.')]
203+
},
204+
{
205+
code: testCase('t.deepEqual.context(a, a);'),
206+
errors: [error('Unknown assertion method `.context`.')]
137207
}
138208
]
139209
});

0 commit comments

Comments
 (0)