Skip to content

Commit eb676f6

Browse files
committed
spelling correction based implementation, solves #244
1 parent 47e43d1 commit eb676f6

File tree

2 files changed

+192
-79
lines changed

2 files changed

+192
-79
lines changed

rules/use-t-well.js

Lines changed: 137 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,100 @@ const {visitIf} = require('enhance-visitors');
33
const util = require('../util');
44
const createAvaRule = require('../create-ava-rule');
55

6-
const isMethod = name => util.executionMethods.has(name);
6+
class MicroCorrecter {
7+
constructor(words) {
8+
this.words = new Set(words);
9+
10+
const letters = new Set();
11+
words.forEach(word => word.split('').forEach(letter => letters.add(letter)));
12+
this.letters = [...letters];
13+
}
14+
15+
edits(word) {
16+
const edits = [];
17+
const {length} = word;
18+
const {letters} = this;
19+
20+
for (let i = 0; i < length; i++) {
21+
edits.push(word.slice(0, i) + word.slice(i + 1)); // Skip
22+
for (const letter of letters) {
23+
edits.push(word.slice(0, i) + letter + word.slice(i + 1)); // Replace
24+
}
25+
}
26+
27+
for (let i = 1; i < length; i++) {
28+
edits.push(word.slice(0, i - 1) + word[i] + word[i - 1] + word.slice(i + 1)); // Transposition
29+
}
30+
31+
for (let i = 0; i <= length; i++) {
32+
for (const letter of letters) {
33+
edits.push(word.slice(0, i) + letter + word.slice(i)); // Addition
34+
}
35+
}
36+
37+
return edits;
38+
}
39+
40+
correct(word, distance) {
41+
const {words} = this;
42+
43+
if (words.has(word)) {
44+
return word;
45+
}
46+
47+
if (distance > 0) {
48+
const edits = this.edits(word);
49+
50+
for (const edit of edits) {
51+
if (words.has(edit)) {
52+
return edit;
53+
}
54+
}
55+
56+
if (distance > 1) {
57+
for (const edit of edits) {
58+
const correction = this.correct(edit, distance - 1);
59+
if (correction !== undefined) {
60+
return correction;
61+
}
62+
}
63+
}
64+
}
65+
}
66+
}
67+
68+
const nonMethods = new Set([
69+
'context',
70+
'title'
71+
]);
72+
73+
const properties = new Set([
74+
...nonMethods,
75+
...util.executionMethods,
76+
'skip'
77+
]);
78+
79+
const correcter = new MicroCorrecter([...properties]);
780

881
const isCallExpression = node =>
982
node.parent.type === 'CallExpression' &&
1083
node.parent.callee === node;
1184

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-
}
85+
const correctIfNeeded = (name, context, node) => {
86+
const correction = correcter.correct(name, Math.max(0, Math.min(name.length - 2, 2)));
87+
if (correction === undefined) {
88+
return undefined;
89+
}
90+
91+
if (correction !== name) {
92+
context.report({
93+
node,
94+
message: `Misspelled \`.${correction}\` as \`.${name}\`.`,
95+
fix: fixer => fixer.replaceText(node.property, correction)
96+
});
97+
}
3098

31-
return res;
32-
}, initial);
99+
return correction;
33100
};
34101

35102
const create = context => {
@@ -58,66 +125,62 @@ const create = context => {
58125
}
59126

60127
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-
});
71-
}
72128

73-
return;
74-
}
129+
let hadSkip = false;
130+
let hadCall = false;
131+
let needCall = true;
132+
for (const [i, member] of members.entries()) {
133+
const corrected = correctIfNeeded(member, context, node);
134+
if (corrected === undefined) {
135+
needCall = false;
136+
if (isCallExpression(node)) {
137+
context.report({
138+
node,
139+
message: `Unknown assertion method \`.${member}\`.`
140+
});
141+
} else {
142+
context.report({
143+
node,
144+
message: `Unknown member \`.${member}\`. Use \`.context.${member}\` instead.`
145+
});
146+
}
75147

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-
});
84-
}
148+
break;
149+
} else if (i === 0 && nonMethods.has(corrected)) {
150+
needCall = false;
151+
if (members.length === 1 && isCallExpression(node)) {
152+
context.report({
153+
node,
154+
message: `Unknown assertion method \`.${member}\`.`
155+
});
156+
}
85157

86-
return;
87-
}
158+
break;
159+
} else if (corrected === 'skip') {
160+
if (hadSkip) {
161+
context.report({
162+
node,
163+
message: 'Too many chained uses of `.skip`.'
164+
});
165+
}
166+
167+
hadSkip = true;
168+
} else {
169+
if (hadCall) {
170+
context.report({
171+
node,
172+
message: 'Can\'t chain assertion methods.'
173+
});
174+
}
88175

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-
});
176+
hadCall = true;
116177
}
117-
} else if (stats.other.length > 0) {
178+
}
179+
180+
if (needCall && !hadCall) {
118181
context.report({
119182
node,
120-
message: `Unknown member \`.${stats.other[0]}\`. Use \`.context.${stats.other[0]}\` instead.`
183+
message: 'Missing assertion method.'
121184
});
122185
}
123186
})

test/use-t-well.js

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,15 @@ ruleTester.run('use-t-well', rule, {
7979
},
8080
{
8181
code: testCase('t.depEqual(a, a);'),
82-
errors: [error('Unknown assertion method `.depEqual`.')]
82+
errors: [error('Misspelled `.deepEqual` as `.depEqual`.')]
8383
},
8484
{
8585
code: testCase('t.deepEqual.skp(a, a);'),
86-
errors: [error('Unknown assertion method `.skp`.')]
86+
errors: [error('Misspelled `.skip` as `.skp`.')]
8787
},
8888
{
8989
code: testCase('t.skp.deepEqual(a, a);'),
90-
errors: [error('Unknown assertion method `.skp`.')]
90+
errors: [error('Misspelled `.skip` as `.skp`.')]
9191
},
9292
{
9393
code: testCase('t.context();'),
@@ -107,15 +107,15 @@ ruleTester.run('use-t-well', rule, {
107107
},
108108
{
109109
code: testCase('t.deepEqu;'),
110-
errors: [error('Unknown member `.deepEqu`. Use `.context.deepEqu` instead.')]
110+
errors: [error('Misspelled `.deepEqual` as `.deepEqu`.')]
111111
},
112112
{
113113
code: testCase('t.deepEqual.is(a, a);'),
114114
errors: [error('Can\'t chain assertion methods.')]
115115
},
116116
{
117117
code: testCase('t.paln(1);'),
118-
errors: [error('Unknown assertion method `.paln`.')]
118+
errors: [error('Misspelled `.plan` as `.paln`.')]
119119
},
120120
{
121121
code: testCase('t.skip();'),
@@ -129,6 +129,56 @@ ruleTester.run('use-t-well', rule, {
129129
code: testCase('t.falsey(a);'),
130130
output: testCase('t.falsy(a);'),
131131
errors: [error('Misspelled `.falsy` as `.falsey`.')]
132+
},
133+
{
134+
code: testCase('t.truthey(a);'),
135+
output: testCase('t.truthy(a);'),
136+
errors: [error('Misspelled `.truthy` as `.truthey`.')]
137+
},
138+
{
139+
code: testCase('t.deepequal(a, {});'),
140+
output: testCase('t.deepEqual(a, {});'),
141+
errors: [error('Misspelled `.deepEqual` as `.deepequal`.')]
142+
},
143+
{
144+
code: testCase('t.contxt;'),
145+
output: testCase('t.context;'),
146+
errors: [error('Misspelled `.context` as `.contxt`.')]
147+
},
148+
{
149+
code: testCase('t.notdeepEqual(a, {});'),
150+
output: testCase('t.notDeepEqual(a, {});'),
151+
errors: [error('Misspelled `.notDeepEqual` as `.notdeepEqual`.')]
152+
},
153+
{
154+
code: testCase('t.throw(a);'),
155+
output: testCase('t.throws(a);'),
156+
errors: [error('Misspelled `.throws` as `.throw`.')]
157+
},
158+
{
159+
code: testCase('t.notThrow(a);'),
160+
output: testCase('t.notThrows(a);'),
161+
errors: [error('Misspelled `.notThrows` as `.notThrow`.')]
162+
},
163+
{
164+
code: testCase('t.throwAsync(a);'),
165+
output: testCase('t.throwsAsync(a);'),
166+
errors: [error('Misspelled `.throwsAsync` as `.throwAsync`.')]
167+
},
168+
{
169+
code: testCase('t.notthrowAsync(a);'),
170+
output: testCase('t.notThrowsAsync(a);'),
171+
errors: [error('Misspelled `.notThrowsAsync` as `.notthrowAsync`.')]
172+
},
173+
{
174+
code: testCase('t.regexp(a, /r/);'),
175+
output: testCase('t.regex(a, /r/);'),
176+
errors: [error('Misspelled `.regex` as `.regexp`.')]
177+
},
178+
{
179+
code: testCase('t.notregexp(a, /r/);'),
180+
output: testCase('t.notRegex(a, /r/);'),
181+
errors: [error('Misspelled `.notRegex` as `.notregexp`.')]
132182
}
133183
]
134184
});

0 commit comments

Comments
 (0)