Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assertion methods auto correction for use-t-well rule #277

Merged
merged 8 commits into from
Feb 26, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions docs/rules/use-t-well.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Translations: [Français](https://github.com/avajs/ava-docs/blob/master/fr_FR/re

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`.

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


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

test('main', t => {
t(value); // `t` is not a function
t.depEqual(value, [2]); // Unknown assertion method `.depEqual`
t.contxt.foo = 100; // Unknown member `.contxt`. Use `.context.contxt` instead
t.depEqual(value, [2]); // Misspelled `.deepEqual` as `.depEqual`, fixable
t.contxt.foo = 100; // Misspelled `.context` as `.contxt`, fixable
t.deepEqual.skip.skip(); // Too many chained uses of `.skip`, fixable
t.skip.deepEqual(1, 1); // `.skip` modifier should be the last in chain, fixable
t.foo = 1000; // Unknown member `.foo`. Use `.context.foo` instead
t.deepEqual.is(value, value); // Can't chain assertion methods
t.skip(); // Missing assertion method
t.deepEqual.skip.skip(); // Too many chained uses of `.skip`
});
```

Expand Down
229 changes: 156 additions & 73 deletions rules/use-t-well.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,91 @@ const {visitIf} = require('enhance-visitors');
const util = require('../util');
const createAvaRule = require('../create-ava-rule');

const isMethod = name => util.executionMethods.has(name);
class MicroCorrecter {
stroncium marked this conversation as resolved.
Show resolved Hide resolved
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 properties = new Set([
...util.executionMethods,
'context',
'title',
'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 getMemberNodes = node => {
if (node.object.type === 'MemberExpression') {
return getMemberNodes(node.object).concat(node.property);
}

return res;
}, initial);
return [node.property];
};

const correctIdentifier = name => correcter.correct(name, Math.max(0, Math.min(name.length - 2, 2)));
stroncium marked this conversation as resolved.
Show resolved Hide resolved

const create = context => {
const ava = createAvaRule();

Expand All @@ -57,67 +113,94 @@ const create = context => {
return;
}

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`.'
});
const members = getMemberNodes(node);

const skipPositions = [];
let hadCall = false;
for (const [i, member] of members.entries()) {
const {name} = member;

let corrected = correctIdentifier(name);

if (i !== 0 && (corrected === 'context' || corrected === 'title')) { // `context` and `title` can only be first
corrected = undefined;
}

return;
}
if (corrected !== name) {
if (corrected === undefined) {
if (isCallExpression(node)) {
context.report({
node,
message: `Unknown assertion method \`.${name}\`.`
});
} else {
context.report({
node,
message: `Unknown member \`.${name}\`. Use \`.context.${name}\` instead.`
});
}
} else {
context.report({
node,
message: `Misspelled \`.${corrected}\` as \`.${name}\`.`,
fix: fixer => fixer.replaceText(member, corrected)
});
}

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`.'
});
return; // Don't check further
}

return;
}
if (name === 'context' || name === 'title') {
if (members.length === 1 && isCallExpression(node)) {
context.report({
node,
message: `Unknown assertion method \`.${name}\`.`
});
}

return; // Don't check further
}

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.'
});
if (name === 'skip') {
skipPositions.push(i);
} else {
if (hadCall) {
context.report({
node,
message: 'Can\'t chain assertion methods.'
});
}

hadCall = true;
}
} else if (stats.other.length > 0) {
}

if (!hadCall) {
context.report({
node,
message: 'Missing assertion method.'
});
}

if (skipPositions.length > 1) {
context.report({
node,
message: 'Too many chained uses of `.skip`.',
fix: fixer => {
const chain = ['t', ...members.map(member => member.name).filter(name => name !== 'skip'), 'skip'];
return fixer.replaceText(node, chain.join('.'));
}
});
}

if (skipPositions.length === 1 && skipPositions[0] !== members.length - 1) {
context.report({
node,
message: `Unknown member \`.${stats.other[0]}\`. Use \`.context.${stats.other[0]}\` instead.`
message: '`.skip` modifier should be the last in chain.',
fix: fixer => {
const chain = ['t', ...members.map(member => member.name).filter(name => name !== 'skip'), 'skip'];
return fixer.replaceText(node, chain.join('.'));
}
});
}
})
Expand Down
Loading