'use strict'; const { getDocsUrl, isFunction } = require('./util'); const reportMsg = 'Promise should be returned to test its fulfillment or rejection'; const isThenOrCatch = node => { return ( node.property && (node.property.name === 'then' || node.property.name === 'catch') ); }; const isExpectCallPresentInFunction = body => { if (body.type === 'BlockStatement') { return body.body.find(line => { if (line.type === 'ExpressionStatement') return isExpectCall(line.expression); if (line.type === 'ReturnStatement') return isExpectCall(line.argument); }); } else { return isExpectCall(body); } }; const isExpectCall = expression => { return ( expression && expression.type === 'CallExpression' && expression.callee.type === 'MemberExpression' && expression.callee.object.type === 'CallExpression' && expression.callee.object.callee.name === 'expect' ); }; const reportReturnRequired = (context, node) => { context.report({ loc: { end: { column: node.parent.parent.loc.end.column, line: node.parent.parent.loc.end.line, }, start: node.parent.parent.loc.start, }, message: reportMsg, node, }); }; const isPromiseReturnedLater = (node, testFunctionBody) => { let promiseName; if (node.parent.parent.type === 'ExpressionStatement') { promiseName = node.parent.parent.expression.callee.object.name; } else if (node.parent.parent.type === 'VariableDeclarator') { promiseName = node.parent.parent.id.name; } const lastLineInTestFunc = testFunctionBody[testFunctionBody.length - 1]; return ( lastLineInTestFunc.type === 'ReturnStatement' && lastLineInTestFunc.argument.name === promiseName ); }; const isTestFunc = node => { return ( node.type === 'CallExpression' && (node.callee.name === 'it' || node.callee.name === 'test') ); }; const getFunctionBody = func => { if (func.body.type === 'BlockStatement') return func.body.body; return func.body; //arrow-short-hand-fn }; const getTestFunction = node => { let { parent } = node; while (parent) { if (isFunction(parent) && isTestFunc(parent.parent)) { return parent; } parent = parent.parent; } }; const isParentThenOrPromiseReturned = (node, testFunctionBody) => { return ( testFunctionBody.type === 'CallExpression' || testFunctionBody.type === 'NewExpression' || node.parent.parent.type === 'ReturnStatement' || isPromiseReturnedLater(node, testFunctionBody) || isThenOrCatch(node.parent.parent) ); }; const verifyExpectWithReturn = ( promiseCallbacks, node, context, testFunctionBody ) => { promiseCallbacks.some(promiseCallback => { if (promiseCallback && isFunction(promiseCallback)) { if ( isExpectCallPresentInFunction(promiseCallback.body) && !isParentThenOrPromiseReturned(node, testFunctionBody) ) { reportReturnRequired(context, node); return true; } } }); }; const isAwaitExpression = node => { return node.parent.parent && node.parent.parent.type === 'AwaitExpression'; }; const isHavingAsyncCallBackParam = testFunction => { try { return testFunction.params[0].type === 'Identifier'; } catch (e) { return false; } }; module.exports = { meta: { docs: { url: getDocsUrl(__filename), }, }, create(context) { return { MemberExpression(node) { if ( node.type === 'MemberExpression' && isThenOrCatch(node) && node.parent.type === 'CallExpression' && !isAwaitExpression(node) ) { const testFunction = getTestFunction(node); if (testFunction && !isHavingAsyncCallBackParam(testFunction)) { const testFunctionBody = getFunctionBody(testFunction); const [ fulfillmentCallback, rejectionCallback, ] = node.parent.arguments; // then block can have two args, fulfillment & rejection // then block can have one args, fulfillment // catch block can have one args, rejection // ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise verifyExpectWithReturn( [fulfillmentCallback, rejectionCallback], node, context, testFunctionBody ); } } }, }; }, };