eslint-plugin-sonarjs
Version:
106 lines (105 loc) • 5.09 kB
JavaScript
;
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]);
}