valid-expect.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.default = void 0;
  6. var _experimentalUtils = require("@typescript-eslint/experimental-utils");
  7. var _utils = require("./utils");
  8. /*
  9. * This implementation is ported from from eslint-plugin-jasmine.
  10. * MIT license, Tom Vincent.
  11. */
  12. /**
  13. * Async assertions might be called in Promise
  14. * methods like `Promise.x(expect1)` or `Promise.x([expect1, expect2])`.
  15. * If that's the case, Promise node have to be awaited or returned.
  16. *
  17. * @Returns CallExpressionNode
  18. */
  19. const getPromiseCallExpressionNode = node => {
  20. if (node.type === _experimentalUtils.AST_NODE_TYPES.ArrayExpression && node.parent && node.parent.type === _experimentalUtils.AST_NODE_TYPES.CallExpression) {
  21. node = node.parent;
  22. }
  23. if (node.type === _experimentalUtils.AST_NODE_TYPES.CallExpression && node.callee && node.callee.type === _experimentalUtils.AST_NODE_TYPES.MemberExpression && (0, _utils.isSupportedAccessor)(node.callee.object) && (0, _utils.getAccessorValue)(node.callee.object) === 'Promise' && node.parent) {
  24. return node;
  25. }
  26. return null;
  27. };
  28. const findPromiseCallExpressionNode = node => node.parent && node.parent.parent && [_experimentalUtils.AST_NODE_TYPES.CallExpression, _experimentalUtils.AST_NODE_TYPES.ArrayExpression].includes(node.parent.type) ? getPromiseCallExpressionNode(node.parent) : null;
  29. const getParentIfThenified = node => {
  30. const grandParentNode = node.parent && node.parent.parent;
  31. if (grandParentNode && grandParentNode.type === _experimentalUtils.AST_NODE_TYPES.CallExpression && grandParentNode.callee && (0, _utils.isExpectMember)(grandParentNode.callee) && ['then', 'catch'].includes((0, _utils.getAccessorValue)(grandParentNode.callee.property)) && grandParentNode.parent) {
  32. // Just in case `then`s are chained look one above.
  33. return getParentIfThenified(grandParentNode);
  34. }
  35. return node;
  36. };
  37. const isAcceptableReturnNode = (node, allowReturn) => {
  38. if (allowReturn && node.type === _experimentalUtils.AST_NODE_TYPES.ReturnStatement) {
  39. return true;
  40. }
  41. if (node.type === _experimentalUtils.AST_NODE_TYPES.ConditionalExpression && node.parent) {
  42. return isAcceptableReturnNode(node.parent, allowReturn);
  43. }
  44. return [_experimentalUtils.AST_NODE_TYPES.ArrowFunctionExpression, _experimentalUtils.AST_NODE_TYPES.AwaitExpression].includes(node.type);
  45. };
  46. const isNoAssertionsParentNode = node => node.type === _experimentalUtils.AST_NODE_TYPES.ExpressionStatement || node.type === _experimentalUtils.AST_NODE_TYPES.AwaitExpression && node.parent !== undefined && node.parent.type === _experimentalUtils.AST_NODE_TYPES.ExpressionStatement;
  47. const promiseArrayExceptionKey = ({
  48. start,
  49. end
  50. }) => `${start.line}:${start.column}-${end.line}:${end.column}`;
  51. var _default = (0, _utils.createRule)({
  52. name: __filename,
  53. meta: {
  54. docs: {
  55. category: 'Best Practices',
  56. description: 'Enforce valid `expect()` usage',
  57. recommended: 'error'
  58. },
  59. messages: {
  60. tooManyArgs: 'Expect takes at most {{ amount }} argument{{ s }}.',
  61. notEnoughArgs: 'Expect requires at least {{ amount }} argument{{ s }}.',
  62. modifierUnknown: 'Expect has no modifier named "{{ modifierName }}".',
  63. matcherNotFound: 'Expect must have a corresponding matcher call.',
  64. matcherNotCalled: 'Matchers must be called to assert.',
  65. asyncMustBeAwaited: 'Async assertions must be awaited{{ orReturned }}.',
  66. promisesWithAsyncAssertionsMustBeAwaited: 'Promises which return async assertions must be awaited{{ orReturned }}.'
  67. },
  68. type: 'suggestion',
  69. schema: [{
  70. type: 'object',
  71. properties: {
  72. alwaysAwait: {
  73. type: 'boolean',
  74. default: false
  75. },
  76. minArgs: {
  77. type: 'number',
  78. minimum: 1
  79. },
  80. maxArgs: {
  81. type: 'number',
  82. minimum: 1
  83. }
  84. },
  85. additionalProperties: false
  86. }]
  87. },
  88. defaultOptions: [{
  89. alwaysAwait: false,
  90. minArgs: 1,
  91. maxArgs: 1
  92. }],
  93. create(context, [{
  94. alwaysAwait,
  95. minArgs = 1,
  96. maxArgs = 1
  97. }]) {
  98. // Context state
  99. const arrayExceptions = new Set();
  100. const pushPromiseArrayException = loc => arrayExceptions.add(promiseArrayExceptionKey(loc));
  101. /**
  102. * Promise method that accepts an array of promises,
  103. * (eg. Promise.all), will throw warnings for the each
  104. * unawaited or non-returned promise. To avoid throwing
  105. * multiple warnings, we check if there is a warning in
  106. * the given location.
  107. */
  108. const promiseArrayExceptionExists = loc => arrayExceptions.has(promiseArrayExceptionKey(loc));
  109. return {
  110. CallExpression(node) {
  111. if (!(0, _utils.isExpectCall)(node)) {
  112. return;
  113. }
  114. const {
  115. expect,
  116. modifier,
  117. matcher
  118. } = (0, _utils.parseExpectCall)(node);
  119. if (expect.arguments.length < minArgs) {
  120. const expectLength = (0, _utils.getAccessorValue)(expect.callee).length;
  121. const loc = {
  122. start: {
  123. column: node.loc.start.column + expectLength,
  124. line: node.loc.start.line
  125. },
  126. end: {
  127. column: node.loc.start.column + expectLength + 1,
  128. line: node.loc.start.line
  129. }
  130. };
  131. context.report({
  132. messageId: 'notEnoughArgs',
  133. data: {
  134. amount: minArgs,
  135. s: minArgs === 1 ? '' : 's'
  136. },
  137. node,
  138. loc
  139. });
  140. }
  141. if (expect.arguments.length > maxArgs) {
  142. const {
  143. start
  144. } = expect.arguments[maxArgs].loc;
  145. const {
  146. end
  147. } = expect.arguments[node.arguments.length - 1].loc;
  148. const loc = {
  149. start,
  150. end: {
  151. column: end.column - 1,
  152. line: end.line
  153. }
  154. };
  155. context.report({
  156. messageId: 'tooManyArgs',
  157. data: {
  158. amount: maxArgs,
  159. s: maxArgs === 1 ? '' : 's'
  160. },
  161. node,
  162. loc
  163. });
  164. } // something was called on `expect()`
  165. if (!matcher) {
  166. if (modifier) {
  167. context.report({
  168. messageId: 'matcherNotFound',
  169. node: modifier.node.property
  170. });
  171. }
  172. return;
  173. }
  174. if ((0, _utils.isExpectMember)(matcher.node.parent)) {
  175. context.report({
  176. messageId: 'modifierUnknown',
  177. data: {
  178. modifierName: matcher.name
  179. },
  180. node: matcher.node.property
  181. });
  182. return;
  183. }
  184. if (!matcher.arguments) {
  185. context.report({
  186. messageId: 'matcherNotCalled',
  187. node: matcher.node.property
  188. });
  189. }
  190. const parentNode = matcher.node.parent;
  191. if (!parentNode.parent || !modifier || modifier.name === _utils.ModifierName.not) {
  192. return;
  193. }
  194. /**
  195. * If parent node is an array expression, we'll report the warning,
  196. * for the array object, not for each individual assertion.
  197. */
  198. const isParentArrayExpression = parentNode.parent.type === _experimentalUtils.AST_NODE_TYPES.ArrayExpression;
  199. const orReturned = alwaysAwait ? '' : ' or returned';
  200. /**
  201. * An async assertion can be chained with `then` or `catch` statements.
  202. * In that case our target CallExpression node is the one with
  203. * the last `then` or `catch` statement.
  204. */
  205. const targetNode = getParentIfThenified(parentNode);
  206. const finalNode = findPromiseCallExpressionNode(targetNode) || targetNode;
  207. if (finalNode.parent && // If node is not awaited or returned
  208. !isAcceptableReturnNode(finalNode.parent, !alwaysAwait) && // if we didn't warn user already
  209. !promiseArrayExceptionExists(finalNode.loc)) {
  210. context.report({
  211. loc: finalNode.loc,
  212. data: {
  213. orReturned
  214. },
  215. messageId: finalNode === targetNode ? 'asyncMustBeAwaited' : 'promisesWithAsyncAssertionsMustBeAwaited',
  216. node
  217. });
  218. if (isParentArrayExpression) {
  219. pushPromiseArrayException(finalNode.loc);
  220. }
  221. }
  222. },
  223. // nothing called on "expect()"
  224. 'CallExpression:exit'(node) {
  225. if ((0, _utils.isExpectCall)(node) && isNoAssertionsParentNode(node.parent)) {
  226. context.report({
  227. messageId: 'matcherNotFound',
  228. node
  229. });
  230. }
  231. }
  232. };
  233. }
  234. });
  235. exports.default = _default;