Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/exo/src/exo-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ export const defendPrototype = (
methodGuards: mg,
symbolMethodGuards,
sloppy,
defaultGuards: dg = sloppy ? 'passable' : defaultGuards,
defaultGuards: dg = sloppy ? 'passable' : undefined,
} = getInterfaceGuardPayload(interfaceGuard);
methodGuards = harden({
...mg,
Expand Down
51 changes: 46 additions & 5 deletions packages/exo/test/exo-wobbly-point.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import test from '@endo/ses-ava/prepare-endo.js';

import { getMethodNames } from '@endo/eventual-send/utils.js';
import { passStyleOf, Far, GET_METHOD_NAMES } from '@endo/pass-style';
import { M } from '@endo/patterns';
import { M, getNamedMethodGuards } from '@endo/patterns';

import { Fail, q } from '@endo/errors';
import { GET_INTERFACE_GUARD } from '../src/get-interface.js';
Expand Down Expand Up @@ -41,6 +41,9 @@ const defineExoClassFromJSClass = klass =>
harden(defineExoClassFromJSClass);

const ExoPointI = M.interface('ExoPoint', {
// Just following the interface inheritance pattern even though it is
// literally vacuous in this case.
...getNamedMethodGuards(ExoBaseClass.implements),
toString: M.call().returns(M.string()),
getX: M.call().returns(M.gte(0)),
getY: M.call().returns(M.number()),
Expand Down Expand Up @@ -136,18 +139,53 @@ test('ExoPoint instances', t => {
});
});

class ExoWobblyPointBad extends ExoPoint {
static init(x, y, getWobble) {
return { ...super.init(x, y), getWobble };
}

getX() {
// @ts-expect-error Property 'state' does not exist on type 'ExoPoint'.
const { getWobble } = this.state;
return super.getX() + getWobble();
}

// Made `wobble` public just to test interface inheritance
wobble() {
// @ts-expect-error Property 'state' does not exist on type 'ExoPoint'.
const { getWobble } = this.state;
return getWobble();
}
}
harden(ExoWobblyPointBad);

test('test need for interface extension', t => {
t.throws(() => defineExoClassFromJSClass(ExoWobblyPointBad), {
message: 'methods ["wobble"] not guarded by "ExoPoint"',
});
});

class ExoWobblyPoint extends ExoPoint {
static implements = M.interface('ExoWobblyPointI', {
...getNamedMethodGuards(ExoPoint.implements),
wobble: M.call().returns(M.gte(0)),
});

static init(x, y, getWobble) {
return { ...super.init(x, y), getWobble };
}

getX() {
const {
// @ts-expect-error Property 'state' does not exist on type 'ExoPoint'.
state: { getWobble },
} = this;
// @ts-expect-error Property 'state' does not exist on type 'ExoPoint'.
const { getWobble } = this.state;
return super.getX() + getWobble();
}

wobble() {
// @ts-expect-error Property 'state' does not exist on type 'ExoPoint'.
const { getWobble } = this.state;
return getWobble();
}
}
harden(ExoWobblyPoint);

Expand All @@ -170,12 +208,15 @@ test('FarWobblyPoint inheritance', t => {
'getY',
'setY',
'toString',
'wobble',
]);
t.is(`${wpt}`, '<4,5>');
t.is(`${wpt}`, '<5,5>');
t.is(`${wpt}`, '<6,5>');
wpt.setY(6);
t.is(`${wpt}`, '<7,6>');
t.is(wpt.wobble(), 5);
t.is(`${wpt}`, '<9,6>');

const otherPt = makeExoPoint(1, 2);
t.false(otherPt instanceof ExoWobblyPoint);
Expand Down
9 changes: 9 additions & 0 deletions packages/patterns/NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ User-visible changes in `@endo/patterns`:
# Next release

- The `sloppy` option for `@endo/patterns` interface guards is deprecated. Use `defaultGuards` instead.
- `@endo/patterns` now exports a new `getNamedMethodGuards(interfaceGuard)` that returns that interface guard's record of method guards. The motivation is to support interface inheritance expressed by patterns like
```js
const I2 = M.interface('I2', {
...getNamedMethodGuards(I1),
doMore: M.call().returns(M.any()),
});
```
See `@endo/exo`'s `exo-wobbly-point.test.js` to see it in action together
with an experiment in class inheritance.

# 1.7.0 (2025-07-11)

Expand Down
1 change: 1 addition & 0 deletions packages/patterns/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export {
getMethodGuardPayload,
getInterfaceGuardPayload,
getInterfaceMethodKeys,
getNamedMethodGuards,
} from './src/patterns/getGuardPayloads.js';

// eslint-disable-next-line import/export
Expand Down
27 changes: 26 additions & 1 deletion packages/patterns/src/patterns/getGuardPayloads.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ const adaptMethodGuard = methodGuard => {
* we smooth the transition to https://github.com/endojs/endo/pull/1712,
* tolerating both the legacy and current guard shapes.
*
* Unlike LegacyAwaitArgGuardShape, tolerating LegacyInterfaceGuardShape
* Unlike `LegacyAwaitArgGuardShape`, tolerating `LegacyInterfaceGuardShape`
* does not seem like a currently exploitable bug, because there is not
* currently any context where either an interfaceGuard or a copyRecord would
* both be meaningful.
Expand Down Expand Up @@ -284,3 +284,28 @@ export const getInterfaceMethodKeys = interfaceGuard => {
]);
};
harden(getInterfaceMethodKeys);

// Tested in @endo/exo by exo-wobbly-point.test.js since that's already
// about class inheritance, which naturally goes with interface
// inheritance.
/**
* This ignores symbol-named method guards (which cannot be represented
* directly in a `CopyRecord` anyway). It returns only a `CopyRecord` of
* the string-named method guards. This is useful for interface guard
* inheritance patterns like
* ```js
* const I2 = M.interface('I2', {
* ...getNamedMethodGuards(I1),
* doMore: M.call().returns(M.any()),
* });
* ```
* While we could do more to support symbol-named method guards,
* this feature is deprecated, and hopefully will disappear soon.
* (TODO link to PRs removing symbol-named methods and method guards.)
*
* @template {Record<RemotableMethodName, MethodGuard>} [T=Record<RemotableMethodName, MethodGuard>]
* @param {InterfaceGuard<T>} interfaceGuard
*/
export const getNamedMethodGuards = interfaceGuard =>
getInterfaceGuardPayload(interfaceGuard).methodGuards;
harden(getNamedMethodGuards);