eslint-plugin-ava
Version:
ESLint rules for AVA
238 lines (189 loc) • 5.74 kB
JavaScript
import {visitIf} from 'enhance-visitors';
import createAvaRule from '../create-ava-rule.js';
import util from '../util.js';
const MESSAGE_ID = 'prefer-t-regex';
const create = context => {
const ava = createAvaRule();
const booleanTests = new Set([
'true',
'false',
'truthy',
'falsy',
]);
const equalityTests = new Set([
'is',
'deepEqual',
]);
// Find the latest reference to the given identifier's name.
const findReference = node => {
const {sourceCode} = context;
const reference = sourceCode.getScope(node).references.find(reference => reference.identifier.name === node.name);
if (reference?.resolved) {
const definitions = reference.resolved.defs;
if (definitions.length === 0) {
return;
}
return definitions.at(-1).node;
}
};
/*
Recursively find the "origin" node of the given node.
Note: `sourceCode.getScope()` doesn't contain information about the outer scope so in most cases this function will only find the reference directly above the current scope. So the following code will only find the reference in this order: y -> x, and it will have no knowledge of the number `0`. (assuming we run this function on the identifier `y`)
```
const test = require('ava');
let x = 0;
let y = x;
test(t => {
t.is(y, 0);
});
```
*/
const findRootReference = node => {
if (node.type === 'Identifier') {
const reference = findReference(node);
if (reference?.init) {
return findRootReference(reference.init);
}
return node;
}
if (node.type === 'CallExpression' || node.type === 'NewExpression') {
return findRootReference(node.callee);
}
if (node.type === 'MemberExpression') {
return findRootReference(node.object);
}
return node;
};
/*
Determine if the given node is a regex expression.
There are two ways to create regex expressions in JavaScript: Regex literal and `RegExp` class.
1. Regex literal can be easily looked up using the `.regex` property on the node.
2. `RegExp` class can't be looked up so the function just checks for the name `RegExp`.
*/
const isRegExp = lookup => {
if (!lookup) {
return false;
}
if (lookup.regex) {
return true;
}
// Look up references in case it's a variable or RegExp declaration.
const reference = findRootReference(lookup);
return reference.regex ?? reference.name === 'RegExp';
};
const booleanHandler = node => {
const firstArgument = node.arguments[0];
if (!firstArgument) {
return;
}
const isFunctionCall = firstArgument.type === 'CallExpression';
if (!isFunctionCall || !firstArgument.callee.property) {
return;
}
const {name} = firstArgument.callee.property;
let lookup = {};
let variable = {};
if (name === 'test') {
// Represent: `lookup.test(variable)`
lookup = firstArgument.callee.object;
variable = firstArgument.arguments[0];
} else if (['search', 'match'].includes(name)) {
// Represent: `variable.match(lookup)`
lookup = firstArgument.arguments[0];
variable = firstArgument.callee.object;
}
if (!isRegExp(lookup)) {
return;
}
const assertion = ['true', 'truthy'].includes(node.callee.property.name) ? 'regex' : 'notRegex';
const fix = fixer => {
const source = context.sourceCode;
return [
fixer.replaceText(node.callee.property, assertion),
fixer.replaceText(firstArgument, `${source.getText(variable)}, ${source.getText(lookup)}`),
];
};
context.report({
node,
messageId: MESSAGE_ID,
data: {assertion},
fix,
});
};
const equalityHandler = node => {
const [firstArgument, secondArgument] = node.arguments;
const firstArgumentIsRegex = isRegExp(firstArgument);
const secondArgumentIsRegex = isRegExp(secondArgument);
// If both are regex, or neither are, the expression is ok.
if (firstArgumentIsRegex === secondArgumentIsRegex) {
return;
}
const matchee = secondArgumentIsRegex ? firstArgument : secondArgument;
if (!matchee) {
return;
}
const regex = secondArgumentIsRegex ? secondArgument : firstArgument;
const booleanFixer = assertion => fixer => {
const source = context.sourceCode;
return [
fixer.replaceText(node.callee.property, assertion),
fixer.replaceText(firstArgument, `${source.getText(regex.arguments[0])}`),
fixer.replaceText(secondArgument, `${source.getText(regex.callee.object)}`),
];
};
// Only fix a statically verifiable equality.
if (regex && matchee.type === 'Literal') {
let assertion;
if (matchee.raw === 'true') {
assertion = 'regex';
} else if (matchee.raw === 'false') {
assertion = 'notRegex';
} else {
return;
}
context.report({
node,
messageId: MESSAGE_ID,
data: {assertion},
fix: booleanFixer(assertion),
});
}
};
return ava.merge({
CallExpression: visitIf([
ava.isInTestFile,
ava.isInTestNode,
])(node => {
if (!node?.callee?.property) {
return;
}
const isAssertion = node.callee.type === 'MemberExpression'
&& util.isTestObject(util.getNameOfRootNodeObject(node.callee));
const isBooleanAssertion = isAssertion
&& booleanTests.has(node.callee.property.name);
const isEqualityAssertion = isAssertion
&& equalityTests.has(node.callee.property.name);
if (isBooleanAssertion) {
booleanHandler(node);
} else if (isEqualityAssertion) {
equalityHandler(node);
}
}),
});
};
export default {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer `t.regex()` over `RegExp#test()` and `String#match()`.',
recommended: true,
url: util.getDocsUrl(import.meta.filename),
},
fixable: 'code',
schema: [],
messages: {
[MESSAGE_ID]: 'Prefer using the `t.{{assertion}}()` assertion.',
},
},
};