eslint-plugin-sonarjs
Version:
145 lines (144 loc) • 6.39 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isAssertion = isAssertion;
exports.isTSAssertion = isTSAssertion;
const typescript_1 = __importDefault(require("typescript"));
const ast_js_1 = require("../ast.js");
const module_js_1 = require("../module.js");
const module_ts_js_1 = require("../module-ts.js");
const TEST_MODULE = 'node:test';
/**
* Every `node:test` entry point resolves to a name rooted at the module itself, which
* {@link getFullyQualifiedName} normalises to `test` — `test` for a default import, `test.it` and
* `test.describe` for the named ones.
*/
const NODE_TEST_ROOT = 'test';
/** `node:test` entry points whose callback receives a `TestContext`. */
const TEST_FUNCTION_NAMES = new Set(['test', 'it']);
/**
* Whether `node` is a `<context>.assert.<method>(...)` call whose receiver is the context parameter
* of an enclosing `node:test` callback. Pure-AST: does not require type information.
*/
function isAssertion(context, node) {
if (node.type !== 'CallExpression') {
return false;
}
const receiver = getAssertReceiver(node.callee);
return receiver !== undefined && isTestContextParameter(context, node, receiver);
}
/**
* Counterpart of {@link isAssertion} for the type-aware visitor, which walks TypeScript nodes and
* never reaches the ESTree detector above.
*
* Deliberately structural rather than type-based: `TestContext` only resolves when `@types/node` is
* part of the program, and the context parameter is plain `any` when it is not — including in this
* repository's own type-aware rule tests. Matching the shape keeps the two paths in agreement
* instead of making recognition depend on whether types happen to be installed.
*/
function isTSAssertion(services, node) {
if (node.kind !== typescript_1.default.SyntaxKind.CallExpression) {
return false;
}
const call = node;
const receiver = getAssertReceiverTS(call.expression);
return (receiver !== undefined &&
(0, module_ts_js_1.importsModuleTS)(node.getSourceFile(), [TEST_MODULE]) &&
isTestContextParameterTS(services, call, receiver));
}
/** {@link getAssertReceiver} on a TypeScript callee. */
function getAssertReceiverTS(callee) {
if (!typescript_1.default.isPropertyAccessExpression(callee)) {
return undefined;
}
const assertAccess = callee.expression;
if (!typescript_1.default.isPropertyAccessExpression(assertAccess) ||
assertAccess.name.text !== 'assert' ||
!typescript_1.default.isIdentifier(assertAccess.expression)) {
return undefined;
}
return assertAccess.expression;
}
/** {@link isTestContextParameter} on the TypeScript AST. */
function isTestContextParameterTS(services, node, receiver) {
const checker = services.program.getTypeChecker();
const receiverSymbol = checker.getSymbolAtLocation(receiver);
if (receiverSymbol === undefined) {
return false;
}
for (let current = node.parent; current !== undefined; current = current.parent) {
if (!typescript_1.default.isFunctionExpression(current) && !typescript_1.default.isArrowFunction(current)) {
continue;
}
const [first] = current.parameters;
if (first === undefined ||
!typescript_1.default.isIdentifier(first.name) ||
checker.getSymbolAtLocation(first.name) !== receiverSymbol) {
continue;
}
const parent = current.parent;
return (parent !== undefined && typescript_1.default.isCallExpression(parent) && isNodeTestCalleeTS(parent.expression));
}
return false;
}
/**
* Whether `callee` names a `node:test` entry point. The source file is already known to import
* `node:test`, so matching the callee name is enough to separate `test(...)` / `it(...)` from an
* unrelated call that happens to take a callback.
*/
function isNodeTestCalleeTS(callee) {
if (typescript_1.default.isIdentifier(callee)) {
return TEST_FUNCTION_NAMES.has(callee.text);
}
return (typescript_1.default.isPropertyAccessExpression(callee) &&
(TEST_FUNCTION_NAMES.has(callee.name.text) || isNodeTestCalleeTS(callee.expression)));
}
/** The `t` of a `t.assert.<method>` callee, or `undefined` if `callee` is not that shape. */
function getAssertReceiver(callee) {
if (callee.type !== 'MemberExpression' || callee.computed) {
return undefined;
}
const assertAccess = callee.object;
if (assertAccess.type !== 'MemberExpression' ||
assertAccess.computed ||
!(0, ast_js_1.isIdentifier)(assertAccess.property, 'assert') ||
assertAccess.object.type !== 'Identifier') {
return undefined;
}
return assertAccess.object;
}
/**
* Whether `receiver` resolves to the first parameter of an enclosing `node:test` callback — i.e.
* the runner-supplied `TestContext`, rather than an unrelated local that happens to expose an
* `assert` member.
*/
function isTestContextParameter(context, node, receiver) {
const receiverVariable = (0, ast_js_1.getVariableFromScope)(context.sourceCode.getScope(receiver), receiver.name);
if (receiverVariable === undefined) {
return false;
}
const ancestors = context.sourceCode.getAncestors(node);
for (let i = ancestors.length - 1; i >= 0; i--) {
const enclosing = ancestors[i];
if (!(0, ast_js_1.isFunctionNode)(enclosing)) {
continue;
}
const [first] = enclosing.params;
if (first?.type !== 'Identifier' ||
(0, ast_js_1.getVariableFromScope)(context.sourceCode.getScope(first), first.name) !== receiverVariable) {
// A nearer function shadowing nothing keeps the search going; one that binds `name` to
// something else means the receiver is not a test context.
continue;
}
const parent = ancestors[i - 1];
return (parent?.type === 'CallExpression' &&
parent.arguments.includes(enclosing) &&
isNodeTestCall(context, parent));
}
return false;
}
function isNodeTestCall(context, call) {
return (0, module_js_1.getFullyQualifiedName)(context, call)?.split('.')[0] === NODE_TEST_ROOT;
}