node/lib/internal/assert/utils.js
Ruben Bridgewater d3f79aa65d
assert: allow printf-style messages as assertion error
Also add functions as allowed message input.
This allows to have leavy message computation to become cheaper.

PR-URL: https://github.com/nodejs/node/pull/58849
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
2025-10-17 20:15:17 +00:00

184 lines
5.3 KiB
JavaScript

'use strict';
const {
Error,
ErrorCaptureStackTrace,
ErrorPrototypeToString,
StringPrototypeCharCodeAt,
StringPrototypeReplace,
} = primordials;
const {
codes: {
ERR_AMBIGUOUS_ARGUMENT,
ERR_INVALID_ARG_TYPE,
},
isErrorStackTraceLimitWritable,
} = require('internal/errors');
const AssertionError = require('internal/assert/assertion_error');
const { isError } = require('internal/util');
const { format } = require('internal/util/inspect');
const {
getErrorSourceExpression,
} = require('internal/errors/error_source');
// Escape control characters but not \n and \t to keep the line breaks and
// indentation intact.
// eslint-disable-next-line no-control-regex
const escapeSequencesRegExp = /[\x00-\x08\x0b\x0c\x0e-\x1f]/g;
const meta = [
'\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004',
'\\u0005', '\\u0006', '\\u0007', '\\b', '',
'', '\\u000b', '\\f', '', '\\u000e',
'\\u000f', '\\u0010', '\\u0011', '\\u0012', '\\u0013',
'\\u0014', '\\u0015', '\\u0016', '\\u0017', '\\u0018',
'\\u0019', '\\u001a', '\\u001b', '\\u001c', '\\u001d',
'\\u001e', '\\u001f',
];
const escapeFn = (str) => meta[StringPrototypeCharCodeAt(str, 0)];
/**
* A function that derives the failure message from the actual and expected values.
* It is invoked only when the assertion fails.
*
* Other return values than a string are ignored.
* @callback MessageFactory
* @param {any} actual
* @param {any} expected
* @returns {string}
*/
/**
* Raw message input is always passed internally as a tuple array.
* Accepted shapes:
* - []
* - [string]
* - [string, ...any[]] (printf-like substitutions)
* - [Error]
* - [MessageFactory]
*
* Additional elements after [Error] or [MessageFactory] are rejected with ERR_AMBIGUOUS_ARGUMENT.
* A first element that is neither string, Error nor function is rejected with ERR_INVALID_ARG_TYPE.
* @typedef {[] | [string] | [string, ...any[]] | [Error] | [MessageFactory]} MessageTuple
*/
/**
* Options consumed by innerFail to construct and throw the AssertionError.
* @typedef {object} InnerFailOptions
* @property {any} actual Actual value
* @property {any} expected Expected value
* @property {MessageTuple} message Message
* @property {string} operator Operator
* @property {Function} stackStartFn Stack start function
* @property {'simple' | 'full'} [diff] Diff mode
* @property {boolean} [generatedMessage] Generated message
*/
function getErrMessage(fn) {
const tmpLimit = Error.stackTraceLimit;
const errorStackTraceLimitIsWritable = isErrorStackTraceLimitWritable();
// Make sure the limit is set to 1. Otherwise it could fail (<= 0) or it
// does to much work.
if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = 1;
// We only need the stack trace. To minimize the overhead use an object
// instead of an error.
const err = {};
ErrorCaptureStackTrace(err, fn);
if (errorStackTraceLimitIsWritable) Error.stackTraceLimit = tmpLimit;
let source = getErrorSourceExpression(err);
if (source) {
source = StringPrototypeReplace(source, escapeSequencesRegExp, escapeFn);
return `The expression evaluated to a falsy value:\n\n ${source}\n`;
}
}
/**
* @param {InnerFailOptions} obj
*/
function innerFail(obj) {
if (obj.message.length === 0) {
obj.message = undefined;
} else if (typeof obj.message[0] === 'string') {
if (obj.message.length > 1) {
obj.message = format(...obj.message);
} else {
obj.message = obj.message[0];
}
} else if (isError(obj.message[0])) {
if (obj.message.length > 1) {
throw new ERR_AMBIGUOUS_ARGUMENT(
'message',
`The error message was passed as error object "${ErrorPrototypeToString(obj.message[0])}" has trailing arguments that would be ignored.`,
);
}
throw obj.message[0];
} else if (typeof obj.message[0] === 'function') {
if (obj.message.length > 1) {
throw new ERR_AMBIGUOUS_ARGUMENT(
'message',
`The error message with function "${obj.message[0].name || 'anonymous'}" has trailing arguments that would be ignored.`,
);
}
try {
obj.message = obj.message[0](obj.actual, obj.expected);
if (typeof obj.message !== 'string') {
obj.message = undefined;
}
} catch {
// Ignore and use default message instead
obj.message = undefined;
}
} else {
throw new ERR_INVALID_ARG_TYPE(
'message',
['string', 'function'],
obj.message[0],
);
}
const error = new AssertionError(obj);
if (obj.generatedMessage !== undefined) {
error.generatedMessage = obj.generatedMessage;
}
throw error;
}
/**
* Internal ok handler delegating to innerFail for message handling.
* @param {Function} fn
* @param {...any} args
*/
function innerOk(fn, ...args) {
if (!args[0]) {
let generatedMessage = false;
let messageArgs;
if (args.length === 0) {
generatedMessage = true;
messageArgs = ['No value argument passed to `assert.ok()`'];
} else if (args.length === 1 || args[1] == null) {
generatedMessage = true;
messageArgs = [getErrMessage(fn)];
} else {
messageArgs = args.slice(1);
}
innerFail({
actual: args[0],
expected: true,
message: messageArgs,
operator: '==',
stackStartFn: fn,
generatedMessage,
});
}
}
module.exports = {
innerOk,
innerFail,
};