"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getNodeChain = getNodeChain; exports.scopeHasLocalReference = exports.parseJestFnCallWithReason = exports.parseJestFnCall = exports.isTypeOfJestFnCall = void 0; var _utils = require("@typescript-eslint/utils"); var _utils2 = require("../utils"); const isTypeOfJestFnCall = (node, context, types) => { const jestFnCall = parseJestFnCall(node, context); return jestFnCall !== null && types.includes(jestFnCall.type); }; exports.isTypeOfJestFnCall = isTypeOfJestFnCall; const joinChains = (a, b) => a && b ? [...a, ...b] : null; 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 joinChains(getNodeChain(node.object), getNodeChain(node.property)); case _utils.AST_NODE_TYPES.CallExpression: return getNodeChain(node.callee); } return null; } 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 resolvePossibleAliasedGlobal = (global, context) => { var _context$settings$jes, _context$settings$jes2; const globalAliases = (_context$settings$jes = (_context$settings$jes2 = context.settings.jest) === null || _context$settings$jes2 === void 0 ? void 0 : _context$settings$jes2.globalAliases) !== null && _context$settings$jes !== void 0 ? _context$settings$jes : {}; const alias = Object.entries(globalAliases).find(([, aliases]) => aliases.includes(global)); if (alias) { return alias[0]; } return null; }; const parseJestFnCallCache = new WeakMap(); const parseJestFnCall = (node, context) => { const jestFnCall = parseJestFnCallWithReason(node, context); if (typeof jestFnCall === 'string') { return null; } return jestFnCall; }; exports.parseJestFnCall = parseJestFnCall; const parseJestFnCallWithReason = (node, context) => { let parsedJestFnCall = parseJestFnCallCache.get(node); if (parsedJestFnCall) { return parsedJestFnCall; } parsedJestFnCall = parseJestFnCallWithReasonInner(node, context); parseJestFnCallCache.set(node, parsedJestFnCall); return parsedJestFnCall; }; exports.parseJestFnCallWithReason = parseJestFnCallWithReason; const parseJestFnCallWithReasonInner = (node, context) => { var _resolved$original, _node$parent2, _node$parent3; const chain = getNodeChain(node); if (!(chain !== null && chain !== void 0 && chain.length)) { return null; } const [first, ...rest] = chain; const lastLink = (0, _utils2.getAccessorValue)(chain[chain.length - 1]); // if we're an `each()`, ensure we're the outer CallExpression (i.e `.each()()`) if (lastLink === 'each') { if (node.callee.type !== _utils.AST_NODE_TYPES.CallExpression && node.callee.type !== _utils.AST_NODE_TYPES.TaggedTemplateExpression) { return null; } } if (node.callee.type === _utils.AST_NODE_TYPES.TaggedTemplateExpression && lastLink !== 'each') { return null; } const resolved = resolveToJestFn(context, (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' && name !== 'expect' && !ValidJestFnCallChains.includes(links.join('.'))) { return null; } const parsedJestFnCall = { name, head: { ...resolved, node: first }, // every member node must have a member expression as their parent // in order to be part of the call chain we're parsing members: rest }; const type = determineJestFnType(name); if (type === 'expect') { const result = parseJestExpectCall(parsedJestFnCall); // if the `expect` call chain is not valid, only report on the topmost node // since all members in the chain are likely to get flagged for some reason if (typeof result === 'string' && (0, _utils2.findTopMostCallExpression)(node) !== node) { return null; } if (result === 'matcher-not-found') { var _node$parent; if (((_node$parent = node.parent) === null || _node$parent === void 0 ? void 0 : _node$parent.type) === _utils.AST_NODE_TYPES.MemberExpression) { return 'matcher-not-called'; } } return result; } // check that every link in the chain except the last is a member expression 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.MemberExpression; })) { return null; } // 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$parent2 = node.parent) === null || _node$parent2 === void 0 ? void 0 : _node$parent2.type) === _utils.AST_NODE_TYPES.CallExpression || ((_node$parent3 = node.parent) === null || _node$parent3 === void 0 ? void 0 : _node$parent3.type) === _utils.AST_NODE_TYPES.MemberExpression) { return null; } return { ...parsedJestFnCall, type }; }; const findModifiersAndMatcher = members => { const modifiers = []; for (const member of members) { var _member$parent, _member$parent$parent; // check if the member is being called, which means it is the matcher // (and also the end of the entire "expect" call chain) if (((_member$parent = member.parent) === null || _member$parent === void 0 ? void 0 : _member$parent.type) === _utils.AST_NODE_TYPES.MemberExpression && ((_member$parent$parent = member.parent.parent) === null || _member$parent$parent === void 0 ? void 0 : _member$parent$parent.type) === _utils.AST_NODE_TYPES.CallExpression) { return { matcher: member, args: member.parent.parent.arguments, modifiers }; } // otherwise, it should be a modifier const name = (0, _utils2.getAccessorValue)(member); if (modifiers.length === 0) { // the first modifier can be any of the three modifiers if (!_utils2.ModifierName.hasOwnProperty(name)) { return 'modifier-unknown'; } } else if (modifiers.length === 1) { // the second modifier can only be "not" if (name !== _utils2.ModifierName.not) { return 'modifier-unknown'; } const firstModifier = (0, _utils2.getAccessorValue)(modifiers[0]); // and the first modifier has to be either "resolves" or "rejects" if (firstModifier !== _utils2.ModifierName.resolves && firstModifier !== _utils2.ModifierName.rejects) { return 'modifier-unknown'; } } else { return 'modifier-unknown'; } modifiers.push(member); } // this will only really happen if there are no members return 'matcher-not-found'; }; const parseJestExpectCall = typelessParsedJestFnCall => { const modifiersAndMatcher = findModifiersAndMatcher(typelessParsedJestFnCall.members); if (typeof modifiersAndMatcher === 'string') { return modifiersAndMatcher; } return { ...typelessParsedJestFnCall, type: 'expect', ...modifiersAndMatcher }; }; 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 = (context, identifier) => { const references = collectReferences(context.getScope()); 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: resolvePossibleAliasedGlobal(identifier, context), 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;