Skip to content

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

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

Merged
merged 8 commits into from
Feb 26, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
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 @@ -80,15 +79,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 @@ -108,28 +105,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`.')]
}
]
});