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