123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409 |
- "use strict";
- Object.defineProperty(exports, "__esModule", {
- value: true
- });
- exports.default = void 0;
- var _experimentalUtils = require("@typescript-eslint/experimental-utils");
- var _utils = require("./utils");
- const isPromiseChainCall = node => {
- if (node.type === _experimentalUtils.AST_NODE_TYPES.CallExpression && node.callee.type === _experimentalUtils.AST_NODE_TYPES.MemberExpression && (0, _utils.isSupportedAccessor)(node.callee.property)) {
- // promise methods should have at least 1 argument
- if (node.arguments.length === 0) {
- return false;
- }
- switch ((0, _utils.getAccessorValue)(node.callee.property)) {
- case 'then':
- return node.arguments.length < 3;
- case 'catch':
- case 'finally':
- return node.arguments.length < 2;
- }
- }
- return false;
- };
- const findTopMostCallExpression = node => {
- let topMostCallExpression = node;
- let {
- parent
- } = node;
- while (parent) {
- if (parent.type === _experimentalUtils.AST_NODE_TYPES.CallExpression) {
- topMostCallExpression = parent;
- parent = parent.parent;
- continue;
- }
- if (parent.type !== _experimentalUtils.AST_NODE_TYPES.MemberExpression) {
- break;
- }
- parent = parent.parent;
- }
- return topMostCallExpression;
- };
- const isTestCaseCallWithCallbackArg = node => {
- if (!(0, _utils.isTestCaseCall)(node)) {
- return false;
- }
- const isJestEach = (0, _utils.getNodeName)(node).endsWith('.each');
- if (isJestEach && node.callee.type !== _experimentalUtils.AST_NODE_TYPES.TaggedTemplateExpression) {
- // isJestEach but not a TaggedTemplateExpression, so this must be
- // the `jest.each([])()` syntax which this rule doesn't support due
- // to its complexity (see jest-community/eslint-plugin-jest#710)
- // so we return true to trigger bailout
- return true;
- }
- if (isJestEach || node.arguments.length >= 2) {
- const [, callback] = node.arguments;
- const callbackArgIndex = Number(isJestEach);
- return callback && (0, _utils.isFunction)(callback) && callback.params.length === 1 + callbackArgIndex;
- }
- return false;
- };
- const isPromiseMethodThatUsesValue = (node, identifier) => {
- const {
- name
- } = identifier;
- if (node.argument === null) {
- return false;
- }
- if (node.argument.type === _experimentalUtils.AST_NODE_TYPES.CallExpression && node.argument.arguments.length > 0) {
- const nodeName = (0, _utils.getNodeName)(node.argument);
- if (['Promise.all', 'Promise.allSettled'].includes(nodeName)) {
- const [firstArg] = node.argument.arguments;
- if (firstArg.type === _experimentalUtils.AST_NODE_TYPES.ArrayExpression && firstArg.elements.some(nod => (0, _utils.isIdentifier)(nod, name))) {
- return true;
- }
- }
- if (['Promise.resolve', 'Promise.reject'].includes(nodeName) && node.argument.arguments.length === 1) {
- return (0, _utils.isIdentifier)(node.argument.arguments[0], name);
- }
- }
- return (0, _utils.isIdentifier)(node.argument, name);
- };
- /**
- * Attempts to determine if the runtime value represented by the given `identifier`
- * is `await`ed within the given array of elements
- */
- const isValueAwaitedInElements = (name, elements) => {
- for (const element of elements) {
- if (element.type === _experimentalUtils.AST_NODE_TYPES.AwaitExpression && (0, _utils.isIdentifier)(element.argument, name)) {
- return true;
- }
- if (element.type === _experimentalUtils.AST_NODE_TYPES.ArrayExpression && isValueAwaitedInElements(name, element.elements)) {
- return true;
- }
- }
- return false;
- };
- /**
- * Attempts to determine if the runtime value represented by the given `identifier`
- * is `await`ed as an argument along the given call expression
- */
- const isValueAwaitedInArguments = (name, call) => {
- let node = call;
- while (node) {
- if (node.type === _experimentalUtils.AST_NODE_TYPES.CallExpression) {
- if (isValueAwaitedInElements(name, node.arguments)) {
- return true;
- }
- node = node.callee;
- }
- if (node.type !== _experimentalUtils.AST_NODE_TYPES.MemberExpression) {
- break;
- }
- node = node.object;
- }
- return false;
- };
- const getLeftMostCallExpression = call => {
- let leftMostCallExpression = call;
- let node = call;
- while (node) {
- if (node.type === _experimentalUtils.AST_NODE_TYPES.CallExpression) {
- leftMostCallExpression = node;
- node = node.callee;
- }
- if (node.type !== _experimentalUtils.AST_NODE_TYPES.MemberExpression) {
- break;
- }
- node = node.object;
- }
- return leftMostCallExpression;
- };
- /**
- * Attempts to determine if the runtime value represented by the given `identifier`
- * is `await`ed or `return`ed within the given `body` of statements
- */
- const isValueAwaitedOrReturned = (identifier, body) => {
- const {
- name
- } = identifier;
- for (const node of body) {
- // skip all nodes that are before this identifier, because they'd probably
- // be affecting a different runtime value (e.g. due to reassignment)
- if (node.range[0] <= identifier.range[0]) {
- continue;
- }
- if (node.type === _experimentalUtils.AST_NODE_TYPES.ReturnStatement) {
- return isPromiseMethodThatUsesValue(node, identifier);
- }
- if (node.type === _experimentalUtils.AST_NODE_TYPES.ExpressionStatement) {
- // it's possible that we're awaiting the value as an argument
- if (node.expression.type === _experimentalUtils.AST_NODE_TYPES.CallExpression) {
- if (isValueAwaitedInArguments(name, node.expression)) {
- return true;
- }
- const leftMostCall = getLeftMostCallExpression(node.expression);
- if ((0, _utils.isExpectCall)(leftMostCall) && leftMostCall.arguments.length > 0 && (0, _utils.isIdentifier)(leftMostCall.arguments[0], name)) {
- const {
- modifier
- } = (0, _utils.parseExpectCall)(leftMostCall);
- if ((modifier === null || modifier === void 0 ? void 0 : modifier.name) === _utils.ModifierName.resolves || (modifier === null || modifier === void 0 ? void 0 : modifier.name) === _utils.ModifierName.rejects) {
- return true;
- }
- }
- }
- if (node.expression.type === _experimentalUtils.AST_NODE_TYPES.AwaitExpression && isPromiseMethodThatUsesValue(node.expression, identifier)) {
- return true;
- } // (re)assignment changes the runtime value, so if we've not found an
- // await or return already we act as if we've reached the end of the body
- if (node.expression.type === _experimentalUtils.AST_NODE_TYPES.AssignmentExpression) {
- var _getNodeName;
- // unless we're assigning to the same identifier, in which case
- // we might be chaining off the existing promise value
- if ((0, _utils.isIdentifier)(node.expression.left, name) && (_getNodeName = (0, _utils.getNodeName)(node.expression.right)) !== null && _getNodeName !== void 0 && _getNodeName.startsWith(`${name}.`) && isPromiseChainCall(node.expression.right)) {
- continue;
- }
- break;
- }
- }
- if (node.type === _experimentalUtils.AST_NODE_TYPES.BlockStatement && isValueAwaitedOrReturned(identifier, node.body)) {
- return true;
- }
- }
- return false;
- };
- const findFirstBlockBodyUp = node => {
- let parent = node;
- while (parent) {
- if (parent.type === _experimentalUtils.AST_NODE_TYPES.BlockStatement) {
- return parent.body;
- }
- parent = parent.parent;
- }
- /* istanbul ignore next */
- throw new Error(`Could not find BlockStatement - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`);
- };
- const isDirectlyWithinTestCaseCall = node => {
- let parent = node;
- while (parent) {
- if ((0, _utils.isFunction)(parent)) {
- var _parent;
- parent = parent.parent;
- return !!(((_parent = parent) === null || _parent === void 0 ? void 0 : _parent.type) === _experimentalUtils.AST_NODE_TYPES.CallExpression && (0, _utils.isTestCaseCall)(parent));
- }
- parent = parent.parent;
- }
- return false;
- };
- const isVariableAwaitedOrReturned = variable => {
- const body = findFirstBlockBodyUp(variable); // it's pretty much impossible for us to track destructuring assignments,
- // so we return true to bailout gracefully
- if (!(0, _utils.isIdentifier)(variable.id)) {
- return true;
- }
- return isValueAwaitedOrReturned(variable.id, body);
- };
- var _default = (0, _utils.createRule)({
- name: __filename,
- meta: {
- docs: {
- category: 'Best Practices',
- description: 'Ensure promises that have expectations in their chain are valid',
- recommended: 'error'
- },
- messages: {
- expectInFloatingPromise: "This promise should either be returned or awaited to ensure the expects in it's chain are called"
- },
- type: 'suggestion',
- schema: []
- },
- defaultOptions: [],
- create(context) {
- let inTestCaseWithDoneCallback = false; // an array of booleans representing each promise chain we enter, with the
- // boolean value representing if we think a given chain contains an expect
- // in it's body.
- //
- // since we only care about the inner-most chain, we represent the state in
- // reverse with the inner-most being the first item, as that makes it
- // slightly less code to assign to by not needing to know the length
- const chains = [];
- return {
- CallExpression(node) {
- // there are too many ways that the done argument could be used with
- // promises that contain expect that would make the promise safe for us
- if (isTestCaseCallWithCallbackArg(node)) {
- inTestCaseWithDoneCallback = true;
- return;
- } // if this call expression is a promise chain, add it to the stack with
- // value of "false", as we assume there are no expect calls initially
- if (isPromiseChainCall(node)) {
- chains.unshift(false);
- return;
- } // if we're within a promise chain, and this call expression looks like
- // an expect call, mark the deepest chain as having an expect call
- if (chains.length > 0 && (0, _utils.isExpectCall)(node)) {
- chains[0] = true;
- }
- },
- 'CallExpression:exit'(node) {
- // there are too many ways that the "done" argument could be used to
- // make promises containing expects safe in a test for us to be able to
- // accurately check, so we just bail out completely if it's present
- if (inTestCaseWithDoneCallback) {
- if ((0, _utils.isTestCaseCall)(node)) {
- inTestCaseWithDoneCallback = false;
- }
- return;
- }
- if (!isPromiseChainCall(node)) {
- return;
- } // since we're exiting this call expression (which is a promise chain)
- // we remove it from the stack of chains, since we're unwinding
- const hasExpectCall = chains.shift(); // if the promise chain we're exiting doesn't contain an expect,
- // then we don't need to check it for anything
- if (!hasExpectCall) {
- return;
- }
- const {
- parent
- } = findTopMostCallExpression(node); // if we don't have a parent (which is technically impossible at runtime)
- // or our parent is not directly within the test case, we stop checking
- // because we're most likely in the body of a function being defined
- // within the test, which we can't track
- if (!parent || !isDirectlyWithinTestCaseCall(parent)) {
- return;
- }
- switch (parent.type) {
- case _experimentalUtils.AST_NODE_TYPES.VariableDeclarator:
- {
- if (isVariableAwaitedOrReturned(parent)) {
- return;
- }
- break;
- }
- case _experimentalUtils.AST_NODE_TYPES.AssignmentExpression:
- {
- if (parent.left.type === _experimentalUtils.AST_NODE_TYPES.Identifier && isValueAwaitedOrReturned(parent.left, findFirstBlockBodyUp(parent))) {
- return;
- }
- break;
- }
- case _experimentalUtils.AST_NODE_TYPES.ExpressionStatement:
- break;
- case _experimentalUtils.AST_NODE_TYPES.ReturnStatement:
- case _experimentalUtils.AST_NODE_TYPES.AwaitExpression:
- default:
- return;
- }
- context.report({
- messageId: 'expectInFloatingPromise',
- node: parent
- });
- }
- };
- }
- });
- exports.default = _default;
|