eslint-plugin-sonarjs
Version:
338 lines (337 loc) • 11.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractChaiAssertion = extractChaiAssertion;
const ast_js_1 = require("./ast.js");
const assertions_chai_common_js_1 = require("./assertions-chai-common.js");
const module_js_1 = require("./module.js");
function extractChaiAssertion(context, node, allowGlobal) {
// chai supports three assertion styles:
// assert-style (`assert.strictEqual(value, value)`)
// expect-style (`expect(value).toBeTruthy()`)
// should-style (`value.should.be.truthy`)
if (node.type === 'CallExpression') {
return (extractChaiAssertAssertion(context, node, allowGlobal) ??
extractChaiExpectCallAssertion(context, node, allowGlobal) ??
extractChaiShouldCallAssertion(node));
}
if (node.type === 'MemberExpression') {
return (extractChaiExpectPropertyAssertion(context, node, allowGlobal) ??
extractChaiShouldPropertyAssertion(node));
}
return null;
}
/**
* Covers Chai's assert style assertions like `assert.strictEqual(value, value)` or `assert.ok(value)`
*/
function extractChaiAssertAssertion(context, node, allowGlobal) {
const assertCall = getChaiAssertCall(context, node, allowGlobal);
if (!assertCall) {
return null;
}
const predicate = getChaiAssertPredicate(assertCall.method);
if (predicate) {
const actual = (0, assertions_chai_common_js_1.getArgumentAtIndex)(node, 0);
if (!actual) {
return null;
}
return {
style: 'chai-assert',
kind: 'predicate',
predicate: predicate.predicate,
actual,
negated: predicate.negated,
node,
reportNode: actual,
};
}
const actual = (0, assertions_chai_common_js_1.getArgumentAtIndex)(node, 0);
const expected = (0, assertions_chai_common_js_1.getArgumentAtIndex)(node, 1);
if (!actual || !expected) {
return null;
}
return {
style: 'chai-assert',
kind: 'comparison',
comparison: getChaiAssertComparison(assertCall.method),
actual,
expected,
negated: assertCall.method.startsWith('not'),
node,
reportNode: assertCall.reportNode,
};
}
function getChaiAssertComparison(method) {
switch (method) {
case 'equal':
case 'notEqual':
return 'loose';
case 'deepEqual':
case 'notDeepEqual':
return 'deep';
default:
return 'strict';
}
}
function getChaiAssertCall(context, node, allowGlobal) {
const fqnMethod = getChaiAssertMethodFromFqn((0, module_js_1.getFullyQualifiedName)(context, node.callee));
if (fqnMethod) {
return { method: fqnMethod, reportNode: node.callee };
}
if (allowGlobal &&
(0, ast_js_1.isIdentifier)(node.callee, 'assert') &&
isGlobalAssertReference(context, node)) {
return { method: 'assert', reportNode: node.callee };
}
if (allowGlobal &&
(0, ast_js_1.isMethodCall)(node) &&
(0, ast_js_1.isIdentifier)(node.callee.object, 'assert') &&
isGlobalAssertReference(context, node)) {
const method = getChaiAssertMethodFromName(node.callee.property.name);
if (method) {
return { method, reportNode: node.callee.property };
}
}
return null;
}
// Chai global `assert` should not steal calls from a locally-bound `assert`
// such as `import assert from 'node:assert'`.
function isGlobalAssertReference(context, node) {
const variable = (0, ast_js_1.getVariableFromName)(context, 'assert', node);
return !variable || variable.defs.length === 0;
}
function getChaiAssertMethodFromFqn(fqn) {
if (!fqn?.startsWith('chai.assert')) {
return null;
}
const [, , method] = fqn.split('.');
return getChaiAssertMethodFromName(method ?? 'assert');
}
function getChaiAssertMethodFromName(name) {
switch (name) {
case 'assert':
case 'ok':
case 'isOk':
case 'isNotOk':
case 'isTrue':
case 'isFalse':
case 'isNull':
case 'isNotNull':
case 'isUndefined':
case 'isDefined':
case 'exists':
case 'notExists':
case 'equal':
case 'notEqual':
case 'strictEqual':
case 'notStrictEqual':
case 'deepEqual':
case 'notDeepEqual':
return name;
default:
return null;
}
}
function getChaiAssertPredicate(method) {
switch (method) {
case 'assert':
case 'ok':
case 'isOk':
return { predicate: 'truthy', negated: false };
case 'isNotOk':
return { predicate: 'truthy', negated: true };
// chai's `isTrue`/`isFalse` are strict (`=== true`/`=== false`), unlike `isOk`/`isNotOk`,
// which accept any truthy/falsy value
case 'isTrue':
return { predicate: 'true', negated: false };
case 'isFalse':
return { predicate: 'false', negated: false };
case 'isNull':
return { predicate: 'null', negated: false };
case 'isNotNull':
return { predicate: 'null', negated: true };
case 'isUndefined':
return { predicate: 'undefined', negated: false };
case 'isDefined':
return { predicate: 'defined', negated: false };
// chai's `exists`/`notExists` are stricter than `isDefined`: they require not-null AND
// not-undefined, unlike `isDefined`, which is only "not undefined"
case 'exists':
return { predicate: 'exists', negated: false };
case 'notExists':
return { predicate: 'exists', negated: true };
default:
return null;
}
}
/**
* Covers Chai's expect style assertions like `expect(value).toBeTruthy()` or `expect(value).toBeNull()`
*/
function extractChaiExpectCallAssertion(context, node, allowGlobal) {
if (!(0, ast_js_1.isMethodCall)(node)) {
return null;
}
const matcher = node.callee.property.name;
const comparison = getChaiComparison(matcher, node.callee.object);
if (!comparison) {
return null;
}
const chain = extractChaiExpectChain(context, node.callee.object, allowGlobal);
if (!chain) {
return null;
}
const expected = (0, assertions_chai_common_js_1.getArgumentAtIndex)(node, 0);
if (!expected) {
return null;
}
return {
style: 'chai-bdd',
kind: 'comparison',
comparison,
actual: chain.actual,
expected,
negated: chain.negated,
node,
reportNode: node.callee.property,
};
}
function extractChaiExpectPropertyAssertion(context, node, allowGlobal) {
if (node.computed || node.property.type !== 'Identifier') {
return null;
}
const predicate = (0, assertions_chai_common_js_1.getChaiPropertyPredicate)(node.property.name);
if (!predicate) {
return null;
}
const chain = extractChaiExpectChain(context, node.object, allowGlobal);
if (!chain) {
return null;
}
return {
style: 'chai-bdd',
kind: 'predicate',
predicate: predicate.predicate,
actual: chain.actual,
negated: chain.negated !== predicate.negated,
node,
reportNode: chain.actual,
};
}
function extractChaiExpectChain(context, node, allowGlobal) {
const chain = extractMemberChain(node);
if (!chain) {
return null;
}
const { base, properties } = chain;
// chai supports `expect(target [, message])`
if (base.type !== 'CallExpression' || base.arguments.length < 1) {
return null;
}
if (!isChaiExpectCall(context, base, allowGlobal)) {
return null;
}
const actual = (0, assertions_chai_common_js_1.getArgumentAtIndex)(base, 0);
if (!actual) {
return null;
}
return {
actual,
negated: properties.includes('not'),
properties,
};
}
function isChaiExpectCall(context, node, allowGlobal) {
return ((0, module_js_1.getFullyQualifiedName)(context, node.callee) === 'chai.expect' ||
(allowGlobal && (0, ast_js_1.isIdentifier)(node.callee, 'expect')));
}
// covers chai's should-style assertions like `value.should.be.truthy` or `value.should.be.null`
function extractChaiShouldCallAssertion(node) {
if (!(0, ast_js_1.isMethodCall)(node)) {
return null;
}
const matcher = node.callee.property.name;
const comparison = getChaiComparison(matcher, node.callee.object);
if (!comparison) {
return null;
}
const chain = extractChaiShouldChain(node.callee.object);
if (!chain) {
return null;
}
const expected = (0, assertions_chai_common_js_1.getArgumentAtIndex)(node, 0);
if (!expected) {
return null;
}
return {
style: 'chai-bdd',
kind: 'comparison',
comparison,
actual: chain.actual,
expected,
negated: chain.negated,
node,
reportNode: node.callee.property,
};
}
function extractChaiShouldPropertyAssertion(node) {
if (node.computed || node.property.type !== 'Identifier') {
return null;
}
const predicate = (0, assertions_chai_common_js_1.getChaiPropertyPredicate)(node.property.name);
if (!predicate) {
return null;
}
const chain = extractChaiShouldChain(node.object);
if (!chain) {
return null;
}
return {
style: 'chai-bdd',
kind: 'predicate',
predicate: predicate.predicate,
actual: chain.actual,
negated: chain.negated !== predicate.negated,
node,
reportNode: chain.actual,
};
}
function extractChaiShouldChain(node) {
const properties = [];
let current = node;
while (current.type === 'MemberExpression' && !current.computed) {
if (current.property.type !== 'Identifier') {
return null;
}
if (current.property.name === 'should') {
return {
actual: current.object,
negated: properties.includes('not'),
properties,
};
}
properties.unshift(current.property.name);
current = current.object;
}
return null;
}
function extractMemberChain(node) {
const properties = [];
let current = node;
while (current.type === 'MemberExpression' && !current.computed) {
if (current.property.type !== 'Identifier') {
return null;
}
properties.unshift(current.property.name);
current = current.object;
}
return { base: current, properties };
}
function getChaiComparison(matcher, chainNode) {
if (matcher === 'eql' || matcher === 'eqls') {
return 'deep';
}
if (matcher === 'equal' || matcher === 'equals' || matcher === 'eq') {
const chain = extractMemberChain(chainNode);
return chain?.properties.includes('deep') ? 'deep' : 'strict';
}
return null;
}