valid-expect-in-promise.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.default = void 0;
  6. var _utils = require("@typescript-eslint/utils");
  7. var _utils2 = require("./utils");
  8. const isPromiseChainCall = node => {
  9. if (node.type === _utils.AST_NODE_TYPES.CallExpression && node.callee.type === _utils.AST_NODE_TYPES.MemberExpression && (0, _utils2.isSupportedAccessor)(node.callee.property)) {
  10. // promise methods should have at least 1 argument
  11. if (node.arguments.length === 0) {
  12. return false;
  13. }
  14. switch ((0, _utils2.getAccessorValue)(node.callee.property)) {
  15. case 'then':
  16. return node.arguments.length < 3;
  17. case 'catch':
  18. case 'finally':
  19. return node.arguments.length < 2;
  20. }
  21. }
  22. return false;
  23. };
  24. const isTestCaseCallWithCallbackArg = (node, context) => {
  25. const jestCallFn = (0, _utils2.parseJestFnCall)(node, context);
  26. if ((jestCallFn === null || jestCallFn === void 0 ? void 0 : jestCallFn.type) !== 'test') {
  27. return false;
  28. }
  29. const isJestEach = jestCallFn.members.some(s => (0, _utils2.getAccessorValue)(s) === 'each');
  30. if (isJestEach && node.callee.type !== _utils.AST_NODE_TYPES.TaggedTemplateExpression) {
  31. // isJestEach but not a TaggedTemplateExpression, so this must be
  32. // the `jest.each([])()` syntax which this rule doesn't support due
  33. // to its complexity (see jest-community/eslint-plugin-jest#710)
  34. // so we return true to trigger bailout
  35. return true;
  36. }
  37. const [, callback] = node.arguments;
  38. const callbackArgIndex = Number(isJestEach);
  39. return callback && (0, _utils2.isFunction)(callback) && callback.params.length === 1 + callbackArgIndex;
  40. };
  41. const isPromiseMethodThatUsesValue = (node, identifier) => {
  42. const {
  43. name
  44. } = identifier;
  45. if (node.argument === null) {
  46. return false;
  47. }
  48. if (node.argument.type === _utils.AST_NODE_TYPES.CallExpression && node.argument.arguments.length > 0) {
  49. const nodeName = (0, _utils2.getNodeName)(node.argument);
  50. if (['Promise.all', 'Promise.allSettled'].includes(nodeName)) {
  51. const [firstArg] = node.argument.arguments;
  52. if (firstArg.type === _utils.AST_NODE_TYPES.ArrayExpression && firstArg.elements.some(nod => (0, _utils2.isIdentifier)(nod, name))) {
  53. return true;
  54. }
  55. }
  56. if (['Promise.resolve', 'Promise.reject'].includes(nodeName) && node.argument.arguments.length === 1) {
  57. return (0, _utils2.isIdentifier)(node.argument.arguments[0], name);
  58. }
  59. }
  60. return (0, _utils2.isIdentifier)(node.argument, name);
  61. };
  62. /**
  63. * Attempts to determine if the runtime value represented by the given `identifier`
  64. * is `await`ed within the given array of elements
  65. */
  66. const isValueAwaitedInElements = (name, elements) => {
  67. for (const element of elements) {
  68. if (element.type === _utils.AST_NODE_TYPES.AwaitExpression && (0, _utils2.isIdentifier)(element.argument, name)) {
  69. return true;
  70. }
  71. if (element.type === _utils.AST_NODE_TYPES.ArrayExpression && isValueAwaitedInElements(name, element.elements)) {
  72. return true;
  73. }
  74. }
  75. return false;
  76. };
  77. /**
  78. * Attempts to determine if the runtime value represented by the given `identifier`
  79. * is `await`ed as an argument along the given call expression
  80. */
  81. const isValueAwaitedInArguments = (name, call) => {
  82. let node = call;
  83. while (node) {
  84. if (node.type === _utils.AST_NODE_TYPES.CallExpression) {
  85. if (isValueAwaitedInElements(name, node.arguments)) {
  86. return true;
  87. }
  88. node = node.callee;
  89. }
  90. if (node.type !== _utils.AST_NODE_TYPES.MemberExpression) {
  91. break;
  92. }
  93. node = node.object;
  94. }
  95. return false;
  96. };
  97. const getLeftMostCallExpression = call => {
  98. let leftMostCallExpression = call;
  99. let node = call;
  100. while (node) {
  101. if (node.type === _utils.AST_NODE_TYPES.CallExpression) {
  102. leftMostCallExpression = node;
  103. node = node.callee;
  104. }
  105. if (node.type !== _utils.AST_NODE_TYPES.MemberExpression) {
  106. break;
  107. }
  108. node = node.object;
  109. }
  110. return leftMostCallExpression;
  111. };
  112. /**
  113. * Attempts to determine if the runtime value represented by the given `identifier`
  114. * is `await`ed or `return`ed within the given `body` of statements
  115. */
  116. const isValueAwaitedOrReturned = (identifier, body, context) => {
  117. const {
  118. name
  119. } = identifier;
  120. for (const node of body) {
  121. // skip all nodes that are before this identifier, because they'd probably
  122. // be affecting a different runtime value (e.g. due to reassignment)
  123. if (node.range[0] <= identifier.range[0]) {
  124. continue;
  125. }
  126. if (node.type === _utils.AST_NODE_TYPES.ReturnStatement) {
  127. return isPromiseMethodThatUsesValue(node, identifier);
  128. }
  129. if (node.type === _utils.AST_NODE_TYPES.ExpressionStatement) {
  130. // it's possible that we're awaiting the value as an argument
  131. if (node.expression.type === _utils.AST_NODE_TYPES.CallExpression) {
  132. if (isValueAwaitedInArguments(name, node.expression)) {
  133. return true;
  134. }
  135. const leftMostCall = getLeftMostCallExpression(node.expression);
  136. const jestFnCall = (0, _utils2.parseJestFnCall)(node.expression, context);
  137. if ((jestFnCall === null || jestFnCall === void 0 ? void 0 : jestFnCall.type) === 'expect' && leftMostCall.arguments.length > 0 && (0, _utils2.isIdentifier)(leftMostCall.arguments[0], name)) {
  138. if (jestFnCall.members.some(m => {
  139. const v = (0, _utils2.getAccessorValue)(m);
  140. return v === _utils2.ModifierName.resolves || v === _utils2.ModifierName.rejects;
  141. })) {
  142. return true;
  143. }
  144. }
  145. }
  146. if (node.expression.type === _utils.AST_NODE_TYPES.AwaitExpression && isPromiseMethodThatUsesValue(node.expression, identifier)) {
  147. return true;
  148. } // (re)assignment changes the runtime value, so if we've not found an
  149. // await or return already we act as if we've reached the end of the body
  150. if (node.expression.type === _utils.AST_NODE_TYPES.AssignmentExpression) {
  151. var _getNodeName;
  152. // unless we're assigning to the same identifier, in which case
  153. // we might be chaining off the existing promise value
  154. if ((0, _utils2.isIdentifier)(node.expression.left, name) && (_getNodeName = (0, _utils2.getNodeName)(node.expression.right)) !== null && _getNodeName !== void 0 && _getNodeName.startsWith(`${name}.`) && isPromiseChainCall(node.expression.right)) {
  155. continue;
  156. }
  157. break;
  158. }
  159. }
  160. if (node.type === _utils.AST_NODE_TYPES.BlockStatement && isValueAwaitedOrReturned(identifier, node.body, context)) {
  161. return true;
  162. }
  163. }
  164. return false;
  165. };
  166. const findFirstBlockBodyUp = node => {
  167. let parent = node;
  168. while (parent) {
  169. if (parent.type === _utils.AST_NODE_TYPES.BlockStatement) {
  170. return parent.body;
  171. }
  172. parent = parent.parent;
  173. }
  174. /* istanbul ignore next */
  175. throw new Error(`Could not find BlockStatement - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`);
  176. };
  177. const isDirectlyWithinTestCaseCall = (node, context) => {
  178. let parent = node;
  179. while (parent) {
  180. if ((0, _utils2.isFunction)(parent)) {
  181. var _parent;
  182. parent = parent.parent;
  183. return ((_parent = parent) === null || _parent === void 0 ? void 0 : _parent.type) === _utils.AST_NODE_TYPES.CallExpression && (0, _utils2.isTypeOfJestFnCall)(parent, context, ['test']);
  184. }
  185. parent = parent.parent;
  186. }
  187. return false;
  188. };
  189. const isVariableAwaitedOrReturned = (variable, context) => {
  190. const body = findFirstBlockBodyUp(variable); // it's pretty much impossible for us to track destructuring assignments,
  191. // so we return true to bailout gracefully
  192. if (!(0, _utils2.isIdentifier)(variable.id)) {
  193. return true;
  194. }
  195. return isValueAwaitedOrReturned(variable.id, body, context);
  196. };
  197. var _default = (0, _utils2.createRule)({
  198. name: __filename,
  199. meta: {
  200. docs: {
  201. category: 'Best Practices',
  202. description: 'Ensure promises that have expectations in their chain are valid',
  203. recommended: 'error'
  204. },
  205. messages: {
  206. expectInFloatingPromise: "This promise should either be returned or awaited to ensure the expects in it's chain are called"
  207. },
  208. type: 'suggestion',
  209. schema: []
  210. },
  211. defaultOptions: [],
  212. create(context) {
  213. let inTestCaseWithDoneCallback = false; // an array of booleans representing each promise chain we enter, with the
  214. // boolean value representing if we think a given chain contains an expect
  215. // in it's body.
  216. //
  217. // since we only care about the inner-most chain, we represent the state in
  218. // reverse with the inner-most being the first item, as that makes it
  219. // slightly less code to assign to by not needing to know the length
  220. const chains = [];
  221. return {
  222. CallExpression(node) {
  223. // there are too many ways that the done argument could be used with
  224. // promises that contain expect that would make the promise safe for us
  225. if (isTestCaseCallWithCallbackArg(node, context)) {
  226. inTestCaseWithDoneCallback = true;
  227. return;
  228. } // if this call expression is a promise chain, add it to the stack with
  229. // value of "false", as we assume there are no expect calls initially
  230. if (isPromiseChainCall(node)) {
  231. chains.unshift(false);
  232. return;
  233. } // if we're within a promise chain, and this call expression looks like
  234. // an expect call, mark the deepest chain as having an expect call
  235. if (chains.length > 0 && (0, _utils2.isTypeOfJestFnCall)(node, context, ['expect'])) {
  236. chains[0] = true;
  237. }
  238. },
  239. 'CallExpression:exit'(node) {
  240. // there are too many ways that the "done" argument could be used to
  241. // make promises containing expects safe in a test for us to be able to
  242. // accurately check, so we just bail out completely if it's present
  243. if (inTestCaseWithDoneCallback) {
  244. if ((0, _utils2.isTypeOfJestFnCall)(node, context, ['test'])) {
  245. inTestCaseWithDoneCallback = false;
  246. }
  247. return;
  248. }
  249. if (!isPromiseChainCall(node)) {
  250. return;
  251. } // since we're exiting this call expression (which is a promise chain)
  252. // we remove it from the stack of chains, since we're unwinding
  253. const hasExpectCall = chains.shift(); // if the promise chain we're exiting doesn't contain an expect,
  254. // then we don't need to check it for anything
  255. if (!hasExpectCall) {
  256. return;
  257. }
  258. const {
  259. parent
  260. } = (0, _utils2.findTopMostCallExpression)(node); // if we don't have a parent (which is technically impossible at runtime)
  261. // or our parent is not directly within the test case, we stop checking
  262. // because we're most likely in the body of a function being defined
  263. // within the test, which we can't track
  264. if (!parent || !isDirectlyWithinTestCaseCall(parent, context)) {
  265. return;
  266. }
  267. switch (parent.type) {
  268. case _utils.AST_NODE_TYPES.VariableDeclarator:
  269. {
  270. if (isVariableAwaitedOrReturned(parent, context)) {
  271. return;
  272. }
  273. break;
  274. }
  275. case _utils.AST_NODE_TYPES.AssignmentExpression:
  276. {
  277. if (parent.left.type === _utils.AST_NODE_TYPES.Identifier && isValueAwaitedOrReturned(parent.left, findFirstBlockBodyUp(parent), context)) {
  278. return;
  279. }
  280. break;
  281. }
  282. case _utils.AST_NODE_TYPES.ExpressionStatement:
  283. break;
  284. case _utils.AST_NODE_TYPES.ReturnStatement:
  285. case _utils.AST_NODE_TYPES.AwaitExpression:
  286. default:
  287. return;
  288. }
  289. context.report({
  290. messageId: 'expectInFloatingPromise',
  291. node: parent
  292. });
  293. }
  294. };
  295. }
  296. });
  297. exports.default = _default;