eslint-plugin-sonarjs
Version:
305 lines (304 loc) • 11.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.predicateHolds = predicateHolds;
exports.freshReferencePredicateHolds = freshReferencePredicateHolds;
exports.strictEqualityHolds = strictEqualityHolds;
exports.resolveConstantPrimitiveValue = resolveConstantPrimitiveValue;
const ast_js_1 = require("../helpers/ast.js");
/**
* Whether a resolved constant `value` satisfies the given assertion `predicate`.
*/
function predicateHolds(predicate, value) {
switch (predicate) {
case 'truthy':
return Boolean(value);
case 'falsy':
return !value;
case 'true':
return value === true;
case 'false':
return value === false;
case 'defined':
return value !== undefined;
case 'undefined':
return value === undefined;
case 'null':
return value === null;
case 'exists':
return value !== null && value !== undefined;
default: {
const exhaustiveCheck = predicate;
return exhaustiveCheck;
}
}
}
/**
* Same as `predicateHolds`, specialized for freshly-created references: they're always truthy,
* always defined, always "existing", and never null, `=== true`, or `=== false` (a fresh
* reference is an object, never the boolean primitive itself).
*/
function freshReferencePredicateHolds(predicate) {
switch (predicate) {
case 'truthy':
case 'defined':
case 'exists':
return true;
case 'falsy':
case 'true':
case 'false':
case 'undefined':
case 'null':
return false;
default: {
const exhaustiveCheck = predicate;
return exhaustiveCheck;
}
}
}
/**
* Whether `style`'s strict-equality matcher is documented as comparing with the SameValue
* algorithm (`Object.is`) rather than `===`: Jest/Vitest/Bun's `toBe`, Playwright's `toBe`, and
* Node's `assert.strictEqual`/`notStrictEqual` all state this explicitly in their docs. This
* matters for `NaN` (`NaN === NaN` is `false`, but `Object.is(NaN, NaN)` is `true`) and signed
* zero (`-0 === 0` is `true`, but `Object.is(-0, 0)` is `false`). Jasmine's `toBe` and chai's
* `.equal`/`strictEqual` (including Cypress, which delegates to chai) are documented as using
* plain `===`. This is an exhaustive switch (rather than a Set) so that adding a new
* `AssertionStyle` forces an explicit choice instead of silently defaulting to `===`.
*/
function usesObjectIsForStrictEquality(style) {
switch (style) {
case 'jest-like':
case 'playwright':
case 'node-assert':
return true;
case 'jasmine':
case 'chai-bdd':
case 'chai-assert':
case 'cypress':
return false;
default: {
const exhaustiveCheck = style;
return exhaustiveCheck;
}
}
}
function strictEqualityHolds(style, actual, expected) {
return usesObjectIsForStrictEquality(style) ? Object.is(actual, expected) : actual === expected;
}
const UNARY_OPERATORS_PRESERVING_CONSTNESS = new Set([
'!',
'+',
'-',
'typeof',
]);
/**
* Real JS semantics for each supported binary/logical operator, applied to already-resolved
* constant operands. `in` and `instanceof` aren't meaningful for primitive constants and are
* intentionally left unsupported.
*
* Equality and logical operators accept the full `ConstantPrimitive` union as-is. The rest
* (arithmetic, relational, bitwise) reject mixed `number | bigint` operand types even when both
* sides are individually valid, so they're narrowed to `number`: a cast rather than a behavior
* change, since the real runtime values (and any resulting TypeError, e.g. mixing bigint and
* number) still flow through unchanged.
*/
const BINARY_OPERATOR_EVALUATORS = {
'+': (l, r) => l + r,
'-': (l, r) => l - r,
'*': (l, r) => l * r,
'/': (l, r) => l / r,
'%': (l, r) => l % r,
'**': (l, r) => l ** r,
'===': (l, r) => l === r,
'!==': (l, r) => l !== r,
// eslint-disable-next-line eqeqeq
'==': (l, r) => l == r,
// eslint-disable-next-line eqeqeq
'!=': (l, r) => l != r,
'<': (l, r) => l < r,
'<=': (l, r) => l <= r,
'>': (l, r) => l > r,
'>=': (l, r) => l >= r,
'<<': (l, r) => l << r,
'>>': (l, r) => l >> r,
'>>>': (l, r) => l >>> r,
'&': (l, r) => l & r,
'|': (l, r) => l | r,
'^': (l, r) => l ^ r,
'&&': (l, r) => (l ? r : l),
'||': (l, r) => l || r,
'??': (l, r) => l ?? r,
};
/**
* Resolves `node` to its constant primitive value when it's statically known (including through
* a resolved `const` binding), or `undefined` when it isn't resolvable. `visited` guards against
* mutually-recursive const references like `const a = b; const b = a;` (TDZ at runtime).
*/
function resolveConstantPrimitiveValue(context, node, visited) {
if (node.type === 'Identifier' && node.name !== 'undefined') {
visited ??= new Set();
const resolved = resolveConstBinding(context, node, visited);
if (resolved) {
return resolveConstantPrimitiveValue(context, resolved, visited);
}
}
switch (node.type) {
case 'Literal':
return resolveLiteral(node);
case 'Identifier':
return resolveUndefinedIdentifier(context, node);
case 'TemplateLiteral':
return node.expressions.length === 0
? { value: node.quasis[0].value.cooked ?? '' }
: undefined;
case 'UnaryExpression':
return resolveUnary(context, node, visited);
case 'BinaryExpression':
case 'LogicalExpression':
return resolveBinary(context, node, visited);
default:
return undefined;
}
}
function resolveLiteral(node) {
if (node.value === null ||
typeof node.value === 'boolean' ||
typeof node.value === 'string' ||
typeof node.value === 'number') {
return { value: node.value };
}
if ('bigint' in node && typeof node.bigint === 'string') {
return { value: BigInt(node.bigint) };
}
return undefined;
}
function resolveUndefinedIdentifier(context, node) {
if (node.name !== 'undefined') {
return undefined;
}
const variable = (0, ast_js_1.getVariableFromName)(context, 'undefined', node);
// global `undefined` has no user definitions; a shadowed binding does
return !variable || variable.defs.length === 0 ? { value: undefined } : undefined;
}
function resolveUnary(context, node, visited) {
// `void X` is always undefined regardless of X
if (node.operator === 'void') {
return { value: undefined };
}
if (!UNARY_OPERATORS_PRESERVING_CONSTNESS.has(node.operator)) {
return undefined;
}
const argument = resolveConstantPrimitiveValue(context, node.argument, visited);
if (!argument) {
return undefined;
}
try {
switch (node.operator) {
case '!':
return { value: !argument.value };
case '+':
return { value: +argument.value };
case '-':
return { value: -argument.value };
case 'typeof':
return { value: typeof argument.value };
default:
return undefined;
}
}
catch {
// e.g. mixing incompatible bigint/number operands throws a TypeError at evaluation time
return undefined;
}
}
function resolveBinary(context, node, visited) {
const evaluate = BINARY_OPERATOR_EVALUATORS[node.operator];
if (!evaluate) {
return undefined;
}
// each operand gets its own copy of `visited`: resolving the same identifier from two
// independent operands (e.g. `a + a`) is not a cycle, only re-entering an identifier that is
// already being expanded along the *same* resolution chain is. Cloning from the set as it was
// when entering this call (rather than sharing one mutable instance) keeps the two operands from
// seeing each other's bindings while still detecting genuine cycles through an outer identifier.
const left = resolveConstantPrimitiveValue(context, node.left, visited ? new Set(visited) : undefined);
if (!left) {
return undefined;
}
// short-circuiting operators don't need the right operand when it's never evaluated
if (node.operator === '&&' && !left.value) {
return { value: left.value };
}
if (node.operator === '||' && left.value) {
return { value: left.value };
}
if (node.operator === '??' && left.value !== null && left.value !== undefined) {
return { value: left.value };
}
const right = resolveConstantPrimitiveValue(context, node.right, visited ? new Set(visited) : undefined);
if (!right) {
return undefined;
}
try {
return { value: evaluate(left.value, right.value) };
}
catch {
// e.g. mixing incompatible bigint/number operands throws a TypeError at evaluation time
return undefined;
}
}
/**
* Scope types that represent a distinct execution context: code inside one only runs once the
* context itself is invoked, rather than inline with the surrounding statements.
*/
const EXECUTION_CONTEXT_SCOPE_TYPES = new Set([
'function',
'global',
'module',
'class-static-block',
]);
/**
* Returns the nearest enclosing scope that represents a distinct execution context (function,
* module, global, or class static block), walking up from `scope`.
*/
function getExecutionContextScope(scope) {
let current = scope;
while (current.upper && !EXECUTION_CONTEXT_SCOPE_TYPES.has(current.type)) {
current = current.upper;
}
return current;
}
/**
* Returns the initializer of `node` if it refers to a `const` binding declared with a plain identifier
* pattern (no destructuring) and that binding has not been visited already, otherwise undefined.
* Restricting to `const` ensures the binding's value is statically equal to the initializer at every read.
* The variable is added to `visited` so mutually-recursive references cannot loop forever.
*/
function resolveConstBinding(context, node, visited) {
const variable = (0, ast_js_1.getVariableFromName)(context, node.name, node);
if (!variable || visited.has(variable) || variable.defs.length !== 1) {
return undefined;
}
const def = variable.defs[0];
if (def.type !== 'Variable' || def.parent?.type !== 'VariableDeclaration') {
return undefined;
}
if (def.parent.kind !== 'const' || def.node.id.type !== 'Identifier') {
return undefined;
}
if (!node.range || !def.node.range) {
return undefined;
}
// a read that textually precedes its `const` declaration is only a guaranteed temporal-dead-zone
// violation when it runs in the same execution context as the declaration: reads deferred into a
// nested function (e.g. a test callback) may execute after the declaration has run, so source
// order alone can't prove a crash there. Restricting the check to the shared execution context
// avoids false negatives on such deferred reads while still catching genuine TDZ violations.
const readScope = context.sourceCode.getScope(node);
const sameExecutionContext = getExecutionContextScope(readScope) === getExecutionContextScope(variable.scope);
if (sameExecutionContext && node.range[0] < def.node.range[0]) {
return undefined;
}
visited.add(variable);
return def.node.init;
}