UNPKG

eslint-plugin-sonarjs

Version:
229 lines (228 loc) • 10.8 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.allAssertionFrameworks = allAssertionFrameworks; exports.hasAssertionEvidenceSource = hasAssertionEvidenceSource; exports.isAssertionEvidence = isAssertionEvidence; exports.isTypeScriptAssertionEvidence = isTypeScriptAssertionEvidence; exports.hasAssertionExecutionSource = hasAssertionExecutionSource; exports.getAssertionExecution = getAssertionExecution; const Angular = __importStar(require("./angular.js")); const AwsCdk = __importStar(require("./assertions-aws-cdk.js")); const Chai = __importStar(require("./chai.js")); const Cypress = __importStar(require("./cypress.js")); const assertion_detection_js_1 = require("./assertion-detection.js"); const module_js_1 = require("../module.js"); const NodeTest = __importStar(require("./node-test.js")); const Sinon = __importStar(require("./sinon.js")); const Supertest = __importStar(require("./supertest.js")); const Uvu = __importStar(require("./uvu.js")); const Vitest = __importStar(require("./vitest.js")); /** * Every assertion framework the analyzer can recognise, keyed by a stable profile name. * * This is the single catalog: a framework is added once here — its module specifiers and its AST * and type-aware detectors — and rules opt in by listing its key in a profile. Nothing here decides * what a match *means*; that is the rule's job, expressed as the profile's extension value (see * {@link AssertionExecution} for S8784's script-capable / runner-bound split). A rule that never * lists a key is blind to that framework, both for `imports`/`dependencies` gating and detection. * * `imports` are module specifiers whose presence in the linted file proves the framework is in * play; `dependencies` are packages that expose their assertion API as globals, so a manifest * dependency is enough and no import appears in the file. */ const assertionFrameworks = { angular: { imports: ['@angular/common/http/testing'], dependencies: [], isAssertion: () => false, isTSAssertion: Angular.isTSAssertion, }, chai: { imports: ['chai'], dependencies: [], isAssertion: Chai.isAssertion, isTSAssertion: (services, node) => Chai.isTSAssertion(services, node) || (0, assertion_detection_js_1.isChaiShouldTSAssertion)(node), }, sinon: { imports: ['sinon'], dependencies: [], isAssertion: Sinon.isAssertion, isTSAssertion: Sinon.isTSAssertion, }, supertest: { imports: ['supertest'], dependencies: [], isAssertion: Supertest.isAssertion, isTSAssertion: Supertest.isTSAssertion, }, nodeAssert: { imports: ['assert', 'assert/strict', 'node:assert', 'node:assert/strict'], dependencies: [], isAssertion: assertion_detection_js_1.isNodeAssertAssertion, isTSAssertion: assertion_detection_js_1.isNodeAssertTSAssertion, }, nodeTest: { // `node:test`'s own `t.assert.*`. Separate from `nodeAssert`: same underlying methods, but // reached through the runner-supplied test context, so neither the import nor the receiver // looks like `node:assert`. // // `imports` is deliberately empty even though `node:test` would qualify. Listing it would make // every `node:test` file an assertion-evidence source, which is a gating change — rules would // start analysing files they skip today. This entry exists to stop `t.assert.*` being missed in // files already under analysis, not to widen which files those are. imports: [], dependencies: [], isAssertion: NodeTest.isAssertion, isTSAssertion: NodeTest.isTSAssertion, }, uvu: { imports: ['uvu/assert'], dependencies: [], isAssertion: Uvu.isAssertion, isTSAssertion: Uvu.isTSAssertion, }, vitest: { imports: ['vitest', 'vitest/node'], dependencies: [], isAssertion: Vitest.isAssertion, isTSAssertion: Vitest.isTSAssertion, }, cypress: { // Cypress exposes `cy` as a global, so a manifest dependency is the only signal — the spec // file never imports the package. imports: [], dependencies: ['cypress'], isAssertion: (_context, node) => Cypress.isAssertion(node), isTSAssertion: (_services, node) => Cypress.isTSAssertion(node), }, globalExpect: { imports: ['bun:test', '@playwright/test'], dependencies: ['jasmine', 'jest', 'cypress', '@playwright/test'], isAssertion: (context, node) => node.type === 'CallExpression' && (0, assertion_detection_js_1.isGlobalExpectAssertion)(context, node), isTSAssertion: assertion_detection_js_1.isGlobalTSExpectAssertion, }, awsCdk: { imports: ['aws-cdk-lib/assertions'], dependencies: [], isAssertion: AwsCdk.isAssertion, isTSAssertion: AwsCdk.isTSAssertion, }, }; const ASSERTION_FRAMEWORK_KEYS = Object.keys(assertionFrameworks); /** * A profile covering every framework in {@link assertionFrameworks}, for rules that react to all of * them. Derived from the catalog rather than written out at the call site, so a framework added to * the catalog reaches those rules instead of being silently invisible to them — an opt-in catalog * makes an omission a false negative with nothing to fail. * * `overrides` carries the extension for the frameworks that need one; every other key gets `{}`. * Call it once per rule, at module scope: {@link getFrameworkEntries} caches on profile identity, * so a fresh object per `create()` would rebuild the entries for every linted file. */ function allAssertionFrameworks(overrides = {}) { return Object.fromEntries(ASSERTION_FRAMEWORK_KEYS.map(key => [key, overrides[key] ?? {}])); } /** * Whether the linted file imports, or the project depends on, any framework in `profile`. Rules * gate on this to stay silent in files that are not tests, before doing any per-node work. */ function hasAssertionEvidenceSource(context, profile) { return hasAssertionFrameworkSource(context, profile); } /** * Whether `node` is an assertion call from any framework in `profile`. Pure-AST: needs no type * information. Frameworks outside the profile are invisible, so this answers "did the rule see an * assertion it cares about?", not "is this an assertion?". */ function isAssertionEvidence(context, node, profile) { return getFrameworkEntries(profile).some(({ framework }) => framework.isAssertion(context, node)); } /** * Type-checker-aware counterpart of {@link isAssertionEvidence}, operating on TypeScript AST nodes. * Used when parser services are available to follow resolved types across declarations, and falls * back to each profile entry's {@link AssertionEvidenceExtension.isTSAssertionFallback} when the * catalog's own type-aware detector misses. */ function isTypeScriptAssertionEvidence(context, services, node, profile) { return getFrameworkEntries(profile).some(({ framework, extension }) => framework.isTSAssertion?.(services, node) || extension.isTSAssertionFallback?.(context, services, node)); } /** * {@link hasAssertionEvidenceSource} for profiles carrying an {@link AssertionExecution}. Same * check — the separate name keeps a rule's gating call and its per-node call reading against the * same vocabulary. */ function hasAssertionExecutionSource(context, profile) { return hasAssertionFrameworkSource(context, profile); } /** * The `extension` of the first profile framework that claims `node`, or `undefined` if none does. * * Classification is per *node*, not per statement, and a single assertion statement often has * several matching nodes that do not agree. A chai `expect(x).to.equal(y)` matches the Chai * detector on the inner `chai.expect(...)` call and the name-based global-`expect` detector on * the outer `.to.equal(...)` call, so with an {@link AssertionExecution} profile the same * statement yields both `script-capable` and `runner-bound`. Callers that reason about the * classification must combine the nodes of a statement themselves — see how S8784 ORs * `script-capable` across a statement rather than trusting any single node. */ function getAssertionExecution(context, node, profile) { return getFrameworkEntries(profile).find(({ framework }) => framework.isAssertion(context, node)) ?.extension; } function hasAssertionFrameworkSource(context, profile) { const entries = getFrameworkEntries(profile); return (0, module_js_1.importsOrDependsOnModule)(context, entries.flatMap(({ framework }) => framework.imports), entries.flatMap(({ framework }) => framework.dependencies)); } /** * Profiles are module constants, but the detection entry points run on every visited node, so the * resolved entries are cached per profile object rather than rebuilt per call. */ const frameworkEntriesCache = new WeakMap(); function getFrameworkEntries(profile) { const cached = frameworkEntriesCache.get(profile); if (cached !== undefined) { return cached; } const entries = Object.entries(profile).flatMap(([key, extension]) => { if (extension === undefined) { return []; } return [{ framework: assertionFrameworks[key], extension }]; }); frameworkEntriesCache.set(profile, entries); return entries; }