Skip to content

Commit

Permalink
fix(exo): do NOT mutate behaviorMethods argument
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Jun 16, 2023
1 parent 16003bc commit 2468362
Show file tree
Hide file tree
Showing 2 changed files with 23 additions and 31 deletions.
2 changes: 2 additions & 0 deletions packages/exo/src/exo-makers.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export const initEmpty = () => emptyRecord;
* @returns {(...args: Parameters<I>) => (M & import('@endo/eventual-send').RemotableBrand<{}, M>)}
*/
export const defineExoClass = (tag, interfaceGuard, init, methods, options) => {
harden(methods);
const { finish = undefined } = options || {};
/** @type {WeakMap<M,ClassContext<ReturnType<I>, M>>} */
const contextMap = new WeakMap();
Expand Down Expand Up @@ -133,6 +134,7 @@ export const defineExoClassKit = (
methodsKit,
options,
) => {
harden(methodsKit);
const { finish = undefined } = options || {};
const contextMapKit = objectMap(methodsKit, () => new WeakMap());
const getContextKit = objectMap(
Expand Down
52 changes: 21 additions & 31 deletions packages/exo/src/exo-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,36 +217,19 @@ const bindMethod = (
export const GET_INTERFACE_GUARD = Symbol.for('getInterfaceGuard');

/**
*
* @template {Record<string | symbol, CallableFunction>} T
* @param {T} behaviorMethods
* @param {(string | symbol)[]} methodNames
* @param {import('@endo/patterns').InterfaceGuard} interfaceGuard
* @returns {T}
*/
const addGetInterfaceGuardMethod = (
behaviorMethods,
methodNames,
interfaceGuard,
) => {
if (!hasOwnPropertyOf(behaviorMethods, GET_INTERFACE_GUARD)) {
methodNames.push(GET_INTERFACE_GUARD);
// Note that we do not also update the interfaceGuard to describe this
// meta method. Currently, it is a symbol-named method, so we cannot
// anyway.
const getInterfaceGuardMethod = {
[GET_INTERFACE_GUARD]() {
return interfaceGuard;
},
}[GET_INTERFACE_GUARD];
defineProperties(behaviorMethods, {
[GET_INTERFACE_GUARD]: {
value: getInterfaceGuardMethod,
writable: false,
enumerable: false,
configurable: false,
},
});
}
};
const withGetInterfaceGuardMethod = (behaviorMethods, interfaceGuard) =>
harden({
[GET_INTERFACE_GUARD]() {
return interfaceGuard;
},
...behaviorMethods,
});

/**
* @template {Record<string | symbol, CallableFunction>} T
Expand All @@ -265,11 +248,13 @@ export const defendPrototype = (
interfaceGuard = undefined,
) => {
const prototype = {};
const methodNames = ownKeys(behaviorMethods).filter(
if (hasOwnPropertyOf(behaviorMethods, 'constructor')) {
// By ignoring any method named "constructor", we can use a
// class.prototype as a behaviorMethods.
name => name !== 'constructor',
);
const { constructor: _, ...methods } = behaviorMethods;
// @ts-expect-error TS misses that hasOwn check makes this safe
behaviorMethods = harden(methods);
}
let methodGuards;
if (interfaceGuard) {
const {
Expand All @@ -282,6 +267,7 @@ export const defendPrototype = (
assert.equal(klass, 'Interface');
assert.typeof(interfaceName, 'string');
{
const methodNames = ownKeys(behaviorMethods);
const methodGuardNames = ownKeys(methodGuards);
const unimplemented = listDifference(methodGuardNames, methodNames);
unimplemented.length === 0 ||
Expand All @@ -292,9 +278,13 @@ export const defendPrototype = (
Fail`methods ${q(unguarded)} not guarded by ${q(interfaceName)}`;
}
}
addGetInterfaceGuardMethod(behaviorMethods, methodNames, interfaceGuard);
behaviorMethods = withGetInterfaceGuardMethod(
behaviorMethods,
interfaceGuard,
);
}
for (const prop of methodNames) {

for (const prop of ownKeys(behaviorMethods)) {
prototype[prop] = bindMethod(
`In ${q(prop)} method of (${tag})`,
contextProvider,
Expand Down

0 comments on commit 2468362

Please sign in to comment.