eslint-plugin-jest
Version:
ESLint rules for Jest
305 lines (234 loc) • 9.09 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.getNodeChain = getNodeChain;
exports.scopeHasLocalReference = exports.parseJestFnCall = exports.isTypeOfJestFnCall = void 0;
var _utils = require("@typescript-eslint/utils");
var _utils2 = require("../utils");
const isTypeOfJestFnCall = (node, scope, types) => {
const jestFnCall = parseJestFnCall(node, scope);
return jestFnCall !== null && types.includes(jestFnCall.type);
};
exports.isTypeOfJestFnCall = isTypeOfJestFnCall;
function getNodeChain(node) {
if ((0, _utils2.isSupportedAccessor)(node)) {
return [node];
}
switch (node.type) {
case _utils.AST_NODE_TYPES.TaggedTemplateExpression:
return getNodeChain(node.tag);
case _utils.AST_NODE_TYPES.MemberExpression:
return [...getNodeChain(node.object), ...getNodeChain(node.property)];
case _utils.AST_NODE_TYPES.NewExpression:
case _utils.AST_NODE_TYPES.CallExpression:
return getNodeChain(node.callee);
}
return [];
}
const determineJestFnType = name => {
// if (name === 'expect') {
// return 'expect';
// }
if (name === 'jest') {
return 'jest';
}
if (_utils2.DescribeAlias.hasOwnProperty(name)) {
return 'describe';
}
if (_utils2.TestCaseName.hasOwnProperty(name)) {
return 'test';
}
/* istanbul ignore else */
if (_utils2.HookName.hasOwnProperty(name)) {
return 'hook';
}
/* istanbul ignore next */
return 'unknown';
};
const ValidJestFnCallChains = ['afterAll', 'afterEach', 'beforeAll', 'beforeEach', 'describe', 'describe.each', 'describe.only', 'describe.only.each', 'describe.skip', 'describe.skip.each', 'fdescribe', 'fdescribe.each', 'xdescribe', 'xdescribe.each', 'it', 'it.concurrent', 'it.concurrent.each', 'it.concurrent.only.each', 'it.concurrent.skip.each', 'it.each', 'it.failing', 'it.only', 'it.only.each', 'it.only.failing', 'it.skip', 'it.skip.each', 'it.skip.failing', 'it.todo', 'fit', 'fit.each', 'fit.failing', 'xit', 'xit.each', 'xit.failing', 'test', 'test.concurrent', 'test.concurrent.each', 'test.concurrent.only.each', 'test.concurrent.skip.each', 'test.each', 'test.failing', 'test.only', 'test.only.each', 'test.only.failing', 'test.skip', 'test.skip.each', 'test.skip.failing', 'test.todo', 'xtest', 'xtest.each', 'xtest.failing'];
const parseJestFnCall = (node, scope) => {
var _node$parent, _node$parent2, _resolved$original;
// ensure that we're at the "top" of the function call chain otherwise when
// parsing e.g. x().y.z(), we'll incorrectly find & parse "x()" even though
// the full chain is not a valid jest function call chain
if (((_node$parent = node.parent) === null || _node$parent === void 0 ? void 0 : _node$parent.type) === _utils.AST_NODE_TYPES.CallExpression || ((_node$parent2 = node.parent) === null || _node$parent2 === void 0 ? void 0 : _node$parent2.type) === _utils.AST_NODE_TYPES.MemberExpression) {
return null;
}
const chain = getNodeChain(node);
if (chain.length === 0) {
return null;
} // ensure that the only call expression in the chain is at the end
if (chain.slice(0, chain.length - 1).some(nod => {
var _nod$parent;
return ((_nod$parent = nod.parent) === null || _nod$parent === void 0 ? void 0 : _nod$parent.type) === _utils.AST_NODE_TYPES.CallExpression;
})) {
return null;
}
const [first, ...rest] = chain;
const lastNode = chain[chain.length - 1]; // if we're an `each()`, ensure we're the outer CallExpression (i.e `.each()()`)
if ((0, _utils2.isSupportedAccessor)(lastNode, 'each')) {
if (node.callee.type !== _utils.AST_NODE_TYPES.CallExpression && node.callee.type !== _utils.AST_NODE_TYPES.TaggedTemplateExpression) {
return null;
}
}
const resolved = resolveToJestFn(scope, (0, _utils2.getAccessorValue)(first)); // we're not a jest function
if (!resolved) {
return null;
}
const name = (_resolved$original = resolved.original) !== null && _resolved$original !== void 0 ? _resolved$original : resolved.local;
const links = [name, ...rest.map(link => (0, _utils2.getAccessorValue)(link))];
if (name !== 'jest' && !ValidJestFnCallChains.includes(links.join('.'))) {
return null;
}
return {
name,
type: determineJestFnType(name),
head: { ...resolved,
node: first
},
members: rest
};
};
exports.parseJestFnCall = parseJestFnCall;
const describeImportDefAsImport = def => {
if (def.parent.type === _utils.AST_NODE_TYPES.TSImportEqualsDeclaration) {
return null;
}
if (def.node.type !== _utils.AST_NODE_TYPES.ImportSpecifier) {
return null;
} // we only care about value imports
if (def.parent.importKind === 'type') {
return null;
}
return {
source: def.parent.source.value,
imported: def.node.imported.name,
local: def.node.local.name
};
};
/**
* Attempts to find the node that represents the import source for the
* given expression node, if it looks like it's an import.
*
* If no such node can be found (e.g. because the expression doesn't look
* like an import), then `null` is returned instead.
*/
const findImportSourceNode = node => {
if (node.type === _utils.AST_NODE_TYPES.AwaitExpression) {
if (node.argument.type === _utils.AST_NODE_TYPES.ImportExpression) {
return node.argument.source;
}
return null;
}
if (node.type === _utils.AST_NODE_TYPES.CallExpression && (0, _utils2.isIdentifier)(node.callee, 'require')) {
var _node$arguments$;
return (_node$arguments$ = node.arguments[0]) !== null && _node$arguments$ !== void 0 ? _node$arguments$ : null;
}
return null;
};
const describeVariableDefAsImport = def => {
var _def$name$parent;
// make sure that we've actually being assigned a value
if (!def.node.init) {
return null;
}
const sourceNode = findImportSourceNode(def.node.init);
if (!sourceNode || !(0, _utils2.isStringNode)(sourceNode)) {
return null;
}
if (((_def$name$parent = def.name.parent) === null || _def$name$parent === void 0 ? void 0 : _def$name$parent.type) !== _utils.AST_NODE_TYPES.Property) {
return null;
}
if (!(0, _utils2.isSupportedAccessor)(def.name.parent.key)) {
return null;
}
return {
source: (0, _utils2.getStringValue)(sourceNode),
imported: (0, _utils2.getAccessorValue)(def.name.parent.key),
local: def.name.name
};
};
/**
* Attempts to describe a definition as an import if possible.
*
* If the definition is an import binding, it's described as you'd expect.
* If the definition is a variable, then we try and determine if it's either
* a dynamic `import()` or otherwise a call to `require()`.
*
* If it's neither of these, `null` is returned to indicate that the definition
* is not describable as an import of any kind.
*/
const describePossibleImportDef = def => {
if (def.type === 'Variable') {
return describeVariableDefAsImport(def);
}
if (def.type === 'ImportBinding') {
return describeImportDefAsImport(def);
}
return null;
};
const collectReferences = scope => {
const locals = new Set();
const imports = new Map();
const unresolved = new Set();
let currentScope = scope;
while (currentScope !== null) {
for (const ref of currentScope.variables) {
if (ref.defs.length === 0) {
continue;
}
const def = ref.defs[ref.defs.length - 1];
const importDetails = describePossibleImportDef(def);
if (importDetails) {
imports.set(importDetails.local, importDetails);
continue;
}
locals.add(ref.name);
}
for (const ref of currentScope.through) {
unresolved.add(ref.identifier.name);
}
currentScope = currentScope.upper;
}
return {
locals,
imports,
unresolved
};
};
const resolveToJestFn = (scope, identifier) => {
const references = collectReferences(scope);
const maybeImport = references.imports.get(identifier);
if (maybeImport) {
// the identifier is imported from @jest/globals,
// so return the original import name
if (maybeImport.source === '@jest/globals') {
return {
original: maybeImport.imported,
local: maybeImport.local,
type: 'import'
};
}
return null;
} // the identifier was found as a local variable or function declaration
// meaning it's not a function from jest
if (references.locals.has(identifier)) {
return null;
}
return {
original: null,
local: identifier,
type: 'global'
};
};
const scopeHasLocalReference = (scope, referenceName) => {
const references = collectReferences(scope);
return (// referenceName was found as a local variable or function declaration.
references.locals.has(referenceName) || // referenceName was found as an imported identifier
references.imports.has(referenceName) || // referenceName was not found as an unresolved reference,
// meaning it is likely not an implicit global reference.
!references.unresolved.has(referenceName)
);
};
exports.scopeHasLocalReference = scopeHasLocalReference;