From eb676f67b7c4615191c446e571f978833b5acc4f Mon Sep 17 00:00:00 2001 From: Yanis Benson Date: Sat, 28 Sep 2019 09:01:37 +0300 Subject: [PATCH] spelling correction based implementation, solves #244 --- rules/use-t-well.js | 211 ++++++++++++++++++++++++++++---------------- test/use-t-well.js | 60 +++++++++++-- 2 files changed, 192 insertions(+), 79 deletions(-) diff --git a/rules/use-t-well.js b/rules/use-t-well.js index 8606d445..cea736d6 100644 --- a/rules/use-t-well.js +++ b/rules/use-t-well.js @@ -3,33 +3,100 @@ const {visitIf} = require('enhance-visitors'); const util = require('../util'); const createAvaRule = require('../create-ava-rule'); -const isMethod = name => util.executionMethods.has(name); +class MicroCorrecter { + constructor(words) { + this.words = new Set(words); + + const letters = new Set(); + words.forEach(word => word.split('').forEach(letter => letters.add(letter))); + this.letters = [...letters]; + } + + edits(word) { + const edits = []; + const {length} = word; + const {letters} = this; + + for (let i = 0; i < length; i++) { + edits.push(word.slice(0, i) + word.slice(i + 1)); // Skip + for (const letter of letters) { + edits.push(word.slice(0, i) + letter + word.slice(i + 1)); // Replace + } + } + + for (let i = 1; i < length; i++) { + edits.push(word.slice(0, i - 1) + word[i] + word[i - 1] + word.slice(i + 1)); // Transposition + } + + for (let i = 0; i <= length; i++) { + for (const letter of letters) { + edits.push(word.slice(0, i) + letter + word.slice(i)); // Addition + } + } + + return edits; + } + + correct(word, distance) { + const {words} = this; + + if (words.has(word)) { + return word; + } + + if (distance > 0) { + const edits = this.edits(word); + + for (const edit of edits) { + if (words.has(edit)) { + return edit; + } + } + + if (distance > 1) { + for (const edit of edits) { + const correction = this.correct(edit, distance - 1); + if (correction !== undefined) { + return correction; + } + } + } + } + } +} + +const nonMethods = new Set([ + 'context', + 'title' +]); + +const properties = new Set([ + ...nonMethods, + ...util.executionMethods, + 'skip' +]); + +const correcter = new MicroCorrecter([...properties]); const isCallExpression = node => node.parent.type === 'CallExpression' && node.parent.callee === node; -const getMemberStats = members => { - const initial = { - skip: [], - falsey: [], - method: [], - other: [] - }; - - return members.reduce((res, member) => { - if (member === 'skip') { - res.skip.push(member); - } else if (member === 'falsey') { - res.falsey.push(member); - } else if (isMethod(member)) { - res.method.push(member); - } else { - res.other.push(member); - } +const correctIfNeeded = (name, context, node) => { + const correction = correcter.correct(name, Math.max(0, Math.min(name.length - 2, 2))); + if (correction === undefined) { + return undefined; + } + + if (correction !== name) { + context.report({ + node, + message: `Misspelled \`.${correction}\` as \`.${name}\`.`, + fix: fixer => fixer.replaceText(node.property, correction) + }); + } - return res; - }, initial); + return correction; }; const create = context => { @@ -58,66 +125,62 @@ const create = context => { } const members = util.getMembers(node); - const stats = getMemberStats(members); - - if (members[0] === 'context') { - // Anything is fine when of the form `t.context...` - if (members.length === 1 && isCallExpression(node)) { - // Except `t.context()` - context.report({ - node, - message: 'Unknown assertion method `.context`.' - }); - } - return; - } + let hadSkip = false; + let hadCall = false; + let needCall = true; + for (const [i, member] of members.entries()) { + const corrected = correctIfNeeded(member, context, node); + if (corrected === undefined) { + needCall = false; + if (isCallExpression(node)) { + context.report({ + node, + message: `Unknown assertion method \`.${member}\`.` + }); + } else { + context.report({ + node, + message: `Unknown member \`.${member}\`. Use \`.context.${member}\` instead.` + }); + } - if (members[0] === 'title') { - // Anything is fine when of the form `t.title...` - if (members.length === 1 && isCallExpression(node)) { - // Except `t.title()` - context.report({ - node, - message: 'Unknown assertion method `.title`.' - }); - } + break; + } else if (i === 0 && nonMethods.has(corrected)) { + needCall = false; + if (members.length === 1 && isCallExpression(node)) { + context.report({ + node, + message: `Unknown assertion method \`.${member}\`.` + }); + } - return; - } + break; + } else if (corrected === 'skip') { + if (hadSkip) { + context.report({ + node, + message: 'Too many chained uses of `.skip`.' + }); + } + + hadSkip = true; + } else { + if (hadCall) { + context.report({ + node, + message: 'Can\'t chain assertion methods.' + }); + } - if (isCallExpression(node)) { - if (stats.other.length > 0) { - context.report({ - node, - message: `Unknown assertion method \`.${stats.other[0]}\`.` - }); - } else if (stats.skip.length > 1) { - context.report({ - node, - message: 'Too many chained uses of `.skip`.' - }); - } else if (stats.falsey.length > 0) { - context.report({ - node, - message: 'Misspelled `.falsy` as `.falsey`.', - fix: fixer => fixer.replaceText(node.property, 'falsy') - }); - } else if (stats.method.length > 1) { - context.report({ - node, - message: 'Can\'t chain assertion methods.' - }); - } else if (stats.method.length === 0) { - context.report({ - node, - message: 'Missing assertion method.' - }); + hadCall = true; } - } else if (stats.other.length > 0) { + } + + if (needCall && !hadCall) { context.report({ node, - message: `Unknown member \`.${stats.other[0]}\`. Use \`.context.${stats.other[0]}\` instead.` + message: 'Missing assertion method.' }); } }) diff --git a/test/use-t-well.js b/test/use-t-well.js index a79ad616..4d6017b9 100644 --- a/test/use-t-well.js +++ b/test/use-t-well.js @@ -79,15 +79,15 @@ ruleTester.run('use-t-well', rule, { }, { code: testCase('t.depEqual(a, a);'), - errors: [error('Unknown assertion method `.depEqual`.')] + errors: [error('Misspelled `.deepEqual` as `.depEqual`.')] }, { code: testCase('t.deepEqual.skp(a, a);'), - errors: [error('Unknown assertion method `.skp`.')] + errors: [error('Misspelled `.skip` as `.skp`.')] }, { code: testCase('t.skp.deepEqual(a, a);'), - errors: [error('Unknown assertion method `.skp`.')] + errors: [error('Misspelled `.skip` as `.skp`.')] }, { code: testCase('t.context();'), @@ -107,7 +107,7 @@ ruleTester.run('use-t-well', rule, { }, { code: testCase('t.deepEqu;'), - errors: [error('Unknown member `.deepEqu`. Use `.context.deepEqu` instead.')] + errors: [error('Misspelled `.deepEqual` as `.deepEqu`.')] }, { code: testCase('t.deepEqual.is(a, a);'), @@ -115,7 +115,7 @@ ruleTester.run('use-t-well', rule, { }, { code: testCase('t.paln(1);'), - errors: [error('Unknown assertion method `.paln`.')] + errors: [error('Misspelled `.plan` as `.paln`.')] }, { code: testCase('t.skip();'), @@ -129,6 +129,56 @@ ruleTester.run('use-t-well', rule, { code: testCase('t.falsey(a);'), output: testCase('t.falsy(a);'), errors: [error('Misspelled `.falsy` as `.falsey`.')] + }, + { + code: testCase('t.truthey(a);'), + output: testCase('t.truthy(a);'), + errors: [error('Misspelled `.truthy` as `.truthey`.')] + }, + { + code: testCase('t.deepequal(a, {});'), + output: testCase('t.deepEqual(a, {});'), + errors: [error('Misspelled `.deepEqual` as `.deepequal`.')] + }, + { + code: testCase('t.contxt;'), + output: testCase('t.context;'), + errors: [error('Misspelled `.context` as `.contxt`.')] + }, + { + code: testCase('t.notdeepEqual(a, {});'), + output: testCase('t.notDeepEqual(a, {});'), + errors: [error('Misspelled `.notDeepEqual` as `.notdeepEqual`.')] + }, + { + code: testCase('t.throw(a);'), + output: testCase('t.throws(a);'), + errors: [error('Misspelled `.throws` as `.throw`.')] + }, + { + code: testCase('t.notThrow(a);'), + output: testCase('t.notThrows(a);'), + errors: [error('Misspelled `.notThrows` as `.notThrow`.')] + }, + { + code: testCase('t.throwAsync(a);'), + output: testCase('t.throwsAsync(a);'), + errors: [error('Misspelled `.throwsAsync` as `.throwAsync`.')] + }, + { + code: testCase('t.notthrowAsync(a);'), + output: testCase('t.notThrowsAsync(a);'), + errors: [error('Misspelled `.notThrowsAsync` as `.notthrowAsync`.')] + }, + { + code: testCase('t.regexp(a, /r/);'), + output: testCase('t.regex(a, /r/);'), + errors: [error('Misspelled `.regex` as `.regexp`.')] + }, + { + code: testCase('t.notregexp(a, /r/);'), + output: testCase('t.notRegex(a, /r/);'), + errors: [error('Misspelled `.notRegex` as `.notregexp`.')] } ] });