prefer-expect-assertions.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  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. const isExpectAssertionsOrHasAssertionsCall = expression => expression.type === _experimentalUtils.AST_NODE_TYPES.CallExpression && expression.callee.type === _experimentalUtils.AST_NODE_TYPES.MemberExpression && (0, _utils.isSupportedAccessor)(expression.callee.object, 'expect') && (0, _utils.isSupportedAccessor)(expression.callee.property) && ['assertions', 'hasAssertions'].includes((0, _utils.getAccessorValue)(expression.callee.property));
  9. const isFirstLineExprStmt = functionBody => functionBody[0] && functionBody[0].type === _experimentalUtils.AST_NODE_TYPES.ExpressionStatement;
  10. const suggestRemovingExtraArguments = (args, extraArgsStartAt) => ({
  11. messageId: 'suggestRemovingExtraArguments',
  12. fix: fixer => fixer.removeRange([args[extraArgsStartAt].range[0] - Math.sign(extraArgsStartAt), args[args.length - 1].range[1]])
  13. });
  14. const suggestions = [['suggestAddingHasAssertions', 'expect.hasAssertions();'], ['suggestAddingAssertions', 'expect.assertions();']];
  15. var _default = (0, _utils.createRule)({
  16. name: __filename,
  17. meta: {
  18. docs: {
  19. category: 'Best Practices',
  20. description: 'Suggest using `expect.assertions()` OR `expect.hasAssertions()`',
  21. recommended: false,
  22. suggestion: true
  23. },
  24. messages: {
  25. hasAssertionsTakesNoArguments: '`expect.hasAssertions` expects no arguments',
  26. assertionsRequiresOneArgument: '`expect.assertions` excepts a single argument of type number',
  27. assertionsRequiresNumberArgument: 'This argument should be a number',
  28. haveExpectAssertions: 'Every test should have either `expect.assertions(<number of assertions>)` or `expect.hasAssertions()` as its first expression',
  29. suggestAddingHasAssertions: 'Add `expect.hasAssertions()`',
  30. suggestAddingAssertions: 'Add `expect.assertions(<number of assertions>)`',
  31. suggestRemovingExtraArguments: 'Remove extra arguments'
  32. },
  33. type: 'suggestion',
  34. hasSuggestions: true,
  35. schema: [{
  36. type: 'object',
  37. properties: {
  38. onlyFunctionsWithAsyncKeyword: {
  39. type: 'boolean'
  40. },
  41. onlyFunctionsWithExpectInLoop: {
  42. type: 'boolean'
  43. },
  44. onlyFunctionsWithExpectInCallback: {
  45. type: 'boolean'
  46. }
  47. },
  48. additionalProperties: false
  49. }]
  50. },
  51. defaultOptions: [{
  52. onlyFunctionsWithAsyncKeyword: false,
  53. onlyFunctionsWithExpectInLoop: false,
  54. onlyFunctionsWithExpectInCallback: false
  55. }],
  56. create(context, [options]) {
  57. let expressionDepth = 0;
  58. let hasExpectInCallback = false;
  59. let hasExpectInLoop = false;
  60. let inTestCaseCall = false;
  61. let inForLoop = false;
  62. const shouldCheckFunction = testFunction => {
  63. if (!options.onlyFunctionsWithAsyncKeyword && !options.onlyFunctionsWithExpectInLoop && !options.onlyFunctionsWithExpectInCallback) {
  64. return true;
  65. }
  66. if (options.onlyFunctionsWithAsyncKeyword) {
  67. if (testFunction.async) {
  68. return true;
  69. }
  70. }
  71. if (options.onlyFunctionsWithExpectInLoop) {
  72. if (hasExpectInLoop) {
  73. return true;
  74. }
  75. }
  76. if (options.onlyFunctionsWithExpectInCallback) {
  77. if (hasExpectInCallback) {
  78. return true;
  79. }
  80. }
  81. return false;
  82. };
  83. const enterExpression = () => inTestCaseCall && expressionDepth++;
  84. const exitExpression = () => inTestCaseCall && expressionDepth--;
  85. const enterForLoop = () => inForLoop = true;
  86. const exitForLoop = () => inForLoop = false;
  87. return {
  88. FunctionExpression: enterExpression,
  89. 'FunctionExpression:exit': exitExpression,
  90. ArrowFunctionExpression: enterExpression,
  91. 'ArrowFunctionExpression:exit': exitExpression,
  92. ForStatement: enterForLoop,
  93. 'ForStatement:exit': exitForLoop,
  94. ForInStatement: enterForLoop,
  95. 'ForInStatement:exit': exitForLoop,
  96. ForOfStatement: enterForLoop,
  97. 'ForOfStatement:exit': exitForLoop,
  98. CallExpression(node) {
  99. if ((0, _utils.isTestCaseCall)(node)) {
  100. inTestCaseCall = true;
  101. return;
  102. }
  103. if ((0, _utils.isExpectCall)(node) && inTestCaseCall) {
  104. if (inForLoop) {
  105. hasExpectInLoop = true;
  106. }
  107. if (expressionDepth > 1) {
  108. hasExpectInCallback = true;
  109. }
  110. }
  111. },
  112. 'CallExpression:exit'(node) {
  113. if (!(0, _utils.isTestCaseCall)(node)) {
  114. return;
  115. }
  116. if (node.arguments.length < 2) {
  117. return;
  118. }
  119. const [, testFn] = node.arguments;
  120. if (!(0, _utils.isFunction)(testFn) || testFn.body.type !== _experimentalUtils.AST_NODE_TYPES.BlockStatement) {
  121. return;
  122. }
  123. if (!shouldCheckFunction(testFn)) {
  124. return;
  125. }
  126. hasExpectInLoop = false;
  127. hasExpectInCallback = false;
  128. const testFuncBody = testFn.body.body;
  129. if (!isFirstLineExprStmt(testFuncBody)) {
  130. context.report({
  131. messageId: 'haveExpectAssertions',
  132. node,
  133. suggest: suggestions.map(([messageId, text]) => ({
  134. messageId,
  135. fix: fixer => fixer.insertTextBeforeRange([testFn.body.range[0] + 1, testFn.body.range[1]], text)
  136. }))
  137. });
  138. return;
  139. }
  140. const testFuncFirstLine = testFuncBody[0].expression;
  141. if (!isExpectAssertionsOrHasAssertionsCall(testFuncFirstLine)) {
  142. context.report({
  143. messageId: 'haveExpectAssertions',
  144. node,
  145. suggest: suggestions.map(([messageId, text]) => ({
  146. messageId,
  147. fix: fixer => fixer.insertTextBefore(testFuncBody[0], text)
  148. }))
  149. });
  150. return;
  151. }
  152. if ((0, _utils.isSupportedAccessor)(testFuncFirstLine.callee.property, 'hasAssertions')) {
  153. if (testFuncFirstLine.arguments.length) {
  154. context.report({
  155. messageId: 'hasAssertionsTakesNoArguments',
  156. node: testFuncFirstLine.callee.property,
  157. suggest: [suggestRemovingExtraArguments(testFuncFirstLine.arguments, 0)]
  158. });
  159. }
  160. return;
  161. }
  162. if (!(0, _utils.hasOnlyOneArgument)(testFuncFirstLine)) {
  163. let {
  164. loc
  165. } = testFuncFirstLine.callee.property;
  166. const suggest = [];
  167. if (testFuncFirstLine.arguments.length) {
  168. loc = testFuncFirstLine.arguments[1].loc;
  169. suggest.push(suggestRemovingExtraArguments(testFuncFirstLine.arguments, 1));
  170. }
  171. context.report({
  172. messageId: 'assertionsRequiresOneArgument',
  173. suggest,
  174. loc
  175. });
  176. return;
  177. }
  178. const [arg] = testFuncFirstLine.arguments;
  179. if (arg.type === _experimentalUtils.AST_NODE_TYPES.Literal && typeof arg.value === 'number' && Number.isInteger(arg.value)) {
  180. return;
  181. }
  182. context.report({
  183. messageId: 'assertionsRequiresNumberArgument',
  184. node: arg
  185. });
  186. }
  187. };
  188. }
  189. });
  190. exports.default = _default;