eslint-plugin-sonarjs
Version:
229 lines (228 loc) • 10.8 kB
JavaScript
;
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;
}