UNPKG

eslint-plugin-sonarjs

Version:
145 lines (144 loc) • 6.39 kB
"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; }