diff --git a/packages/assert/src/assert.js b/packages/assert/src/assert.js index e6713a1067b..ff214003030 100644 --- a/packages/assert/src/assert.js +++ b/packages/assert/src/assert.js @@ -13,6 +13,7 @@ import './types'; +/** @type {Assert} */ const globalAssert = globalThis.assert; if (globalAssert === undefined) { @@ -38,6 +39,18 @@ if (missing.length > 0) { ); } +const { details, quote } = globalAssert; + +export { globalAssert as assert, details, quote }; + +// DEPRECATED: Going forward we encourage the pattern over importing the +// abbreviation 'q' for quote. +// +// ```js +// import { quote as q, details as d } from '@agoric/assert'; +// ``` +export { quote as q }; + /** * Prepend the correct indefinite article onto a noun, typically a typeof result * e.g., "an Object" vs. "a Number" @@ -54,204 +67,4 @@ function an(str) { return `a ${str}`; } harden(an); - -/** - * To "declassify" and quote a substitution value used in a - * details`...` template literal, enclose that substitution expression - * in a call to `q`. This states that the argument should appear quoted (with - * `JSON.stringify`), in the error message of the thrown error. The payload - * itself is still passed unquoted to the console as it would be without q. - * - * Starting from the example in the `details` comment, say instead that the - * color the sky is supposed to be is also computed. Say that we still don't - * want to reveal the sky's actual color, but we do want the thrown error's - * message to reveal what color the sky was supposed to be: - * ```js - * assert.equal( - * sky.color, - * color, - * details`${sky.color} should be ${q(color)}`, - * ); - * ``` - * - * @typedef {Object} StringablePayload - * @property {() => string} toString How to print the payload - * - * @param {*} payload What to declassify - * @returns {StringablePayload} The declassified payload - */ -function quote(payload) { - return globalAssert.quote(payload); -} -harden(quote); - -/** - * Use the `details` function as a template literal tag to create - * informative error messages. The assertion functions take such messages - * as optional arguments: - * ```js - * assert(sky.isBlue(), details`${sky.color} should be "blue"`); - * ``` - * The details template tag returns an object that can print itself with the - * formatted message in two ways. It will report the real details to the - * console but include only the typeof information in the thrown error - * to prevent revealing secrets up the exceptional path. In the example - * above, the thrown error may reveal only that `sky.color` is a string, - * whereas the same diagnostic printed to the console reveals that the - * sky was green. - * - * WARNING: this function currently returns an unhardened result, as hardening - * proved to cause significant performance degradation. Consequently, callers - * should take care to use it only in contexts where this lack of hardening - * does not present a hazard. In current usage, a `details` template literal - * may only appear either as an argument to `assert`, where we know hardening - * won't matter, or inside another hardened object graph, where hardening is - * already ensured. However, there is currently no means to enfoce these - * constraints, so users are required to employ this function with caution. - * Our intent is to eventually have a lint rule that will check for - * inappropriate uses or find an alternative means of implementing `details` - * that does not encounter the performance issue. The final disposition of - * this is being discussed and tracked in issue #679 in the agoric-sdk - * repository. - * - * @typedef {Object} Complainer An object that has custom assert behaviour - * @property {() => Error} complain Return an Error to throw, and print details to console - * - * @typedef {string|Complainer} Details Either a plain string, or made by details`` - * - * @param {TemplateStringsArray | string[]} template The template to format - * @param {any[]} args Arguments to the template - * @returns {Complainer} The complainer for these details - */ -function details(template, ...args) { - return globalAssert.details(template, ...args); -} -harden(details); - -/** - * Fail an assertion, recording details to the console and - * raising an exception with just type information. - * - * The optional `optDetails` can be a string for backwards compatibility - * with the nodejs assertion library. - * - * @param {Details} [optDetails] The details of what was asserted - */ -function fail(optDetails = details`Assert failed`) { - return globalAssert.fail(optDetails); -} -// hardened under combinedAssert - -/* eslint-disable jsdoc/require-returns-check,jsdoc/valid-types */ -/** - * @param {*} flag The truthy/falsy value - * @param {Details} [optDetails] The details to throw - * @returns {asserts flag} - */ -/* eslint-enable jsdoc/require-returns-check,jsdoc/valid-types */ -function assert(flag, optDetails = details`Check failed`) { - globalAssert(flag, optDetails); -} -// hardened under combinedAssert - -/** - * Assert that two values must be `Object.is`. - * - * @template T - * @param {T} actual The value we received - * @param {T} expected What we wanted - * @param {Details} [optDetails] The details to throw - * @returns {void} - */ -function equal( - actual, - expected, - optDetails = details`Expected ${actual} is same as ${expected}`, -) { - globalAssert.equal(actual, expected, optDetails); -} -// hardened under combinedAssert - -/** - * Assert an expected typeof result. - * - * @type {AssertTypeof} - * @param {any} specimen The value to get the typeof - * @param {TypeName} typeName The expected name - * @param {Details} [optDetails] The details to throw - */ -const assertTypeof = (specimen, typeName, optDetails) => - /** @type {function(any, TypeName, Details): void} */ - globalAssert.typeof(specimen, typeName, optDetails); -// hardened under combinedAssert - -/** - * @param {any} specimen The value to get the typeof - * @param {Details} [optDetails] The details to throw - */ -const string = (specimen, optDetails) => - assertTypeof(specimen, 'string', optDetails); -// hardened under combinedAssert - -/** - * Adds debugger details to an error that will be inaccessible to any stack - * that catches the error but will be revealed to the console. - * - * @param {Error} error - * @param {Details} detailsNote - */ -const note = (error, detailsNote) => globalAssert.note(error, detailsNote); -// hardened under combinedAssert - -/* eslint-disable jsdoc/valid-types */ -/** - * assert that expr is truthy, with an optional details to describe - * the assertion. It is a tagged template literal like - * ```js - * assert(expr, details`....`);` - * ``` - * If expr is falsy, then the template contents are reported to the - * console and also in a thrown error. - * - * The literal portions of the template are assumed non-sensitive, as - * are the `typeof` types of the substitution values. These are - * assembled into the thrown error message. The actual contents of the - * substitution values are assumed sensitive, to be revealed to the - * console only. We assume only the virtual platform's owner can read - * what is written to the console, where the owner is in a privileged - * position over computation running on that platform. - * - * The optional `optDetails` can be a string for backwards compatibility - * with the nodejs assertion library. - * - * @type {typeof assert & { - * typeof: AssertTypeof, - * fail: typeof fail, - * equal: typeof equal, - * string: typeof string, - * note: typeof note, - * details: typeof details, - * quote: typeof quote - * }} - */ -/* eslint-enable jsdoc/valid-types */ -const combinedAssert = Object.assign(assert, { - fail, - equal, - typeof: assertTypeof, - string, - note, - details, - quote, -}); -harden(combinedAssert); - -export { combinedAssert as assert, details, an, quote }; - -// DEPRECATED: Going forward we encourage the pattern over importing the -// abbreviation 'q' for quote. -// -// ```js -// import { quote as q, details as d } from '@agoric/assert'; -// ``` -export { quote as q }; +export { an }; diff --git a/packages/assert/src/types.js b/packages/assert/src/types.js index 2e787db172e..94aad4280b1 100644 --- a/packages/assert/src/types.js +++ b/packages/assert/src/types.js @@ -1,67 +1,220 @@ +/* eslint-disable jsdoc/require-returns-check,jsdoc/valid-types */ // eslint-disable-next-line spaced-comment /// +// Based on +// https://github.com/Agoric/SES-shim/blob/master/packages/ses/src/error/types.js +// Coordinate edits until we refactor to avoid this duplication + +/** + * @callback BaseAssert + * The `assert` function itself. + * + * @param {*} flag The truthy/falsy value + * @param {Details=} optDetails The details to throw + * @param {ErrorConstructor=} ErrorConstructor An optional alternate error + * constructor to use. + * @returns {asserts flag} + */ + +/** + * @callback AssertFail + * + * The `assert.fail` method. + * + * Fail an assertion, recording details to the console and + * raising an exception with just type information. + * + * The optional `optDetails` can be a string for backwards compatibility + * with the nodejs assertion library. + * @param {Details=} optDetails The details of what was asserted + * @param {ErrorConstructor=} ErrorConstructor An optional alternate error + * constructor to use. + * @returns {never} + */ + +/** + * @callback AssertEqual + * The `assert.equal` method + * + * Assert that two values must be `Object.is`. + * @param {*} actual The value we received + * @param {*} expected What we wanted + * @param {Details=} optDetails The details to throw + * @param {ErrorConstructor=} ErrorConstructor An optional alternate error + * constructor to use. + * @returns {void} + */ + // Type all the overloads of the assertTypeof function. // There may eventually be a better way to do this, but // thems the breaks with Typescript 4.0. -/* eslint-disable jsdoc/valid-types */ /** - * @typedef {'bigint' | 'boolean' | 'function' | 'number' | 'object' | - * 'string' | 'symbol' | 'undefined'} TypeName - * * @callback AssertTypeofBigint * @param {any} specimen * @param {'bigint'} typename - * @param {Details} [optDetails] + * @param {Details=} optDetails * @returns {asserts specimen is bigint} * * @callback AssertTypeofBoolean * @param {any} specimen * @param {'boolean'} typename - * @param {Details} [optDetails] + * @param {Details=} optDetails * @returns {asserts specimen is boolean} * * @callback AssertTypeofFunction * @param {any} specimen * @param {'function'} typename - * @param {Details} [optDetails] + * @param {Details=} optDetails * @returns {asserts specimen is Function} * * @callback AssertTypeofNumber * @param {any} specimen * @param {'number'} typename - * @param {Details} [optDetails] + * @param {Details=} optDetails * @returns {asserts specimen is number} * * @callback AssertTypeofObject * @param {any} specimen * @param {'object'} typename - * @param {Details} [optDetails] + * @param {Details=} optDetails * @returns {asserts specimen is object} * * @callback AssertTypeofString * @param {any} specimen * @param {'string'} typename - * @param {Details} [optDetails] + * @param {Details=} optDetails * @returns {asserts specimen is string} * * @callback AssertTypeofSymbol * @param {any} specimen * @param {'symbol'} typename - * @param {Details} [optDetails] + * @param {Details=} optDetails * @returns {asserts specimen is symbol} * * @callback AssertTypeofUndefined * @param {any} specimen * @param {'undefined'} typename - * @param {Details} [optDetails] + * @param {Details=} optDetails * @returns {asserts specimen is undefined} */ -/* eslint-enable jsdoc/valid-types */ /** - * @typedef {AssertTypeofBigint & AssertTypeofBoolean & AssertTypeofFunction & - * AssertTypeofNumber & AssertTypeofObject & AssertTypeofString & - * AssertTypeofSymbol & AssertTypeofUndefined - * } AssertTypeof + * The `assert.typeof` method + * + * @typedef {AssertTypeofBigint & AssertTypeofBoolean & AssertTypeofFunction & AssertTypeofNumber & AssertTypeofObject & AssertTypeofString & AssertTypeofSymbol & AssertTypeofUndefined} AssertTypeof + */ + +/** + * @callback AssertString + * The `assert.string` method. + * + * `assert.string(v)` is equivalent to `assert.typeof(v, 'string')`. We + * special case this one because it is the most frequently used. + * + * Assert an expected typeof result. + * @param {any} specimen The value to get the typeof + * @param {Details=} optDetails The details to throw + */ + +/** + * @callback AssertNote + * The `assert.note` method. + * + * Annotate this error with these details, potentially to be used by an + * augmented console, like the causal console of `console.js`, to + * provide extra information associated with logged errors. + * + * @param {Error} error + * @param {Details} detailsNote + * @returns {void} + */ + +// ///////////////////////////////////////////////////////////////////////////// + +/** + * @typedef {{}} DetailsToken + * A call to the `details` template literal makes and returns a fresh details + * token, which is a frozen empty object associated with the arguments of that + * `details` template literal expression. + */ + +/** + * @typedef {string | DetailsToken} Details + * Either a plain string, or made by the `details` template literal tag. + */ + +/** + * @typedef {Object} StringablePayload + * Holds the payload passed to quote so that its printed form is visible. + * @property {() => string} toString How to print the payload + */ + +/** + * @callback AssertQuote + * + * To "declassify" and quote a substitution value used in a + * details`...` template literal, enclose that substitution expression + * in a call to `quote`. This states that the argument should appear quoted + * (as if with `JSON.stringify`), in the error message of the thrown error. The + * payload itself is still passed unquoted to the console as it would be + * without `quote`. + * + * Starting from the example in the `details` comment, say instead that the + * color the sky is supposed to be is also computed. Say that we still don't + * want to reveal the sky's actual color, but we do want the thrown error's + * message to reveal what color the sky was supposed to be: + * ```js + * assert.equal( + * sky.color, + * color, + * details`${sky.color} should be ${quote(color)}`, + * ); + * ``` + * + * The normal convention is to locally rename `quote` to `q` and + * `details` to `d` + * ```js + * const { details: d, quote: q } = assert; + * ``` + * so the above example would then be + * ```js + * assert.equal( + * sky.color, + * color, + * d`${sky.color} should be ${q(color)}`, + * ); + * ``` + * + * @param {*} payload What to declassify + * @returns {StringablePayload} The declassified payload + */ + +/** + * assert that expr is truthy, with an optional details to describe + * the assertion. It is a tagged template literal like + * ```js + * assert(expr, details`....`);` + * ``` + * + * The literal portions of the template are assumed non-sensitive, as + * are the `typeof` types of the substitution values. These are + * assembled into the thrown error message. The actual contents of the + * substitution values are assumed sensitive, to be revealed to + * the console only. We assume only the virtual platform's owner can read + * what is written to the console, where the owner is in a privileged + * position over computation running on that platform. + * + * The optional `optDetails` can be a string for backwards compatibility + * with the nodejs assertion library. + * + * @typedef { BaseAssert & { + * typeof: AssertTypeof, + * fail: AssertFail, + * equal: AssertEqual, + * string: AssertString, + * note: AssertNote, + * details: DetailsTag, + * quote: AssertQuote + * } } Assert */