UNPKG

eslint-plugin-sonarjs

Version:
106 lines (105 loc) 5.09 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isFunctionLike = isFunctionLike; exports.getAwaitedCall = getAwaitedCall; exports.findFirstTopLevelAwait = findFirstTopLevelAwait; exports.findTestsRegisteredAfter = findTestsRegisteredAfter; const ancestor_js_1 = require("../helpers/ancestor.js"); const ast_js_1 = require("../helpers/ast.js"); const mocha_style_test_frameworks_js_1 = require("../helpers/mocha-style-test-frameworks.js"); /** Type guard for any function-like node, including named function declarations used as helpers. */ function isFunctionLike(node) { return (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression' || node.type === 'FunctionDeclaration'); } /** * If `awaitNode` is an `AwaitExpression` whose argument is a `CallExpression`, returns that * `CallExpression`; otherwise returns `undefined`. */ function getAwaitedCall(awaitNode) { if (awaitNode.type === 'AwaitExpression' && awaitNode.argument.type === 'CallExpression') { return awaitNode.argument; } return undefined; } /** * Finds the earliest `await` (or `for await...of`) that runs directly in the function's body, * i.e. not nested inside another function. Returns `undefined` when there is none. */ function findFirstTopLevelAwait(context, callback) { if (callback.body === null) { return undefined; } const { sourceCode } = context; const stack = [callback.body]; let earliest; let earliestStart = Infinity; while (stack.length > 0) { const current = stack.pop(); if (current === undefined || (0, ast_js_1.isFunctionNode)(current)) { // A nested function has its own execution context; its awaits are not top-level here. continue; } if (isTopLevelAwait(current)) { const start = sourceCode.getRange(current)[0]; if (start < earliestStart) { earliest = current; earliestStart = start; } } stack.push(...(0, ancestor_js_1.childrenOf)(current, sourceCode.visitorKeys)); } return earliest; } function isTopLevelAwait(node) { return (node.type === 'AwaitExpression' || (node.type === 'ForOfStatement' && node.await)); } /** * Collects the test/suite registrations declared in the function body after the given await node. * Returns the callee of each such call, to be used as a secondary location. * * The walk descends into nested blocks (`if`, `try`, loops, ...) because a registration there is * dropped just like a top-level one: once the callback suspends on the await, everything that runs * afterwards does so too late to be registered. It stops at nested function boundaries — a test * inside a dropped nested suite's own callback belongs to that suite's registration, not to a * separate dropped registration, so only the nested suite's callee is collected. */ function findTestsRegisteredAfter(sourceCode, callback, awaitNode, testAndSuiteNames, beforeNode) { if (callback.body?.type !== 'BlockStatement') { return []; } // The offset at which the callback suspends. A `for await...of` suspends at the loop itself (the // first `iterator.next()` call, even for a synchronous iterable), so its entire body is dropped // too — use the loop's start. A plain `await` only drops what follows it — use the await's end. const suspendOffset = awaitNode.type === 'ForOfStatement' ? sourceCode.getRange(awaitNode)[0] : sourceCode.getRange(awaitNode)[1]; const endOffset = beforeNode !== undefined ? sourceCode.getRange(beforeNode)[0] : Infinity; const tests = []; const stack = [...(0, ancestor_js_1.childrenOf)(callback.body, sourceCode.visitorKeys)]; while (stack.length > 0) { const current = stack.pop(); if (current === undefined || (0, ast_js_1.isFunctionNode)(current)) { continue; } const nodeStart = sourceCode.getRange(current)[0]; if (current.type === 'ExpressionStatement' && current.expression.type === 'CallExpression' && nodeStart >= suspendOffset && nodeStart < endOffset) { const parts = (0, mocha_style_test_frameworks_js_1.getMochaCalleeParts)(current.expression.callee); if (parts && testAndSuiteNames.has(parts.base.name)) { tests.push(current.expression.callee); // Don't descend into the registered call: its own callback is a function boundary and any // tests it contains belong to its registration, not to a separate dropped one. continue; } } stack.push(...(0, ancestor_js_1.childrenOf)(current, sourceCode.visitorKeys)); } // The stack-based walk yields nodes out of source order; sort so secondary locations are emitted // deterministically (asserted in unit tests, dumped in ruling). return tests.sort((a, b) => sourceCode.getRange(a)[0] - sourceCode.getRange(b)[0]); }