diff --git a/packages/captp/src/captp.js b/packages/captp/src/captp.js index fdf527a08e..91d2b68d9b 100644 --- a/packages/captp/src/captp.js +++ b/packages/captp/src/captp.js @@ -322,9 +322,6 @@ export const makeCapTP = ( } const IS_REMOTE_PUMPKIN = harden({}); - /** - * @type {import('@endo/marshal').ConvertSlotToVal} - */ const assertValIsLocal = val => { const slot = valToSlot.get(val); if (slot && slot[1] === '-') { @@ -413,6 +410,7 @@ export const makeCapTP = ( epoch, questionID, target, + // @ts-expect-error Type 'unknown' is not assignable to type 'Passable'. method: serialize(harden([null, args])), }); return promise; @@ -428,6 +426,7 @@ export const makeCapTP = ( epoch, questionID, target, + // @ts-expect-error Type 'unknown' is not assignable to type 'Passable'. method: serialize(harden([prop, args])), }); return promise; diff --git a/packages/captp/test/test-trap.js b/packages/captp/test/test-trap.js index 63f795f7c5..fc478182e3 100644 --- a/packages/captp/test/test-trap.js +++ b/packages/captp/test/test-trap.js @@ -1,3 +1,4 @@ +/* global SharedArrayBuffer */ import test from '@endo/ses-ava/prepare-endo.js'; import { Worker } from 'worker_threads'; diff --git a/packages/daemon/src/types.d.ts b/packages/daemon/src/types.d.ts index e3097408d1..c8d5633783 100644 --- a/packages/daemon/src/types.d.ts +++ b/packages/daemon/src/types.d.ts @@ -568,7 +568,7 @@ export interface EndoHost extends EndoAgent { readerRef: ERef>, petName: string, ): Promise>; - storeValue(value: T, petName: string): Promise; + storeValue(value: T, petName: string): Promise; provideGuest( petName?: string, opts?: MakeHostOrGuestOptions, diff --git a/packages/eslint-plugin/lib/configs/internal.js b/packages/eslint-plugin/lib/configs/internal.js index f90280fcdf..704198661f 100644 --- a/packages/eslint-plugin/lib/configs/internal.js +++ b/packages/eslint-plugin/lib/configs/internal.js @@ -9,10 +9,11 @@ const dynamicConfig = { // typescript-eslint has its own config that must be dynamically referenced // to include vs. exclude non-"src" files because it cannot itself be dynamic. // https://github.com/microsoft/TypeScript/issues/30751 -const rootTsProjectGlob = './{js,ts}config.eslint-full.json'; +const rootTsProjectGlob = './tsconfig.eslint-full.json'; const parserOptions = { tsconfigRootDir: path.join(__dirname, '../../../..'), - project: [rootTsProjectGlob, 'packages/*/{js,ts}config.eslint.json'], + EXPERIMENTAL_useProjectService: true, + project: [rootTsProjectGlob], }; const fileGlobs = ['**/*.{js,ts}']; diff --git a/packages/exo/index.js b/packages/exo/index.js index 3924dd4a1a..1f95aae1ff 100644 --- a/packages/exo/index.js +++ b/packages/exo/index.js @@ -1,3 +1,6 @@ export * from './src/exo-makers.js'; +// eslint-disable-next-line import/export -- ESLint not aware of type exports in types.d.ts +export * from './src/types.js'; + export { GET_INTERFACE_GUARD } from './src/get-interface.js'; diff --git a/packages/exo/src/types.d.ts b/packages/exo/src/types.d.ts index 148b5a4bea..2131cfa1a6 100644 --- a/packages/exo/src/types.d.ts +++ b/packages/exo/src/types.d.ts @@ -1,4 +1,5 @@ import type { RemotableBrand } from '@endo/eventual-send'; +import type { RemotableObject } from '@endo/pass-style'; import type { InterfaceGuard, MethodGuard, Pattern } from '@endo/patterns'; import type { GetInterfaceGuard } from './get-interface.js'; @@ -110,7 +111,9 @@ export type FarClassOptions = { */ receiveInstanceTester?: ReceivePower | undefined; }; -export type Farable = M & RemotableBrand<{}, M>; +export type Farable = M & + RemotableBrand<{}, M> & + RemotableObject; export type Guarded = Farable>; export type GuardedKit> = { [K in keyof F]: Guarded; diff --git a/packages/exo/src/types.js b/packages/exo/src/types.js new file mode 100644 index 0000000000..879c4efdf9 --- /dev/null +++ b/packages/exo/src/types.js @@ -0,0 +1,2 @@ +/** @file Empty twin for .d.ts */ +export {}; diff --git a/packages/far/test/test-marshal-far-obj.js b/packages/far/test/test-marshal-far-obj.js index 773b6d885b..7ae827a814 100644 --- a/packages/far/test/test-marshal-far-obj.js +++ b/packages/far/test/test-marshal-far-obj.js @@ -5,6 +5,10 @@ import { q } from '@endo/errors'; import { Far, passStyleOf, getInterfaceOf } from '../src/index.js'; +/** + * @import {PassStyled} from '@endo/pass-style'; + */ + const { create, getPrototypeOf } = Object; // this only includes the tests that do not use liveSlots @@ -129,6 +133,7 @@ test('passStyleOf validation of remotables', t => { t.throws(() => passStyleOf(badRemotableProto3), NON_METHOD); t.throws(() => passStyleOf(badRemotableProto4), NON_METHOD); + // @ts-expect-error UNTIL https://github.com/microsoft/TypeScript/issues/38385 t.is(passStyleOf(sub(goodRemotableProto)), 'remotable'); t.throws(() => passStyleOf(sub(badRemotableProto1)), EXPECTED_PASS_STYLE); diff --git a/packages/marshal/src/deeplyFulfilled.js b/packages/marshal/src/deeplyFulfilled.js index 3cdbf07f8e..7a69e163b3 100644 --- a/packages/marshal/src/deeplyFulfilled.js +++ b/packages/marshal/src/deeplyFulfilled.js @@ -11,6 +11,7 @@ import { X, q } from '@endo/errors'; const { ownKeys } = Reflect; const { fromEntries } = Object; +// TODO return a type contingent on the parameter as deeplyFullfilledObject from agoric-sdk does /** * Given a Passable `val` whose pass-by-copy structure may contain leaf * promises, return a promise for a replacement Passable, @@ -38,7 +39,7 @@ const { fromEntries } = Object; * // revise the above comment to match. * // See https://github.com/endojs/endo/pull/1451 * - * @param {Passable} val + * @param {any} val * @returns {Promise} */ export const deeplyFulfilled = async val => { diff --git a/packages/marshal/src/encodePassable.js b/packages/marshal/src/encodePassable.js index 298043b18a..ac149b6bd9 100644 --- a/packages/marshal/src/encodePassable.js +++ b/packages/marshal/src/encodePassable.js @@ -38,7 +38,7 @@ const getSuffix = (str, index) => (index === 0 ? str : str.substring(index)); * string-named own properties. `recordNames` returns those name *reverse* * sorted, because that's how records are compared, encoded, and sorted. * - * @template T + * @template {Passable} T * @param {CopyRecord} record * @returns {string[]} */ @@ -55,7 +55,7 @@ harden(recordNames); * Assuming that `record` is a CopyRecord and `names` is `recordNames(record)`, * return the corresponding array of property values. * - * @template T + * @template {Passable} T * @param {CopyRecord} record * @param {string[]} names * @returns {T[]} @@ -324,7 +324,7 @@ const decodeLegacyStringSuffix = encoded => encoded; * format, each terminated by a space (which is part of the escaped range in * "compactOrdered" encoded strings). * - * @param {unknown[]} array + * @param {Passable[]} array * @param {(p: Passable) => string} encodePassable * @returns {string} */ @@ -399,7 +399,7 @@ const decodeCompactArray = (encoded, decodePassable, skip = 0) => { * This necessitated an undesirable amount of iteration and expansion; see * https://github.com/endojs/endo/pull/1260#discussion_r960369826 * - * @param {unknown[]} array + * @param {Passable[]} array * @param {(p: Passable) => string} encodePassable * @returns {string} */ @@ -728,6 +728,7 @@ const makeInnerDecode = (decodeStringSuffix, decodeArray, options) => { } } }; + // @ts-expect-error Type 'unknown' is not assignable to type 'Passable'. return innerDecode; }; diff --git a/packages/marshal/src/encodeToCapData.js b/packages/marshal/src/encodeToCapData.js index 78126ffedb..052b5795a1 100644 --- a/packages/marshal/src/encodeToCapData.js +++ b/packages/marshal/src/encodeToCapData.js @@ -19,10 +19,8 @@ import { } from '@endo/pass-style'; import { X, Fail, q } from '@endo/errors'; -/** @import {Passable} from '@endo/pass-style' */ -/** @import {Encoding} from './types.js' */ -/** @import {Remotable} from '@endo/pass-style' */ -/** @import {EncodingUnion} from './types.js' */ +/** @import {Passable, RemotableObject} from '@endo/pass-style' */ +/** @import {Encoding, EncodingUnion} from './types.js' */ const { ownKeys } = Reflect; const { isArray } = Array; @@ -62,7 +60,7 @@ const qclassMatches = (encoded, qclass) => /** * @typedef {object} EncodeToCapDataOptions * @property {( - * remotable: Remotable, + * remotable: RemotableObject, * encodeRecur: (p: Passable) => Encoding * ) => Encoding} [encodeRemotableToCapData] * @property {( @@ -117,7 +115,7 @@ export const makeEncodeToCapData = (encodeOptions = {}) => { * Readers must not care about this order anyway. We impose this requirement * mainly to reduce non-determinism exposed outside a vat. * - * @param {Passable} passable + * @param {any} passable * @returns {Encoding} except that `encodeToCapData` does not generally * `harden` this result before returning. Rather, `encodeToCapData` is not * directly exposed. @@ -269,11 +267,11 @@ harden(makeEncodeToCapData); * @property {( * encodedRemotable: Encoding, * decodeRecur: (e: Encoding) => Passable - * ) => (Promise|Remotable)} [decodeRemotableFromCapData] + * ) => (Promise|RemotableObject)} [decodeRemotableFromCapData] * @property {( * encodedPromise: Encoding, * decodeRecur: (e: Encoding) => Passable - * ) => (Promise|Remotable)} [decodePromiseFromCapData] + * ) => (Promise|RemotableObject)} [decodePromiseFromCapData] * @property {( * encodedError: Encoding, * decodeRecur: (e: Encoding) => Passable diff --git a/packages/marshal/src/encodeToSmallcaps.js b/packages/marshal/src/encodeToSmallcaps.js index 36f10ac522..9122e56c79 100644 --- a/packages/marshal/src/encodeToSmallcaps.js +++ b/packages/marshal/src/encodeToSmallcaps.js @@ -1,3 +1,4 @@ +// @ts-check /// // This module is based on the `encodePassable.js` in `@agoric/store`, @@ -159,7 +160,7 @@ export const makeEncodeToSmallcaps = (encodeOptions = {}) => { * Readers must not care about this order anyway. We impose this requirement * mainly to reduce non-determinism exposed outside a vat. * - * @param {Passable} passable + * @param {any} passable * @returns {SmallcapsEncoding} except that `encodeToSmallcaps` does not generally * `harden` this result before returning. Rather, `encodeToSmallcaps` is not * directly exposed. @@ -386,6 +387,7 @@ export const makeDecodeFromSmallcaps = (decodeOptions = {}) => { encoding, decodeFromSmallcaps, ); + // @ts-ignore XXX SmallCapsEncoding if (passStyleOf(result) !== 'remotable') { Fail`internal: decodeRemotableFromSmallcaps option must return a remotable: ${result}`; } diff --git a/packages/marshal/src/marshal-stringify.js b/packages/marshal/src/marshal-stringify.js index c07a0d52f8..e581cdefed 100644 --- a/packages/marshal/src/marshal-stringify.js +++ b/packages/marshal/src/marshal-stringify.js @@ -44,7 +44,7 @@ harden(stringify); /** * @param {string} str - * @returns {Passable} + * @returns {unknown} */ const parse = str => unserialize( diff --git a/packages/marshal/src/marshal.js b/packages/marshal/src/marshal.js index a2bf0f835b..ed4ebc2060 100644 --- a/packages/marshal/src/marshal.js +++ b/packages/marshal/src/marshal.js @@ -21,11 +21,12 @@ import { makeEncodeToSmallcaps, } from './encodeToSmallcaps.js'; -/** @import {ConvertSlotToVal, ConvertValToSlot, FromCapData, MakeMarshalOptions, ToCapData} from './types.js' */ -/** @import {Passable} from '@endo/pass-style' */ -/** @import {InterfaceSpec} from '@endo/pass-style' */ -/** @import {Encoding} from './types.js' */ -/** @import {RemotableObject as Remotable} from '@endo/pass-style' */ +/** + * @import {ConvertSlotToVal, ConvertValToSlot, FromCapData, MakeMarshalOptions, ToCapData} from './types.js'; + * @import {Passable, PassableCap, RemotableObject} from '@endo/pass-style'; + * @import {InterfaceSpec} from '@endo/pass-style'; + * @import {Encoding} from './types.js'; + */ const { defineProperties } = Object; const { isArray } = Array; @@ -78,7 +79,7 @@ export const makeMarshal = ( const slotMap = new Map(); /** - * @param {Remotable | Promise} passable + * @param {PassableCap} passable * @returns {{index: number, repeat: boolean}} */ const encodeSlotCommon = passable => { @@ -134,7 +135,7 @@ export const makeMarshal = ( if (serializeBodyFormat === 'capdata') { /** - * @param {Passable} passable + * @param {PassableCap} passable * @param {InterfaceSpec} [iface] * @returns {Encoding} */ @@ -148,9 +149,11 @@ export const makeMarshal = ( } }; + /** @type {(promise: RemotableObject, encodeRecur: (p: Passable) => Encoding) => Encoding} */ const encodeRemotableToCapData = (val, _encodeRecur) => encodeSlotToCapData(val, getInterfaceOf(val)); + /** @type {(promise: Promise, encodeRecur: (p: Passable) => Encoding) => Encoding} */ const encodePromiseToCapData = (promise, _encodeRecur) => encodeSlotToCapData(promise); @@ -184,7 +187,7 @@ export const makeMarshal = ( } else if (serializeBodyFormat === 'smallcaps') { /** * @param {string} prefix - * @param {Passable} passable + * @param {PassableCap} passable * @param {InterfaceSpec} [iface] * @returns {string} */ @@ -231,19 +234,20 @@ export const makeMarshal = ( }; const makeFullRevive = slots => { - /** @type {Map} */ + /** @type {Map} */ const valMap = new Map(); /** * @param {{iface?: string, index: number}} slotData - * @returns {Remotable | Promise} + * @returns {PassableCap} */ const decodeSlotCommon = slotData => { const { iface = undefined, index, ...rest } = slotData; ownKeys(rest).length === 0 || Fail`unexpected encoded slot properties ${q(ownKeys(rest))}`; - if (valMap.has(index)) { - return valMap.get(index); + const extant = valMap.get(index); + if (extant) { + return extant; } // TODO SECURITY HAZARD: must enfoce that remotable vs promise // is according to the encoded string. @@ -280,11 +284,13 @@ export const makeMarshal = ( const dName = decodeRecur(name); const dMessage = decodeRecur(message); // errorId is a late addition so be tolerant of its absence. - const dErrorId = errorId && decodeRecur(errorId); - typeof dName === 'string' || - Fail`invalid error name typeof ${q(typeof dName)}`; - typeof dMessage === 'string' || - Fail`invalid error message typeof ${q(typeof dMessage)}`; + const dErrorId = /** @type {string} */ (errorId && decodeRecur(errorId)); + if (typeof dName !== 'string') { + throw Fail`invalid error name typeof ${q(typeof dName)}`; + } + if (typeof dMessage !== 'string') { + throw Fail`invalid error message typeof ${q(typeof dMessage)}`; + } const errConstructor = getErrorConstructor(dName) || Error; const errorName = dErrorId === undefined @@ -340,8 +346,8 @@ export const makeMarshal = ( const makeDecodeSlotFromSmallcaps = prefix => { /** * @param {string} stringEncoding - * @param {(e: unknown) => Passable} _decodeRecur - * @returns {Remotable | Promise} + * @param {(e: unknown) => PassableCap} _decodeRecur + * @returns {RemotableObject | Promise} */ return (stringEncoding, _decodeRecur) => { assert(stringEncoding.charAt(0) === prefix); @@ -364,7 +370,9 @@ export const makeMarshal = ( }; const reviveFromSmallcaps = makeDecodeFromSmallcaps({ + // @ts-ignore XXX SmallCapsEncoding decodeRemotableFromSmallcaps, + // @ts-ignore XXX SmallCapsEncoding decodePromiseFromSmallcaps, decodeErrorFromSmallcaps, }); @@ -396,7 +404,7 @@ export const makeMarshal = ( // which should be considered fixed once we've completed the switch // to smallcaps. assertPassable(result); - return result; + return /** @type {PassableCap} */ (result); }; return harden({ diff --git a/packages/marshal/src/rankOrder.js b/packages/marshal/src/rankOrder.js index cfbb24924f..2018833417 100644 --- a/packages/marshal/src/rankOrder.js +++ b/packages/marshal/src/rankOrder.js @@ -284,9 +284,10 @@ harden(assertRankSorted); * function. This is a genuine bug for us NOW because sometimes we sort * in reverse order by passing a reversed rank comparison function. * - * @param {Iterable} passables + * @template {Passable} T + * @param {Iterable} passables * @param {RankCompare} compare - * @returns {Passable[]} + * @returns {T[]} */ export const sortByRank = (passables, compare) => { if (Array.isArray(passables)) { @@ -380,18 +381,20 @@ export const coveredEntries = (sorted, [leftIndex, rightIndex]) => { harden(coveredEntries); /** + * @template {Passable} T * @param {RankCompare} compare - * @param {Passable} a - * @param {Passable} b - * @returns {Passable} + * @param {T} a + * @param {T} b + * @returns {T} */ const maxRank = (compare, a, b) => (compare(a, b) >= 0 ? a : b); /** + * @template {Passable} T * @param {RankCompare} compare - * @param {Passable} a - * @param {Passable} b - * @returns {Passable} + * @param {T} a + * @param {T} b + * @returns {T} */ const minRank = (compare, a, b) => (compare(a, b) <= 0 ? a : b); diff --git a/packages/marshal/src/types.js b/packages/marshal/src/types.js index d1d42ccfd9..c70818bf34 100644 --- a/packages/marshal/src/types.js +++ b/packages/marshal/src/types.js @@ -1,19 +1,23 @@ // @ts-check export {}; +/** @import {Passable, PassableCap} from '@endo/pass-style' */ + /** * @template Slot + * @template {PassableCap} [Value=any] * @callback ConvertValToSlot - * @param {import("@endo/pass-style").PassableCap} val + * @param {Value} val * @returns {Slot} */ /** * @template Slot + * @template {PassableCap} [Value=any] * @callback ConvertSlotToVal * @param {Slot} slot * @param {string} [iface] - * @returns {import("@endo/pass-style").PassableCap} + * @returns {Value} */ /** @@ -86,7 +90,7 @@ export {}; /** * @template Slot * @callback ToCapData - * @param {any} val a Passable + * @param {Passable} val * @returns {CapData} */ @@ -174,8 +178,8 @@ export {}; * ordering would also compare magnitudes, and so agree with the rank ordering * of all values other than `NaN`. An array sorted by rank would enable range * queries by magnitude. - * @param {any} left a Passable - * @param {any} right a Passable + * @param {any} left + * @param {any} right * @returns {RankComparison} */ diff --git a/packages/marshal/src/types.test-d.ts b/packages/marshal/src/types.test-d.ts index 47ff6da6c3..a94dcd420b 100644 --- a/packages/marshal/src/types.test-d.ts +++ b/packages/marshal/src/types.test-d.ts @@ -1,6 +1,11 @@ import { expectType, expectNotType } from 'tsd'; -import type { PrimitiveStyle } from '@endo/pass-style'; +import { + Far, + type PrimitiveStyle, + type RemotableObject, +} from '@endo/pass-style'; +import { makeMarshal } from './marshal.js'; expectType('string'); expectType('number'); @@ -8,3 +13,21 @@ expectType('number'); expectType(1); // @ts-expect-error expectType('str'); + +type KCap = RemotableObject & { getKref: () => string; iface: () => string }; +const valToSlot = (s: KCap) => s.getKref(); +const slotToVal = (s: string) => null as unknown as KCap; +const marshal = makeMarshal(valToSlot, slotToVal); +const cycled = marshal.fromCapData(marshal.toCapData(null as unknown as KCap)); +expectType(cycled); + +const m = makeMarshal(); +type SlottedRemotable = { getBoardId: () => string }; +const foo = Far('foo'); +const foo1 = Far('foo', { getBoardId: () => 'board1' }); +const foo2 = Far('foo', { getBoardId: () => 'board2' }); +const bar = Far('bar'); +const bar1 = Far('bar', { getBoardId: () => 'board1' }); +m.toCapData(harden({ o: foo1 })); +m.toCapData(harden({ o: foo2 })); +m.toCapData(harden({ o: bar1 })); diff --git a/packages/marshal/test/test-marshal-far-obj.js b/packages/marshal/test/test-marshal-far-obj.js index c9cba7bc9d..d8cb838ca4 100644 --- a/packages/marshal/test/test-marshal-far-obj.js +++ b/packages/marshal/test/test-marshal-far-obj.js @@ -148,6 +148,7 @@ test('passStyleOf validation of remotables', t => { t.throws(() => passStyleOf(badRemotableProto3), NON_METHOD); t.throws(() => passStyleOf(badRemotableProto4), NON_METHOD); + // @ts-expect-error UNTIL https://github.com/microsoft/TypeScript/issues/38385 t.is(passStyleOf(sub(goodRemotableProto)), 'remotable'); t.throws(() => passStyleOf(sub(badRemotableProto1)), EXPECTED_PASS_STYLE); diff --git a/packages/marshal/test/test-marshal-stringify.js b/packages/marshal/test/test-marshal-stringify.js index 8272c96389..e72dd2a1a9 100644 --- a/packages/marshal/test/test-marshal-stringify.js +++ b/packages/marshal/test/test-marshal-stringify.js @@ -38,9 +38,11 @@ test('marshal stringify errors', t => { t.throws(() => stringify({}), { message: /Cannot pass non-frozen objects like .*. Use harden()/, }); + // @ts-expect-error intentional error t.throws(() => stringify(harden(new Uint8Array(1))), { message: 'Cannot pass mutable typed arrays like "[Uint8Array]".', }); + // @ts-expect-error intentional error t.throws(() => stringify(harden(new Int16Array(1))), { message: 'Cannot pass mutable typed arrays like "[Int16Array]".', }); diff --git a/packages/pass-style/.eslintignore b/packages/pass-style/.eslintignore new file mode 100644 index 0000000000..d76bcff161 --- /dev/null +++ b/packages/pass-style/.eslintignore @@ -0,0 +1,2 @@ +# typescript-eslint errors on this because it has no typecheck information, because tsc produced it for the `types.d.ts` instead +/src/types.js diff --git a/packages/pass-style/index.js b/packages/pass-style/index.js index f841e22a37..32ee735144 100644 --- a/packages/pass-style/index.js +++ b/packages/pass-style/index.js @@ -48,5 +48,5 @@ export { isCopyArray, } from './src/typeGuards.js'; -// eslint-disable-next-line import/export +// eslint-disable-next-line import/export -- ESLint not aware of type exports in types.d.ts export * from './src/types.js'; diff --git a/packages/pass-style/src/error.js b/packages/pass-style/src/error.js index 89aa72c0a6..d9ce1112df 100644 --- a/packages/pass-style/src/error.js +++ b/packages/pass-style/src/error.js @@ -4,7 +4,7 @@ import { X, q } from '@endo/errors'; import { assertChecker } from './passStyle-helpers.js'; /** @import {PassStyleHelper} from './internal-types.js' */ -/** @import {Checker, PassStyleOf} from './types.js' */ +/** @import {Checker, PassStyle, PassStyleOf} from './types.js' */ const { getPrototypeOf, getOwnPropertyDescriptors, hasOwn, entries } = Object; @@ -88,7 +88,7 @@ harden(isErrorLike); /** * @param {string} propName * @param {PropertyDescriptor} desc - * @param {PassStyleOf} passStyleOfRecur + * @param {(val: any) => PassStyle} passStyleOfRecur * @param {Checker} [check] * @returns {boolean} */ @@ -157,7 +157,7 @@ harden(checkRecursivelyPassableErrorPropertyDesc); /** * @param {unknown} candidate - * @param {PassStyleOf} passStyleOfRecur + * @param {(val: any) => PassStyle} passStyleOfRecur * @param {Checker} [check] * @returns {boolean} */ diff --git a/packages/pass-style/src/internal-types.js b/packages/pass-style/src/internal-types.js index 68d8c01435..d3796eb0de 100644 --- a/packages/pass-style/src/internal-types.js +++ b/packages/pass-style/src/internal-types.js @@ -23,6 +23,6 @@ export {}; * `assertValid` still needs to be called to determine if it * actually is valid. * @property {(candidate: any, - * passStyleOfRecur: PassStyleOf + * passStyleOfRecur: (val: any) => PassStyle * ) => void} assertValid */ diff --git a/packages/pass-style/src/make-far.js b/packages/pass-style/src/make-far.js index 072ccd8273..0baa8323cf 100644 --- a/packages/pass-style/src/make-far.js +++ b/packages/pass-style/src/make-far.js @@ -6,7 +6,7 @@ import { assertChecker, PASS_STYLE } from './passStyle-helpers.js'; import { assertIface, getInterfaceOf, RemotableHelper } from './remotable.js'; /** @import {RemotableBrand} from '@endo/eventual-send' */ -/** @import {InterfaceSpec} from './types.js' */ +/** @import {InterfaceSpec, RemotableObject} from './types.js' */ const { prototype: functionPrototype } = Function; const { @@ -61,7 +61,8 @@ const assertCanBeRemotable = candidate => * // https://github.com/Agoric/agoric-sdk/issues/804 * * @template {{}} T - * @param {InterfaceSpec} [iface] The interface specification for + * @template {InterfaceSpec} I + * @param {I} [iface] The interface specification for * the remotable. For now, a string iface must be "Remotable" or begin with * "Alleged: " or "DebugName: ", to serve as the alleged name. More * general ifaces are not yet implemented. This is temporary. We include the @@ -74,9 +75,10 @@ const assertCanBeRemotable = candidate => * @param {undefined} [props] Currently may only be undefined. * That plan is that own-properties are copied to the remotable * @param {T} [remotable] The object used as the remotable - * @returns {T & RemotableBrand<{}, T>} remotable, modified for debuggability + * @returns {T & RemotableObject & RemotableBrand<{}, T>}} remotable, modified for debuggability */ export const Remotable = ( + // @ts-expect-error I could have different subtype than string iface = 'Remotable', props = undefined, remotable = /** @type {T} */ ({}), @@ -124,7 +126,7 @@ export const Remotable = ( // COMMITTED! // We're committed, so keep the interface for future reference. assert(iface !== undefined); // To make TypeScript happy - return /** @type {T & RemotableBrand<{}, T>} */ (remotable); + return /** @type {any} */ (remotable); }; harden(Remotable); @@ -207,14 +209,18 @@ harden(Far); * when the function comes from elsewhere under less control. For functions * you author in place, better to use `Far` on their function literal directly. * + * @template {(...args: any[]) => any} F * @param {string} farName to be used only if `func` is not already a * far function. - * @param {(...args: any[]) => any} func + * @param {F} func + * @returns {F & RemotableObject & RemotableBrand<{}, F>} */ export const ToFarFunction = (farName, func) => { if (getInterfaceOf(func) !== undefined) { + // @ts-expect-error checked cast return func; } + // @ts-expect-error could be different subtype return Far(farName, (...args) => func(...args)); }; harden(ToFarFunction); diff --git a/packages/pass-style/src/makeTagged.js b/packages/pass-style/src/makeTagged.js index 0a80c69106..bfe1f2e805 100644 --- a/packages/pass-style/src/makeTagged.js +++ b/packages/pass-style/src/makeTagged.js @@ -6,6 +6,13 @@ import { assertPassable } from './passStyleOf.js'; const { create, prototype: objectPrototype } = Object; +/** + * @template {string} T + * @template {import('./types.js').Passable} P + * @param {T} tag + * @param {P} payload + * @returns {import('./types.js').CopyTagged} + */ export const makeTagged = (tag, payload) => { typeof tag === 'string' || Fail`The tag of a tagged record must be a string: ${tag}`; diff --git a/packages/pass-style/src/passStyle-helpers.js b/packages/pass-style/src/passStyle-helpers.js index bd1895f187..19f2c39001 100644 --- a/packages/pass-style/src/passStyle-helpers.js +++ b/packages/pass-style/src/passStyle-helpers.js @@ -30,6 +30,7 @@ export const hasOwnPropertyOf = (obj, prop) => apply(objectHasOwnProperty, obj, [prop]); harden(hasOwnPropertyOf); +// TODO try typing this; `=> val is {} too narrow, implies no properties export const isObject = val => Object(val) === val; harden(isObject); @@ -119,6 +120,11 @@ export const checkNormalProperty = ( }; harden(checkNormalProperty); +/** + * @template {import('./types.js').InterfaceSpec} T + * @param {import('./types.js').PassStyled} tagRecord + * @returns {T} + */ export const getTag = tagRecord => tagRecord[Symbol.toStringTag]; harden(getTag); @@ -135,7 +141,7 @@ harden(checkPassStyle); const makeCheckTagRecord = checkProto => { /** - * @param {{ [PASS_STYLE]: string }} tagRecord + * @param {import('./types.js').PassStyled} tagRecord * @param {PassStyle} passStyle * @param {Checker} [check] * @returns {boolean} diff --git a/packages/pass-style/src/passStyleOf.js b/packages/pass-style/src/passStyleOf.js index 1b6f497ba2..12d8563561 100644 --- a/packages/pass-style/src/passStyleOf.js +++ b/packages/pass-style/src/passStyleOf.js @@ -87,28 +87,26 @@ const makePassStyleOf = passStyleHelpers => { * structures, so without this cache, these algorithms could be * O(N**2) or worse. * - * @type {WeakMap} + * @type {WeakMap} */ const passStyleMemo = new WeakMap(); /** * @type {PassStyleOf} */ + // @ts-expect-error cast const passStyleOf = passable => { // Even when a WeakSet is correct, when the set has a shorter lifetime // than its keys, we prefer a Set due to expected implementation // tradeoffs. const inProgress = new Set(); - /** - * @type {PassStyleOf} - */ const passStyleOfRecur = inner => { const innerIsObject = isObject(inner); if (innerIsObject) { - if (passStyleMemo.has(inner)) { - // @ts-ignore TypeScript doesn't know that `get` after `has` is safe - return passStyleMemo.get(inner); + const innerStyle = passStyleMemo.get(inner); + if (innerStyle) { + return innerStyle; } !inProgress.has(inner) || Fail`Pass-by-copy data cannot be cyclic ${inner}`; @@ -123,9 +121,6 @@ const makePassStyleOf = passStyleHelpers => { return passStyle; }; - /** - * @type {PassStyleOf} - */ const passStyleOfInternal = inner => { const typestr = typeof inner; switch (typestr) { diff --git a/packages/pass-style/src/remotable.js b/packages/pass-style/src/remotable.js index 5e96992817..0a4584f8b4 100644 --- a/packages/pass-style/src/remotable.js +++ b/packages/pass-style/src/remotable.js @@ -14,8 +14,7 @@ import { /** * @import {Checker} from './types.js' - * @import {InterfaceSpec} from './types.js' - * @import {MarshalGetInterfaceOf} from './types.js' + * @import {InterfaceSpec, PassStyled} from './types.js' * @import {PassStyleHelper} from './internal-types.js' * @import {RemotableObject as Remotable} from './types.js' */ @@ -141,9 +140,9 @@ const checkRemotableProtoOf = (original, check) => { const confirmedRemotables = new WeakSet(); /** - * @param {Remotable} val + * @param {any} val * @param {Checker} [check] - * @returns {boolean} + * @returns {val is Remotable} */ const checkRemotable = (val, check) => { if (confirmedRemotables.has(val)) { @@ -164,15 +163,24 @@ const checkRemotable = (val, check) => { return result; }; -/** @type {MarshalGetInterfaceOf} */ +/** + * Simple semantics, just tell what interface (or undefined) a remotable has. + * @type {{ + * (val: PassStyled): T; + * (val: any): string | undefined; + * }} + * @returns the interface specification, or undefined if not a deemed to be a Remotable + */ export const getInterfaceOf = val => { if ( !isObject(val) || val[PASS_STYLE] !== 'remotable' || !checkRemotable(val) ) { + // @ts-expect-error narrowed return undefined; } + // @ts-expect-error narrowed return getTag(val); }; harden(getInterfaceOf); diff --git a/packages/pass-style/src/typeGuards.js b/packages/pass-style/src/typeGuards.js index 344f36bea2..facff7b81a 100644 --- a/packages/pass-style/src/typeGuards.js +++ b/packages/pass-style/src/typeGuards.js @@ -7,7 +7,7 @@ import { passStyleOf } from './passStyleOf.js'; * Check whether the argument is a pass-by-copy array, AKA a "copyArray" * in @endo/marshal terms * - * @param {Passable} arr + * @param {any} arr * @returns {arr is CopyArray} */ const isCopyArray = arr => passStyleOf(arr) === 'copyArray'; @@ -17,7 +17,7 @@ harden(isCopyArray); * Check whether the argument is a pass-by-copy record, AKA a * "copyRecord" in @endo/marshal terms * - * @param {Passable} record + * @param {any} record * @returns {record is CopyRecord} */ const isRecord = record => passStyleOf(record) === 'copyRecord'; @@ -33,13 +33,10 @@ const isRemotable = remotable => passStyleOf(remotable) === 'remotable'; harden(isRemotable); /** - * @callback AssertArray - * @param {Passable} array + * @param {any} array * @param {string=} optNameOfArray * @returns {asserts array is CopyArray} */ - -/** @type {AssertArray} */ const assertCopyArray = (array, optNameOfArray = 'Alleged array') => { const passStyle = passStyleOf(array); passStyle === 'copyArray' || @@ -50,13 +47,10 @@ const assertCopyArray = (array, optNameOfArray = 'Alleged array') => { harden(assertCopyArray); /** - * @callback AssertRecord - * @param {Passable} record + * @param {any} record * @param {string=} optNameOfRecord * @returns {asserts record is CopyRecord} */ - -/** @type {AssertRecord} */ const assertRecord = (record, optNameOfRecord = 'Alleged record') => { const passStyle = passStyleOf(record); passStyle === 'copyRecord' || @@ -67,13 +61,10 @@ const assertRecord = (record, optNameOfRecord = 'Alleged record') => { harden(assertRecord); /** - * @callback AssertRemotable * @param {Passable} remotable * @param {string=} optNameOfRemotable * @returns {asserts remotable is RemotableObject} */ - -/** @type {AssertRemotable} */ const assertRemotable = ( remotable, optNameOfRemotable = 'Alleged remotable', diff --git a/packages/pass-style/src/types.d.ts b/packages/pass-style/src/types.d.ts new file mode 100644 index 0000000000..65080677cc --- /dev/null +++ b/packages/pass-style/src/types.d.ts @@ -0,0 +1,203 @@ +/* eslint-disable no-use-before-define */ +import { PASS_STYLE } from './passStyle-helpers.js'; + +/** + * Matches any [primitive value](https://developer.mozilla.org/en-US/docs/Glossary/Primitive). + */ +export type Primitive = + | null + | undefined + | string + | number + | boolean + | symbol + | bigint; + +export type PrimitiveStyle = + | 'undefined' + | 'null' + | 'boolean' + | 'number' + | 'bigint' + | 'string' + | 'symbol'; + +export type ContainerStyle = 'copyRecord' | 'copyArray' | 'tagged'; + +export type PassStyle = + | PrimitiveStyle + | ContainerStyle + | 'remotable' + | 'error' + | 'promise'; + +export type TaggedOrRemotable = 'tagged' | 'remotable'; + +/** + * Tagged has own [PASS_STYLE]: "tagged", [Symbol.toStringTag]: $tag. + * + * Remotable has a prototype chain in which the penultimate object has own [PASS_STYLE]: "remotable", [Symbol.toStringTag]: $iface (where both $tag and $iface must be strings, and the latter must either be "Remotable" or start with "Alleged: " or "DebugName: "). + */ +export type PassStyled = { + [PASS_STYLE]: S; + [Symbol.toStringTag]: I; +}; + +export type ExtractStyle

> = P[typeof PASS_STYLE]; + +export type PassByCopy = + | Primitive + | Error + | CopyArray + | CopyRecord + | CopyTagged; + +export type PassByRef = + | RemotableObject + | Promise + | Promise; + +/** + * A Passable is acyclic data that can be marshalled. It must be hardened to + * remain + * stable (even if some components are proxies; see PureData restriction below), + * and is classified by PassStyle: + * * Atomic primitive values have a PrimitiveStyle (PassStyle + * 'undefined' | 'null' | 'boolean' | 'number' | 'bigint' + * | 'string' | 'symbol'). + * * Containers aggregate other Passables into + * * sequences as CopyArrays (PassStyle 'copyArray'), or + * * string-keyed dictionaries as CopyRecords (PassStyle 'copyRecord'), or + * * higher-level types as CopyTaggeds (PassStyle 'tagged'). + * * PassableCaps (PassStyle 'remotable' | 'promise') expose local values to + * remote interaction. + * * As a special case to support system observability, error objects are + * Passable (PassStyle 'error'). + * + * A Passable is essentially a pass-by-copy superstructure with a + * pass-by-reference + * exit point at the site of each PassableCap (which marshalling represents + * using 'slots'). + */ +export type Passable< + PC extends PassableCap = PassableCap, + E extends Error = Error, +> = Primitive | Container | PC | E; + +export type Container = + | CopyArrayI + | CopyRecordI + | CopyTaggedI; +interface CopyArrayI + extends CopyArray> {} +interface CopyRecordI + extends CopyRecord> {} +interface CopyTaggedI + extends CopyTagged> {} + +export type PassStyleOf = { + (p: undefined): 'undefined'; + (p: string): 'string'; + (p: boolean): 'boolean'; + (p: number): 'number'; + (p: bigint): 'bigint'; + (p: symbol): 'symbol'; + (p: null): 'null'; + (p: Promise): 'promise'; + (p: Error): 'error'; + (p: CopyTagged): 'tagged'; + (p: any[]): 'copyArray'; + (p: Iterable): 'remotable'; + (p: Iterator): 'remotable'; + >(p: T): ExtractStyle; + (p: { [key: string]: any }): 'copyRecord'; + (p: any): PassStyle; +}; +/** + * A Passable is PureData when its entire data structure is free of PassableCaps + * (remotables and promises) and error objects. + * PureData is an arbitrary composition of primitive values into CopyArray + * and/or + * CopyRecord and/or CopyTagged containers (or a single primitive value with no + * container), and is fully pass-by-copy. + * + * This restriction assures absence of side effects and interleaving risks *given* + * that none of the containers can be a Proxy instance. + * TODO SECURITY BUG we plan to enforce this, giving PureData the same security + * properties as the proposed + * [Records and Tuples](https://github.com/tc39/proposal-record-tuple). + * + * Given this (currently counter-factual) assumption, a PureData value cannot + * be used as a communications channel, + * and can therefore be safely shared with subgraphs that should not be able + * to communicate with each other. + * Without that assumption, such a guarantee requires a marshal-unmarshal round + * trip (as exists between vats) to produce data structures disconnected from + * any potential proxies. + */ +export type PureData = Passable; +/** + * An object marked as remotely accessible using the `Far` or `Remotable` + * functions, or a local presence representing such a remote object. + * + * A more natural name would be Remotable, but that could be confused with the + * value of the `Remotable` export of this module (a function). + */ +export type RemotableObject = PassStyled< + 'remotable', + I +>; +/** + * The authority-bearing leaves of a Passable's pass-by-copy superstructure. + */ +export type PassableCap = Promise | RemotableObject; +/** + * A Passable sequence of Passable values. + */ +export type CopyArray = Array; + +/** + * A Passable dictionary in which each key is a string and each value is Passable. + */ +export type CopyRecord = Record; +/** + * A Passable "tagged record" with semantics specific to the tag identified in + * the `[Symbol.toStringTag]` property (such as 'copySet', 'copyBag', + * or 'copyMap'). + * It must have a property with key equal to the `PASS_STYLE` export and + * value 'tagged' + * and no other properties except `[Symbol.toStringTag]` and `payload`. + */ +export type CopyTagged< + Tag extends string = string, + Payload extends Passable = any, +> = PassStyled<'tagged', Tag> & { + payload: Payload; +}; +/** + * This is an interface specification. + * For now, it is just a string, but we retain the option to make it `PureData`. + * Either way, it must remain pure, so that it can be safely shared by subgraphs + * that are not supposed to be able to communicate. + */ +export type InterfaceSpec = string; +/** + * Internal to a useful pattern for writing checking logic + * (a "checkFoo" function) that can be used to implement a predicate + * (an "isFoo" function) or a validator (an "assertFoo" function). + * + * * A predicate ideally only returns `true` or `false` and rarely throws. + * * A validator throws an informative diagnostic when the predicate + * would have returned `false`, and simply returns `undefined` normally + * when the predicate would have returned `true`. + * * The internal checking function that they share is parameterized by a + * `Checker` that determines how to proceed with a failure condition. + * Predicates pass in an identity function as checker. Validators + * pass in `assertChecker` which is a trivial wrapper around `assert`. + * + * See the various uses for good examples. + */ +export type Checker = ( + cond: boolean, + details?: import('ses').Details | undefined, +) => boolean; diff --git a/packages/pass-style/src/types.js b/packages/pass-style/src/types.js index 438d053605..879c4efdf9 100644 --- a/packages/pass-style/src/types.js +++ b/packages/pass-style/src/types.js @@ -1,161 +1,2 @@ +/** @file Empty twin for .d.ts */ export {}; - -/** - * @typedef { 'undefined' | 'null' | - * 'boolean' | 'number' | 'bigint' | 'string' | 'symbol' - * } PrimitiveStyle - */ - -/** - * @typedef { PrimitiveStyle | - * 'copyRecord' | 'copyArray' | 'tagged' | - * 'remotable' | - * 'error' | 'promise' - * } PassStyle - */ - -// TODO declare more precise types throughout this file, so the type system -// and IDE can be more helpful. - -/** - * @typedef {any} Passable - * - * A Passable is acyclic data that can be marshalled. It must be hardened to - * remain - * stable (even if some components are proxies; see PureData restriction below), - * and is classified by PassStyle: - * * Atomic primitive values have a PrimitiveStyle (PassStyle - * 'undefined' | 'null' | 'boolean' | 'number' | 'bigint' - * | 'string' | 'symbol'). - * * Containers aggregate other Passables into - * * sequences as CopyArrays (PassStyle 'copyArray'), or - * * string-keyed dictionaries as CopyRecords (PassStyle 'copyRecord'), or - * * higher-level types as CopyTaggeds (PassStyle 'tagged'). - * * PassableCaps (PassStyle 'remotable' | 'promise') expose local values to - * remote interaction. - * * As a special case to support system observability, error objects are - * Passable (PassStyle 'error'). - * - * A Passable is essentially a pass-by-copy superstructure with a - * pass-by-reference - * exit point at the site of each PassableCap (which marshalling represents - * using 'slots'). - */ - -/** - * @callback PassStyleOf - * @param {Passable} passable - * @returns {PassStyle} - */ - -/** - * @typedef {Passable} PureData - * - * A Passable is PureData when its entire data structure is free of PassableCaps - * (remotables and promises) and error objects. - * PureData is an arbitrary composition of primitive values into CopyArray - * and/or - * CopyRecord and/or CopyTagged containers (or a single primitive value with no - * container), and is fully pass-by-copy. - * - * This restriction assures absence of side effects and interleaving risks *given* - * that none of the containers can be a Proxy instance. - * TODO SECURITY BUG we plan to enforce this, giving PureData the same security - * properties as the proposed - * [Records and Tuples](https://github.com/tc39/proposal-record-tuple). - * - * Given this (currently counter-factual) assumption, a PureData value cannot - * be used as a communications channel, - * and can therefore be safely shared with subgraphs that should not be able - * to communicate with each other. - * Without that assumption, such a guarantee requires a marshal-unmarshal round - * trip (as exists between vats) to produce data structures disconnected from - * any potential proxies. - */ - -/** - * @typedef {Passable} RemotableObject - * - * An object marked as remotely accessible using the `Far` or `Remotable` - * functions, or a local presence representing such a remote object. - */ - -/** - * @typedef {Promise | RemotableObject} PassableCap - * - * The authority-bearing leaves of a Passable's pass-by-copy superstructure. - */ - -/** - * @template {Passable} [T=Passable] - * @typedef {T[]} CopyArray - * - * A Passable sequence of Passable values. - */ - -/** - * @template {Passable} [T=Passable] - * @typedef {Record} CopyRecord - * - * A Passable dictionary in which each key is a string and each value is Passable. - */ - -/** - * @template {string} [Tag=string] - * @template {Passable} [Payload=Passable] - * @typedef {{ - * [Symbol.toStringTag]: Tag, - * payload: Payload, - * [passStyle: symbol]: 'tagged' | string, - * }} CopyTagged - * - * A Passable "tagged record" with semantics specific to the tag identified in - * the `[Symbol.toStringTag]` property (such as 'copySet', 'copyBag', - * or 'copyMap'). - * It must have a property with key equal to the `PASS_STYLE` export and - * value 'tagged' - * and no other properties except `[Symbol.toStringTag]` and `payload`. - * - * TODO - * But TypeScript complains about a declaration like `[PASS_STYLE]: 'tagged'` - * because importing packages do not know what `PASS_STYLE` is, - * so we appease it with a looser but less accurate definition - * using symbol index properties and `| string`. - */ - -/** - * @typedef {string} InterfaceSpec - * This is an interface specification. - * For now, it is just a string, but will eventually be `PureData`. Either - * way, it must remain pure, so that it can be safely shared by subgraphs that - * are not supposed to be able to communicate. - */ - -/** - * @callback MarshalGetInterfaceOf - * Simple semantics, just tell what interface (or undefined) a remotable has. - * @param {any} maybeRemotable the value to check - * @returns {InterfaceSpec|undefined} the interface specification, or undefined - * if not a deemed to be a Remotable - */ - -/** - * @callback Checker - * Internal to a useful pattern for writing checking logic - * (a "checkFoo" function) that can be used to implement a predicate - * (an "isFoo" function) or a validator (an "assertFoo" function). - * - * * A predicate ideally only returns `true` or `false` and rarely throws. - * * A validator throws an informative diagnostic when the predicate - * would have returned `false`, and simply returns `undefined` normally - * when the predicate would have returned `true`. - * * The internal checking function that they share is parameterized by a - * `Checker` that determines how to proceed with a failure condition. - * Predicates pass in an identity function as checker. Validators - * pass in `assertChecker` which is a trivial wrapper around `assert`. - * - * See the various uses for good examples. - * @param {boolean} cond - * @param {import('ses').Details} [details] - * @returns {boolean} - */ diff --git a/packages/pass-style/src/types.test-d.ts b/packages/pass-style/src/types.test-d.ts new file mode 100644 index 0000000000..957f15ff59 --- /dev/null +++ b/packages/pass-style/src/types.test-d.ts @@ -0,0 +1,31 @@ +/* eslint-disable */ +import { expectType, expectNotType } from 'tsd'; +import { Far } from './make-far'; +import { passStyleOf } from './passStyleOf'; +import { makeTagged } from './makeTagged'; +import { CopyTagged, PassStyle } from './types'; +import { PASS_STYLE } from './passStyle-helpers'; + +const remotable = Far('foo', {}); + +const copyTagged = makeTagged('someTag', remotable); +expectType>(copyTagged); + +const someUnknown: unknown = null; + +expectType<'undefined'>(passStyleOf(undefined)); +expectType<'string'>(passStyleOf('str')); +expectType<'boolean'>(passStyleOf(true)); +expectType<'number'>(passStyleOf(1)); +expectType<'bigint'>(passStyleOf(1n)); +expectType<'symbol'>(passStyleOf(Symbol.for('foo'))); +expectType<'null'>(passStyleOf(null)); +expectType<'promise'>(passStyleOf(Promise.resolve())); +expectType<'error'>(passStyleOf(new Error())); +expectType<'tagged'>(passStyleOf(copyTagged)); +expectType<'copyArray'>(passStyleOf([])); +expectType<'copyRecord'>(passStyleOf({})); +// though the object is specifying a PASS_STYLE, it doesn't match the case for extracting it +expectType<'copyRecord'>(passStyleOf({ [PASS_STYLE]: 'arbitrary' } as const)); +expectType<'remotable'>(passStyleOf(remotable)); +expectType(passStyleOf(someUnknown)); diff --git a/packages/pass-style/test/test-passStyleOf.js b/packages/pass-style/test/test-passStyleOf.js index 1588d46297..d09cd55260 100644 --- a/packages/pass-style/test/test-passStyleOf.js +++ b/packages/pass-style/test/test-passStyleOf.js @@ -4,10 +4,15 @@ import test from '@endo/ses-ava/prepare-endo.js'; import { q } from '@endo/errors'; import { passStyleOf } from '../src/passStyleOf.js'; -import { Far } from '../src/make-far.js'; +import { Far, ToFarFunction } from '../src/make-far.js'; import { makeTagged } from '../src/makeTagged.js'; import { PASS_STYLE } from '../src/passStyle-helpers.js'; +const harden = /** @type {import('ses').Harden & { isFake?: boolean }} */ ( + // eslint-disable-next-line no-undef + global.harden +); + const { getPrototypeOf, defineProperty } = Object; const { ownKeys } = Reflect; @@ -31,6 +36,11 @@ test('passStyleOf basic success cases', t => { t.is(passStyleOf(harden(Error('ok'))), 'error'); }); +test('ToFarFunction', t => { + const ff = ToFarFunction('far', () => {}); + t.is(passStyleOf(ff), 'remotable'); +}); + test('some passStyleOf rejections', t => { const hairlessError = Error('hairless'); for (const k of ownKeys(hairlessError)) { @@ -106,6 +116,7 @@ test('some passStyleOf rejections', t => { * * @param {string} [tag] * @param {object|null} [proto] + * @returns {{ [PASS_STYLE]: 'remotable', [Symbol.toStringTag]: string }} */ const makeTagishRecord = (tag = 'Remotable', proto = undefined) => { return Object.create(proto === undefined ? Object.prototype : proto, { @@ -181,12 +192,14 @@ test('passStyleOf testing remotables', t => { t.is(passStyleOf(Far('foo', () => 'far function')), 'remotable'); const tagRecord1 = harden(makeTagishRecord('Alleged: manually constructed')); + /** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */ const farObj1 = harden({ __proto__: tagRecord1, }); t.is(passStyleOf(farObj1), 'remotable'); const tagRecord2 = makeTagishRecord('Alleged: tagRecord not hardened'); + /** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */ const farObj2 = Object.freeze({ __proto__: tagRecord2, }); @@ -202,12 +215,14 @@ test('passStyleOf testing remotables', t => { const tagRecord3 = Object.freeze( makeTagishRecord('Alleged: both manually frozen'), ); + /** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */ const farObj3 = Object.freeze({ __proto__: tagRecord3, }); t.is(passStyleOf(farObj3), 'remotable'); const tagRecord4 = harden(makeTagishRecord('Remotable')); + /** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */ const farObj4 = harden({ __proto__: tagRecord4, }); @@ -226,6 +241,7 @@ test('passStyleOf testing remotables', t => { const farObjProto6 = harden({ __proto__: tagRecord6, }); + /** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */ const farObj6 = harden({ __proto__: farObjProto6, }); @@ -253,6 +269,7 @@ test('passStyleOf testing remotables', t => { const farTagRecord7 = getPrototypeOf(farBaseProto7); t.is(farTagRecord7[PASS_STYLE], 'remotable'); t.is(getPrototypeOf(farTagRecord7), Object.prototype); + /** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */ const farObj7 = new FarBaseClass7(3); t.is(passStyleOf(farObj7), 'remotable'); t.is(farObj7.add(7), 10); @@ -264,6 +281,7 @@ test('passStyleOf testing remotables', t => { } } harden(FarSubclass8); + /** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */ const farObj8 = new FarSubclass8(3); t.is(passStyleOf(farObj8), 'remotable'); t.is(farObj8.twice(), 14); @@ -409,6 +427,7 @@ test('remotables - safety from the gibson042 attack', t => { test('Unexpected stack on errors', t => { let err; try { + // @ts-expect-error purposeful type violation for testing null.error; } catch (e) { err = e; @@ -429,11 +448,13 @@ test('Allow toStringTag overrides', t => { t.is(`${alice}`, '[object DebugName: Allison]'); t.is(`${q(alice)}`, '"[DebugName: Allison]"'); + /** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */ const carol = harden({ __proto__: alice }); t.is(passStyleOf(carol), 'remotable'); t.is(`${carol}`, '[object DebugName: Allison]'); t.is(`${q(carol)}`, '"[DebugName: Allison]"'); + /** @type {any} UNTIL https://github.com/microsoft/TypeScript/issues/38385 */ const bob = harden({ __proto__: carol, [Symbol.toStringTag]: 'DebugName: Robert', @@ -446,7 +467,9 @@ test('Allow toStringTag overrides', t => { t.is(fred.name, 'fred'); defineProperty(fred, Symbol.toStringTag, { value: 'DebugName: Friedrich' }); const f = Far('Fred', fred); + // @ts-expect-error TS doesn't know `fred` has changed t.is(f, fred); + // @ts-expect-error TS doesn't know `fred` has changed t.is(passStyleOf(fred), 'remotable'); t.is(`${fred}`, '() => {}'); t.is(Object.prototype.toString.call(fred), '[object DebugName: Friedrich]'); diff --git a/packages/pass-style/tools/arb-passable.js b/packages/pass-style/tools/arb-passable.js index 1d6651cdec..22549f5d83 100644 --- a/packages/pass-style/tools/arb-passable.js +++ b/packages/pass-style/tools/arb-passable.js @@ -99,7 +99,12 @@ const { arbDag } = fc.letrec(tie => { ), }), ) - .map(({ type, payload }) => makeTagged(type, payload)), + .map(({ type, payload }) => + makeTagged( + type, + /** @type {import('../src/types.js').Passable} */ (payload), + ), + ), ), }; }); diff --git a/packages/patterns/NEWS.md b/packages/patterns/NEWS.md index 1de20be073..34e8ef8c6e 100644 --- a/packages/patterns/NEWS.md +++ b/packages/patterns/NEWS.md @@ -1,5 +1,10 @@ User-visible changes in `@endo/patterns`: +# Next release + +- `Passable` is now an accurate type instead of `any`. Downstream type checking may require changes ([example](https://github.com/Agoric/agoric-sdk/pull/8774)) +- Some downstream types that take or return `Passable` were changed to `any` to defer downstream work to accomodate. + # v1.2.0 (2024-02-22) - Add `M.tagged(tagPattern, payloadPattern)` for making patterns that match diff --git a/packages/patterns/src/keys/checkKey.js b/packages/patterns/src/keys/checkKey.js index 54b8e717b1..5a2bad79c4 100644 --- a/packages/patterns/src/keys/checkKey.js +++ b/packages/patterns/src/keys/checkKey.js @@ -47,7 +47,7 @@ const checkPrimitiveKey = (val, check) => { }; /** - * @param {Passable} val + * @param {any} val * @returns {boolean} */ export const isPrimitiveKey = val => checkPrimitiveKey(val, identChecker); @@ -79,7 +79,7 @@ export const checkScalarKey = (val, check) => { }; /** - * @param {Passable} val + * @param {any} val * @returns {boolean} */ export const isScalarKey = val => checkScalarKey(val, identChecker); @@ -96,11 +96,13 @@ harden(assertScalarKey); // ////////////////////////////// Keys ///////////////////////////////////////// +// @ts-expect-error Key does not satisfy WeakKey /** @type {WeakSet} */ +// @ts-expect-error Key does not satisfy WeakKey const keyMemo = new WeakSet(); /** - * @param {Passable} val + * @param {unknown} val * @param {Checker} check * @returns {boolean} */ @@ -111,6 +113,7 @@ export const checkKey = (val, check) => { assertPassable(val); return true; } + // @ts-expect-error narrowed if (keyMemo.has(val)) { return true; } @@ -119,6 +122,7 @@ export const checkKey = (val, check) => { if (result) { // Don't cache the undefined cases, so that if it is tried again // with `assertChecker` it'll throw a diagnostic again + // @ts-expect-error narrowed keyMemo.add(val); } // Note that we do not memoize a negative judgement, so that if it is tried @@ -128,14 +132,17 @@ export const checkKey = (val, check) => { harden(checkKey); /** - * @param {Passable} val - * @returns {boolean} + * @type {{ + * (val: Passable): val is Key; + * (val: any): boolean; + * }} */ export const isKey = val => checkKey(val, identChecker); harden(isKey); /** * @param {Key} val + * @returns {asserts val is Key} */ export const assertKey = val => { checkKey(val, assertChecker); @@ -151,7 +158,7 @@ harden(assertKey); const copySetMemo = new WeakSet(); /** - * @param {Passable} s + * @param {any} s * @param {Checker} check * @returns {boolean} */ @@ -172,12 +179,9 @@ export const checkCopySet = (s, check) => { harden(checkCopySet); /** - * @callback IsCopySet - * @param {Passable} s + * @param {any} s * @returns {s is CopySet} */ - -/** @type {IsCopySet} */ export const isCopySet = s => checkCopySet(s, identChecker); harden(isCopySet); @@ -194,7 +198,7 @@ export const assertCopySet = s => { harden(assertCopySet); /** - * @template K + * @template {Key} K * @param {CopySet} s * @returns {K[]} */ @@ -205,7 +209,7 @@ export const getCopySetKeys = s => { harden(getCopySetKeys); /** - * @template K + * @template {Key} K * @param {CopySet} s * @param {(key: K, index: number) => boolean} fn * @returns {boolean} @@ -215,7 +219,7 @@ export const everyCopySetKey = (s, fn) => harden(everyCopySetKey); /** - * @template K + * @template {Key} K * @param {Iterable} elementIter * @returns {CopySet} */ @@ -235,7 +239,7 @@ harden(makeCopySet); const copyBagMemo = new WeakSet(); /** - * @param {Passable} b + * @param {any} b * @param {Checker} check * @returns {boolean} */ @@ -256,12 +260,9 @@ export const checkCopyBag = (b, check) => { harden(checkCopyBag); /** - * @callback IsCopyBag - * @param {Passable} b + * @param {any} b * @returns {b is CopyBag} */ - -/** @type {IsCopyBag} */ export const isCopyBag = b => checkCopyBag(b, identChecker); harden(isCopyBag); @@ -278,7 +279,7 @@ export const assertCopyBag = b => { harden(assertCopyBag); /** - * @template K + * @template {Key} K * @param {CopyBag} b * @returns {CopyBag['payload']} */ @@ -289,7 +290,7 @@ export const getCopyBagEntries = b => { harden(getCopyBagEntries); /** - * @template K + * @template {Key} K * @param {CopyBag} b * @param {(entry: [K, bigint], index: number) => boolean} fn * @returns {boolean} @@ -299,7 +300,7 @@ export const everyCopyBagEntry = (b, fn) => harden(everyCopyBagEntry); /** - * @template K + * @template {Key} K * @param {Iterable<[K,bigint]>} bagEntryIter * @returns {CopyBag} */ @@ -311,7 +312,7 @@ export const makeCopyBag = bagEntryIter => { harden(makeCopyBag); /** - * @template K + * @template {Key} K * @param {Iterable} elementIter * @returns {CopyBag} */ @@ -344,7 +345,7 @@ harden(makeCopyBagFromElements); const copyMapMemo = new WeakSet(); /** - * @param {Passable} m + * @param {any} m * @param {Checker} check * @returns {boolean} */ @@ -383,22 +384,16 @@ export const checkCopyMap = (m, check) => { harden(checkCopyMap); /** - * @callback IsCopyMap - * @param {Passable} m + * @param {any} m * @returns {m is CopyMap} */ - -/** @type {IsCopyMap} */ export const isCopyMap = m => checkCopyMap(m, identChecker); harden(isCopyMap); /** - * @callback AssertCopyMap * @param {Passable} m * @returns {asserts m is CopyMap} */ - -/** @type {AssertCopyMap} */ export const assertCopyMap = m => { checkCopyMap(m, assertChecker); }; @@ -539,7 +534,7 @@ harden(makeCopyMap); // //////////////////////// Keys Recur ///////////////////////////////////////// /** - * @param {Passable} val + * @param {any} val * @param {Checker} check * @returns {boolean} */ diff --git a/packages/patterns/src/keys/compareKeys.js b/packages/patterns/src/keys/compareKeys.js index 63820aa008..b68bc3b84c 100644 --- a/packages/patterns/src/keys/compareKeys.js +++ b/packages/patterns/src/keys/compareKeys.js @@ -17,7 +17,7 @@ import { } from './checkKey.js'; import { makeCompareCollection } from './keycollection-operators.js'; -/** @import {CopySet, KeyCompare} from '../types.js' */ +/** @import {CopySet, Key, KeyCompare} from '../types.js' */ /** * CopySet X is smaller than CopySet Y iff all of these conditions hold: @@ -27,7 +27,7 @@ import { makeCompareCollection } from './keycollection-operators.js'; * X is equivalent to Y iff the condition 1 holds but condition 2 does not. */ export const setCompare = makeCompareCollection( - /** @type {(s: CopySet) => Array<[K, 1]>} */ ( + /** @type {(s: CopySet) => Array<[K, 1]>} */ ( s => harden(getCopySetKeys(s).map(key => [key, 1])) ), 0, @@ -133,8 +133,10 @@ export const compareKeys = (left, right) => { // rank order. // Because the invariants above apply to the elements of the array, // they apply to the array as a whole. + // @ts-expect-error narrowed const len = Math.min(left.length, right.length); for (let i = 0; i < len; i += 1) { + // @ts-expect-error narrowed const result = compareKeys(left[i], right[i]); if (result !== 0) { return result; @@ -142,11 +144,14 @@ export const compareKeys = (left, right) => { } // If all matching elements are keyEQ, then according to their lengths. // Thus, if array X is a prefix of array Y, then X is smaller than Y. + // @ts-expect-error narrowed return compareRank(left.length, right.length); } case 'copyRecord': { // Pareto partial order comparison. + // @ts-expect-error narrowed const leftNames = recordNames(left); + // @ts-expect-error narrowed const rightNames = recordNames(right); // eslint-disable-next-line no-use-before-define @@ -158,7 +163,9 @@ export const compareKeys = (left, right) => { // to avoid more irrelevant ones. return NaN; } + // @ts-expect-error narrowed const leftValues = recordValues(left, leftNames); + // @ts-expect-error narrowed const rightValues = recordValues(right, rightNames); // Presume that both copyRecords have the same key order // until encountering a property disproving that hypothesis. @@ -189,7 +196,9 @@ export const compareKeys = (left, right) => { return result; } case 'tagged': { + // @ts-expect-error narrowed const leftTag = getTag(left); + // @ts-expect-error narrowed const rightTag = getTag(right); if (leftTag !== rightTag) { // different tags are incommensurate @@ -197,9 +206,11 @@ export const compareKeys = (left, right) => { } switch (leftTag) { case 'copySet': { + // @ts-expect-error narrowed return setCompare(left, right); } case 'copyBag': { + // @ts-expect-error narrowed return bagCompare(left, right); } case 'copyMap': { diff --git a/packages/patterns/src/keys/copyBag.js b/packages/patterns/src/keys/copyBag.js index 52d6dbcc7e..09d9605ff9 100644 --- a/packages/patterns/src/keys/copyBag.js +++ b/packages/patterns/src/keys/copyBag.js @@ -15,11 +15,11 @@ import { X } from '@endo/errors'; /** * @import {Passable} from '@endo/pass-style' * @import {Checker} from '@endo/marshal' - * @import {CopyBag, FullCompare} from '../types.js' + * @import {CopyBag, Key, FullCompare} from '../types.js' */ /** - * @template T + * @template {Key} T * @param {[T,bigint][]} bagEntries * @param {FullCompare | undefined} fullCompare If provided and `bagEntries` is already * known to be sorted by this `fullCompare`, then we should get a memo hit @@ -55,7 +55,7 @@ const checkNoDuplicateKeys = (bagEntries, fullCompare, check) => { }; /** - * @template T + * @template {Key} T * @param {[T,bigint][]} bagEntries * @param {FullCompare} [fullCompare] * @returns {void} @@ -100,6 +100,7 @@ export const checkBagEntries = (bagEntries, check) => { ); } } + // @ts-expect-error XXX Key types return checkNoDuplicateKeys(bagEntries, undefined, check); }; harden(checkBagEntries); @@ -114,6 +115,10 @@ export const assertBagEntries = bagEntries => { }; harden(assertBagEntries); +/** + * @template {Key} K + * @param {Iterable<[K, bigint]>} bagEntriesList + */ export const coerceToBagEntries = bagEntriesList => { const bagEntries = sortByRank(bagEntriesList, compareAntiRank); assertBagEntries(bagEntries); @@ -122,7 +127,7 @@ export const coerceToBagEntries = bagEntriesList => { harden(coerceToBagEntries); /** - * @template K + * @template {Key} K * @param {Iterable<[K, bigint]>} bagEntryIter * @returns {CopyBag} */ diff --git a/packages/patterns/src/keys/copySet.js b/packages/patterns/src/keys/copySet.js index 9741b7b2d3..3901c125bc 100644 --- a/packages/patterns/src/keys/copySet.js +++ b/packages/patterns/src/keys/copySet.js @@ -15,11 +15,11 @@ import { X } from '@endo/errors'; /** * @import {Passable} from '@endo/pass-style' * @import {Checker} from '@endo/marshal' - * @import {CopySet, FullCompare} from '../types.js' + * @import {CopySet, FullCompare, Key} from '../types.js' */ /** - * @template T + * @template {Passable} T * @param {T[]} elements * @param {FullCompare | undefined} fullCompare If provided and `elements` is already known * to be sorted by this `fullCompare`, then we should get a memo hit rather @@ -52,7 +52,7 @@ const checkNoDuplicates = (elements, fullCompare, check) => { }; /** - * @template T + * @template {Passable} T * @param {T[]} elements * @param {FullCompare} [fullCompare] * @returns {void} @@ -88,6 +88,10 @@ export const assertElements = elements => { }; harden(assertElements); +/** + * @template {Key} K + * @param {Iterable} elementsList + */ export const coerceToElements = elementsList => { const elements = sortByRank(elementsList, compareAntiRank); assertElements(elements); @@ -96,7 +100,7 @@ export const coerceToElements = elementsList => { harden(coerceToElements); /** - * @template K + * @template {Key} K * @param {Iterable} elementIter * @returns {CopySet} */ diff --git a/packages/patterns/src/keys/keycollection-operators.js b/packages/patterns/src/keys/keycollection-operators.js index 7d51bdb42c..06ed17f34d 100644 --- a/packages/patterns/src/keys/keycollection-operators.js +++ b/packages/patterns/src/keys/keycollection-operators.js @@ -27,6 +27,7 @@ import { q, Fail } from '@endo/errors'; * @returns {IterableIterator<[Key, V]>} */ const generateFullSortedEntries = (entries, rankCompare, fullCompare) => { + // @ts-expect-error XXX Key types assertRankSorted(entries, rankCompare); const { length } = entries; let i = 0; @@ -56,8 +57,10 @@ const generateFullSortedEntries = (entries, rankCompare, fullCompare) => { // Sort the ties by `fullCompare`, enforce key uniqueness, and delegate to // a sub-iterator. + // @ts-expect-error XXX Key types const sortedTies = sortByRank(ties, fullCompare); for (let k = 1; k < sortedTies.length; k += 1) { + // @ts-expect-error XXX Key types const [key0] = sortedTies[k - 1]; const [key1] = sortedTies[k]; Math.sign(fullCompare(key0, key1)) || diff --git a/packages/patterns/src/keys/merge-bag-operators.js b/packages/patterns/src/keys/merge-bag-operators.js index 79a95f2630..9ae1a8dd44 100644 --- a/packages/patterns/src/keys/merge-bag-operators.js +++ b/packages/patterns/src/keys/merge-bag-operators.js @@ -8,8 +8,9 @@ import { q, Fail } from '@endo/errors'; import { assertNoDuplicateKeys, makeBagOfEntries } from './copyBag.js'; /** + * @import {Passable} from '@endo/pass-style'; * @import {RankCompare} from '@endo/marshal' - * @import {FullCompare} from '../types.js' + * @import {FullCompare, Key} from '../types.js' */ // Based on merge-set-operators.js, but altered for the bag representation. @@ -29,7 +30,7 @@ import { assertNoDuplicateKeys, makeBagOfEntries } from './copyBag.js'; * to `fullOrder`. However, it optimizes for the case where these contiguous * runs that need to be resorted are either absent or small. * - * @template T + * @template {Key} T * @param {[T,bigint][]} bagEntries * @param {RankCompare} rankCompare * @param {FullCompare} fullCompare @@ -96,7 +97,7 @@ const bagWindowResort = (bagEntries, rankCompare, fullCompare) => { * For sets, these counts are always 0 or 1, but this representation * generalizes nicely for bags. * - * @template T + * @template {Key} T * @param {[T,bigint][]} xbagEntries * @param {[T,bigint][]} ybagEntries * @returns {Iterable<[T,bigint,bigint]>} diff --git a/packages/patterns/src/keys/merge-set-operators.js b/packages/patterns/src/keys/merge-set-operators.js index c345b0e646..a9bdf2b65d 100644 --- a/packages/patterns/src/keys/merge-set-operators.js +++ b/packages/patterns/src/keys/merge-set-operators.js @@ -8,6 +8,7 @@ import { q, Fail } from '@endo/errors'; import { assertNoDuplicates, makeSetOfElements } from './copySet.js'; /** + * @import {Passable} from '@endo/pass-style'; * @import {RankCompare} from '@endo/marshal' * @import {FullCompare, KeyComparison} from '../types.js' */ @@ -25,7 +26,7 @@ import { assertNoDuplicates, makeSetOfElements } from './copySet.js'; * to `fullOrder`. However, it optimizes for the case where these contiguous * runs that need to be resorted are either absent or small. * - * @template T + * @template {Passable} T * @param {T[]} elements * @param {RankCompare} rankCompare * @param {FullCompare} fullCompare @@ -89,7 +90,7 @@ const windowResort = (elements, rankCompare, fullCompare) => { * For sets, these counts are always 0 or 1, but this representation * generalizes nicely for bags. * - * @template T + * @template {Passable} T * @param {T[]} xelements * @param {T[]} yelements * @returns {Iterable<[T,bigint,bigint]>} diff --git a/packages/patterns/src/patterns/patternMatchers.js b/packages/patterns/src/patterns/patternMatchers.js index b46c3bffb7..735ceccb2c 100644 --- a/packages/patterns/src/patterns/patternMatchers.js +++ b/packages/patterns/src/patterns/patternMatchers.js @@ -1,3 +1,5 @@ +// @ts-nocheck So many errors that the suppressions hamper readability. +// TODO parameterize MatchHelper which will solve most of them import { assertChecker, Far, @@ -232,7 +234,7 @@ const makePatternKit = () => { * invariants associated with that recognition. * Otherwise, `check(false, ...)` and returns undefined * - * @param {Passable} specimen + * @param {any} specimen * @param {Checker} [check] * @returns {Kind | undefined} */ @@ -263,7 +265,7 @@ const makePatternKit = () => { * Checks only recognized kinds, and only if the specimen * passes the invariants associated with that recognition. * - * @param {Passable} specimen + * @param {any} specimen * @param {Kind} kind * @param {Checker} check * @returns {boolean} @@ -291,14 +293,14 @@ const makePatternKit = () => { * Checks only recognized kinds, and only if the specimen * passes the invariants associated with that recognition. * - * @param {Passable} specimen + * @param {any} specimen * @param {Kind} kind * @returns {boolean} */ const isKind = (specimen, kind) => checkKind(specimen, kind, identChecker); /** - * @param {Passable} specimen + * @param {any} specimen * @param {Key} keyAsPattern * @param {Checker} check * @returns {boolean} @@ -402,7 +404,7 @@ const makePatternKit = () => { // /////////////////////// matches /////////////////////////////////////////// /** - * @param {Passable} specimen + * @param {any} specimen * @param {Pattern} pattern * @param {Checker} check * @param {string|number} [label] @@ -413,7 +415,7 @@ const makePatternKit = () => { applyLabelingError(checkMatchesInternal, [specimen, pattern, check], label); /** - * @param {Passable} specimen + * @param {any} specimen * @param {Pattern} patt * @param {Checker} check * @returns {boolean} @@ -561,7 +563,7 @@ const makePatternKit = () => { }; /** - * @param {Passable} specimen + * @param {any} specimen * @param {Pattern} patt * @returns {boolean} */ @@ -572,7 +574,7 @@ const makePatternKit = () => { * Returning normally indicates success. Match failure is indicated by * throwing. * - * @param {Passable} specimen + * @param {any} specimen * @param {Pattern} patt * @param {string|number} [label] */ diff --git a/packages/patterns/src/patterns/types.js b/packages/patterns/src/patterns/types.js index 7c15fe3f99..af50133967 100644 --- a/packages/patterns/src/patterns/types.js +++ b/packages/patterns/src/patterns/types.js @@ -37,15 +37,15 @@ export {}; /** * @typedef {object} PatternKit - * @property {(specimen: Passable, + * @property {(specimen: any, * patt: Passable, * check: Checker, * label?: string|number * ) => boolean} checkMatches - * @property {(specimen: Passable, patt: Pattern) => boolean} matches - * @property {(specimen: Passable, patt: Pattern, label?: string|number) => void} mustMatch + * @property {(specimen: any, patt: Pattern) => boolean} matches + * @property {(specimen: any, patt: Pattern, label?: string|number) => void} mustMatch * @property {(patt: Pattern) => void} assertPattern - * @property {(patt: Passable) => boolean} isPattern + * @property {(patt: any) => boolean} isPattern * @property {GetRankCover} getRankCover * @property {MatcherNamespace} M * @property {(specimen: Passable, check?: Checker) => Kind | undefined} kindOf diff --git a/packages/patterns/src/types.js b/packages/patterns/src/types.js index 7b86c88c54..52f930731a 100644 --- a/packages/patterns/src/types.js +++ b/packages/patterns/src/types.js @@ -2,13 +2,14 @@ export {}; +// NB: as of TS 5.5 nightly, TS thinks RankCover and Checker "is declared but never read" but they are /** - * @import {CopyArray, CopyRecord, CopyTagged, Passable, PassStyle, Checker} from '@endo/pass-style' - * @import {RankCompare, RankCover} from '@endo/marshal' + * @import {Checker, CopyArray, CopyRecord, CopyTagged, Passable, PassStyle, RemotableObject} from '@endo/pass-style'; + * @import {RankCompare, RankCover} from '@endo/marshal'; */ /** - * @typedef {Passable} Key + * @typedef {Exclude, Error | Promise>} Key * * Keys are Passable arbitrarily-nested pass-by-copy containers * (CopyArray, CopyRecord, CopySet, CopyBag, CopyMap) in which every @@ -47,7 +48,7 @@ export {}; */ /** - * @typedef {Passable} Pattern + * @typedef {Exclude} Pattern * * Patterns are Passable arbitrarily-nested pass-by-copy containers * (CopyArray, CopyRecord, CopySet, CopyBag, CopyMap) in which every @@ -425,14 +426,14 @@ export {}; * The CopyRecord must have all properties that appear on `required`, * but may omit properties that appear on `optional`. * - * @property {(basePatt: CopyRecord<*> | CopyArray<*>, + * @property {(basePatt: CopyRecord | CopyArray, * rest?: Pattern, * ) => Matcher} split * Deprecated. Use `M.splitArray` or `M.splitRecord` instead. * An array or record is split into the first part that is matched by * `basePatt`, and the remainder, which is matched against `rest` if present. * - * @property {(basePatt: CopyRecord<*> | CopyArray<*>, + * @property {(basePatt: CopyRecord | CopyArray, * rest?: Pattern, * ) => Matcher} partial * Deprecated. Use `M.splitArray` or `M.splitRecord` instead. diff --git a/packages/patterns/test/test-patterns.js b/packages/patterns/test/test-patterns.js index 55a5b028c8..f5279579df 100644 --- a/packages/patterns/test/test-patterns.js +++ b/packages/patterns/test/test-patterns.js @@ -532,6 +532,7 @@ const runTests = (t, successCase, failCase) => { t.throws( () => { copyMapComparison || Fail`No CopyMap comparison support`; + // @ts-expect-error XXX Key types successCase(specimen, M.gt(makeCopyMap([]))); }, { message: 'No CopyMap comparison support' }, diff --git a/packages/patterns/test/types.test-d.ts b/packages/patterns/test/types.test-d.ts new file mode 100644 index 0000000000..5641af3b6d --- /dev/null +++ b/packages/patterns/test/types.test-d.ts @@ -0,0 +1,36 @@ +import type { Passable } from '@endo/pass-style'; +import { expectNotType, expectType } from 'tsd'; +import { isKey } from '../src/keys/checkKey.js'; +import { M } from '../src/patterns/patternMatchers.js'; +import type { Key } from '../src/types.js'; + +// @ts-expect-error M.any missing parens +M.arrayOf(M.any); +M.arrayOf(M.any()); + +{ + const maybeKey: Passable = 'key'; + const result = isKey(maybeKey); + expectType(result); + if (result) { + expectType(maybeKey); + } else { + expectNotType(maybeKey); + } +} +{ + const str = 'some string'; + if (isKey(str)) { + // doesn't widen + expectType(str); + } +} + +{ + const someAny: any = null; + someAny.foo; + if (isKey(someAny)) { + // still any + someAny.foo; + } +} diff --git a/packages/ses/test/test-anticipate-async-iterator-helpers-shimmed.js b/packages/ses/test/test-anticipate-async-iterator-helpers-shimmed.js index b3cbcdeeda..d98c0b5d0f 100644 --- a/packages/ses/test/test-anticipate-async-iterator-helpers-shimmed.js +++ b/packages/ses/test/test-anticipate-async-iterator-helpers-shimmed.js @@ -1,3 +1,4 @@ +/* global AsyncIterator */ import './enforce-cjs-strict.js'; import './core-js-configuration.js'; import 'core-js/actual/async-iterator/index.js'; diff --git a/packages/ses/test/test-anticipate-iterator-helpers-shimmed.js b/packages/ses/test/test-anticipate-iterator-helpers-shimmed.js index 9fa11e4d37..50424d2732 100644 --- a/packages/ses/test/test-anticipate-iterator-helpers-shimmed.js +++ b/packages/ses/test/test-anticipate-iterator-helpers-shimmed.js @@ -1,3 +1,4 @@ +/* global Iterator */ import './enforce-cjs-strict.js'; import './core-js-configuration.js'; import 'core-js/actual/iterator/index.js';