Skip to content

Commit

Permalink
Assertion methods auto correction for use-t-well rule (#277)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
stroncium and sindresorhus committed Feb 26, 2020
1 parent a217bee commit d9f9a49
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 87 deletions.
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"espree": "^6.1.2",
"espurify": "^2.0.1",
"import-modules": "^2.0.0",
"micro-spelling-correcter": "^1.1.1",
"pkg-dir": "^4.2.0",
"resolve-from": "^5.0.0"
},
Expand Down
169 changes: 95 additions & 74 deletions rules/use-t-well.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,29 @@
'use strict';
const {visitIf} = require('enhance-visitors');
const MicroSpellingCorrecter = require('micro-spelling-correcter');

const util = require('../util');
const createAvaRule = require('../create-ava-rule');

const isMethod = name => util.executionMethods.has(name);
const properties = new Set([
...util.executionMethods,
'context',
'title',
'skip'
]);

const correcter = new MicroSpellingCorrecter([...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);
}

return res;
}, initial);
const getMemberNodes = node => {
if (node.object.type === 'MemberExpression') {
return getMemberNodes(node.object).concat(node.property);
}

return [node.property];
};

const create = context => {
Expand Down Expand Up @@ -57,67 +51,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 = correcter.correct(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
88 changes: 79 additions & 9 deletions test/use-t-well.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ ruleTester.run('use-t-well', rule, {
testCase('t.snapshot(v);'),
testCase('t.ifError(error);'),
testCase('t.deepEqual.skip(a, a);'),
testCase('t.skip.deepEqual(a, a);'),
testCase('t.context.a = 1;'),
testCase('t.context.foo.skip();'),
testCase('console.log(t.context);'),
Expand Down Expand Up @@ -84,15 +83,13 @@ ruleTester.run('use-t-well', rule, {
},
{
code: testCase('t.depEqual(a, a);'),
errors: [error('Unknown assertion method `.depEqual`.')]
output: testCase('t.deepEqual(a, a);'),
errors: [error('Misspelled `.deepEqual` as `.depEqual`.')]
},
{
code: testCase('t.deepEqual.skp(a, a);'),
errors: [error('Unknown assertion method `.skp`.')]
},
{
code: testCase('t.skp.deepEqual(a, a);'),
errors: [error('Unknown assertion method `.skp`.')]
output: testCase('t.deepEqual.skip(a, a);'),
errors: [error('Misspelled `.skip` as `.skp`.')]
},
{
code: testCase('t.context();'),
Expand All @@ -112,28 +109,101 @@ ruleTester.run('use-t-well', rule, {
},
{
code: testCase('t.deepEqu;'),
errors: [error('Unknown member `.deepEqu`. Use `.context.deepEqu` instead.')]
output: testCase('t.deepEqual;'),
errors: [error('Misspelled `.deepEqual` as `.deepEqu`.')]
},
{
code: testCase('t.deepEqual.is(a, a);'),
errors: [error('Can\'t chain assertion methods.')]
},
{
code: testCase('t.paln(1);'),
errors: [error('Unknown assertion method `.paln`.')]
output: testCase('t.plan(1);'),
errors: [error('Misspelled `.plan` as `.paln`.')]
},
{
code: testCase('t.skip();'),
errors: [error('Missing assertion method.')]
},
{
code: testCase('t.deepEqual.skip.skip(a, a);'),
output: testCase('t.deepEqual.skip(a, a);'),
errors: [error('Too many chained uses of `.skip`.')]
},
{
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`.')]
},
{
code: testCase('t.contxt.foo = 1;'),
output: testCase('t.context.foo = 1;'),
errors: [error('Misspelled `.context` as `.contxt`.')]
},

{
code: testCase('t.skip.deepEqual(a, a);'),
output: testCase('t.deepEqual.skip(a, a);'),
errors: [error('`.skip` modifier should be the last in chain.')]
},
{
code: testCase('t.skp.deepEqual(a, a);'),
output: testCase('t.skip.deepEqual(a, a);'),
errors: [error('Misspelled `.skip` as `.skp`.')]
},
{
code: testCase('t.deepEqual.context(a, a);'),
errors: [error('Unknown assertion method `.context`.')]
}
]
});

0 comments on commit d9f9a49

Please sign in to comment.