UNPKG

eslint-plugin-sonarjs

Version:
209 lines (208 loc) 9.71 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.rule = void 0; const generate_meta_js_1 = require("../helpers/generate-meta.js"); const ast_js_1 = require("../helpers/ast.js"); const mocha_style_test_frameworks_js_1 = require("../helpers/mocha-style-test-frameworks.js"); const assertion_detection_js_1 = require("../helpers/assertion-detection.js"); const meta = __importStar(require("./generated-meta.js")); const messages = { moveAssertion: 'Move this assertion into a test case or a lifecycle hook.', }; exports.rule = { meta: (0, generate_meta_js_1.generateMeta)(meta, { messages }), create(context) { if (!(0, assertion_detection_js_1.hasSupportedAssertionLibrary)(context)) { return {}; } // Dedupe by enclosing statement: a single `expect(x).toBe(y)` produces two // matching CallExpressions (the inner `expect()` and the outer chained // call), and Cypress `.should().and()` matches per link. They all share one // ExpressionStatement, so we report each statement at most once. const reportedStatements = new Set(); // Test structure is a whole-file property, so top-level assertions are only // resolved at Program:exit. let hasTestStructure = false; // Top-level assertion statements → whether any matched call is script-capable. const topLevelStatements = new Map(); const report = (statement) => { if (!reportedStatements.has(statement)) { reportedStatements.add(statement); context.report({ node: statement, messageId: 'moveAssertion' }); } }; return { CallExpression(node) { if (isTestStructureConstruct(context, node)) { hasTestStructure = true; } if (!(0, assertion_detection_js_1.isAssertion)(context, node)) { return; } if ((0, assertion_detection_js_1.isTypeLevelAssertion)(context, node)) { return; } const statement = findEnclosingExpressionStatement(context, node); if (statement === undefined) { return; } switch (classifyPlacement(context, node)) { case 'inside-function': return; // a test case, a lifecycle hook, or an unrelated helper case 'suite-body': report(statement); // runs at suite-collection time — always misplaced return; case 'top-level': { const scriptCapable = (topLevelStatements.get(statement) ?? false) || (0, assertion_detection_js_1.isScriptCapableAssertion)(context, node); topLevelStatements.set(statement, scriptCapable); break; } } }, 'Program:exit'() { // A top-level script-capable assertion in a file with no test structure is // a standalone script (the assertion is the test); a runner-bound one is // always misplaced. Flag everything else. for (const [statement, scriptCapable] of topLevelStatements) { if (hasTestStructure || !scriptCapable) { report(statement); } } }, }; }, }; /** * Whether `call` proves the file is a test file: a Mocha-style suite or test case, * a `@playwright/test`-bound `test(...)`, or a `test.describe(...)`. Binding-checked * (reusing `isMochaTestConstruct`), so a locally-defined `describe`/`it`/`test` does * not count. The name-only `test.describe` arm (for Playwright's extended-fixtures * pattern, where `test` does not resolve to `@playwright/test`) is restricted to * `describe` so a bare local `test()` is not mistaken for test structure. */ function isTestStructureConstruct(context, call) { return ((0, mocha_style_test_frameworks_js_1.isMochaTestConstruct)(context, call, mocha_style_test_frameworks_js_1.SUITE_FUNCTION_NAMES, { allowParameterized: true }) || (0, mocha_style_test_frameworks_js_1.isMochaTestConstruct)(context, call, mocha_style_test_frameworks_js_1.TEST_FUNCTION_NAMES, { allowParameterized: true }) || (0, mocha_style_test_frameworks_js_1.getPlaywrightTestQualifiers)(context, call.callee) !== undefined || isPlaywrightDescribe(context, call)); } /** * Returns the ExpressionStatement `node` is the expression of, or `undefined` when * the assertion is not a standalone statement (an argument, declaration, return, or * arrow concise body) — we only flag statement-level assertions, the strongest * signal it actually executes here. The walk climbs only the assertion's own * call/member chain (including `await`/`yield`/optional-chaining/non-null wrappers, * which matters for async-first libraries like supertest). */ function findEnclosingExpressionStatement(context, node) { const ancestors = context.sourceCode.getAncestors(node); let child = node; for (let i = ancestors.length - 1; i >= 0; i--) { const parent = ancestors[i]; switch (parent.type) { case 'ExpressionStatement': return parent; case 'MemberExpression': if (parent.object !== child) { return undefined; } break; case 'CallExpression': if (parent.callee !== child) { return undefined; } break; case 'ChainExpression': case 'AwaitExpression': case 'YieldExpression': break; default: // TSNonNullExpression (`x!.should()`) is a TS-only node absent from the // estree type union; treat it as a transparent chain wrapper too. if (parent.type !== 'TSNonNullExpression') { return undefined; } } child = parent; } return undefined; } /** * Where an assertion sits relative to test cases: * - `top-level`: no enclosing function — it runs at module load. * - `suite-body`: its nearest enclosing function is a suite callback * (`describe`/`context`/`suite` or Playwright's `test.describe`) — it runs at * suite-collection time, never as part of a test. * - `inside-function`: any other enclosing function — a test case, a lifecycle * hook, or an unrelated helper. We cannot prove it runs outside a test, so we * stay silent. */ function classifyPlacement(context, node) { const ancestors = context.sourceCode.getAncestors(node); const fnIndex = findEnclosingFunctionIndex(ancestors); if (fnIndex === -1) { return 'top-level'; } const enclosingFunction = ancestors[fnIndex]; const parent = ancestors[fnIndex - 1]; const isSuiteBody = parent?.type === 'CallExpression' && parent.arguments.includes(enclosingFunction) && isSuiteCallback(context, parent); return isSuiteBody ? 'suite-body' : 'inside-function'; } /** * Whether `call` is a suite declaration whose body runs at collection time: * a Mocha-style `describe`/`context`/`suite` (also covering Jest/Vitest/Jasmine/ * Cypress), or Playwright's `test.describe(...)`. */ function isSuiteCallback(context, call) { return ((0, mocha_style_test_frameworks_js_1.isMochaTestConstruct)(context, call, mocha_style_test_frameworks_js_1.SUITE_FUNCTION_NAMES, { allowParameterized: true }) || isPlaywrightDescribe(context, call)); } function isPlaywrightDescribe(context, call) { const qualifiers = (0, mocha_style_test_frameworks_js_1.getPlaywrightTestQualifiers)(context, call.callee) ?? (0, mocha_style_test_frameworks_js_1.getPlaywrightDescribeQualifiers)(call.callee); return qualifiers?.[0] === 'describe'; } function findEnclosingFunctionIndex(ancestors) { for (let i = ancestors.length - 1; i >= 0; i--) { if (ast_js_1.FUNCTION_NODES.includes(ancestors[i].type)) { return i; } } return -1; }