Skip to content

Commit f311422

Browse files
committed
feat(ses): Support global lexicals
As distinct from endowments, extra properties of `globalThis`, global lexicals are constant free variables that are not reachable by computing properties of `globalThis`.
1 parent facae46 commit f311422

7 files changed

+355
-10
lines changed

packages/ses/src/compartment-shim.js

+17-2
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,12 @@ const assertModuleHooks = compartment => {
8282
export class Compartment {
8383
constructor(endowments = {}, modules = {}, options = {}) {
8484
// Extract options, and shallow-clone transforms.
85-
const { transforms = [], resolveHook, importHook } = options;
85+
const {
86+
transforms = [],
87+
globalLexicals = {},
88+
resolveHook,
89+
importHook,
90+
} = options;
8691
const globalTransforms = [...transforms];
8792

8893
const realmRec = getCurrentRealmRec();
@@ -133,6 +138,11 @@ export class Compartment {
133138
instances,
134139
globalTransforms,
135140
globalObject,
141+
// The caller continues to own the globalLexicals object they passed to
142+
// the compartment constructor, but the compartment only respects the
143+
// original values and they are constants in the scope of evaluated
144+
// programs and executed modules.
145+
globalLexicals: Object.freeze({ ...globalLexicals }),
136146
});
137147
}
138148

@@ -162,12 +172,17 @@ export class Compartment {
162172
} = options;
163173
const localTransforms = [...transforms];
164174

165-
const { globalTransforms, globalObject } = privateFields.get(this);
175+
const {
176+
globalTransforms,
177+
globalObject,
178+
globalLexicals,
179+
} = privateFields.get(this);
166180
const realmRec = getCurrentRealmRec();
167181
return performEval(realmRec, source, globalObject, endowments, {
168182
globalTransforms,
169183
localTransforms,
170184
sloppyGlobalsMode,
185+
globalLexicals,
171186
});
172187
}
173188

packages/ses/src/evaluate.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export function performEval(
2121
{
2222
localTransforms = [],
2323
globalTransforms = [],
24+
globalLexicals = {},
2425
sloppyGlobalsMode = false,
2526
} = {},
2627
) {
@@ -33,12 +34,13 @@ export function performEval(
3334
]);
3435

3536
const scopeHandler = createScopeHandler(realmRec, globalObject, endowments, {
37+
globalLexicals,
3638
sloppyGlobalsMode,
3739
});
3840
const scopeProxyRevocable = proxyRevocable(immutableObject, scopeHandler);
3941
// Ensure that "this" resolves to the scope proxy.
4042

41-
const constants = getScopeConstants(globalObject, endowments);
43+
const constants = getScopeConstants(globalObject, endowments, globalLexicals);
4244
const evaluateFactory = makeEvaluateFactory(realmRec, constants);
4345
const evaluate = apply(evaluateFactory, scopeProxyRevocable.proxy, []);
4446

packages/ses/src/module-instance.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ export const makeModuleInstance = (
3030
exportAlls,
3131
} = moduleRecord;
3232

33+
const compartmentFields = privateFields.get(compartment);
34+
3335
const { exportsProxy, proxiedExports, activate } = getDeferredExports(
3436
compartment,
35-
privateFields.get(compartment),
37+
compartmentFields,
3638
moduleAliases,
3739
moduleSpecifier,
3840
);
@@ -301,6 +303,8 @@ export const makeModuleInstance = (
301303
activate();
302304
}
303305

306+
const { globalLexicals } = compartmentFields;
307+
304308
const realmRec = getCurrentRealmRec();
305309
let optFunctor = performEval(
306310
realmRec,
@@ -310,6 +314,7 @@ export const makeModuleInstance = (
310314
{
311315
localTransforms: [],
312316
globalTransforms: [],
317+
globalLexicals,
313318
sloppyGlobalsMode: false,
314319
},
315320
);

packages/ses/src/scope-constants.js

+12-3
Original file line numberDiff line numberDiff line change
@@ -141,16 +141,25 @@ function isImmutableDataProperty(obj, name) {
141141
* service if any of the names are keywords or keyword-like. This is
142142
* safe and only prevent performance optimization.
143143
*/
144-
export function getScopeConstants(globalObject, localObject = {}) {
144+
export function getScopeConstants(
145+
globalObject,
146+
localObject = {},
147+
globalLexicals = {},
148+
) {
145149
// getOwnPropertyNames() does ignore Symbols so we don't need to
146150
// filter them out.
147151
const globalNames = getOwnPropertyNames(globalObject);
148152
const localNames = getOwnPropertyNames(localObject);
149153

154+
// All lexical globals are constants.
155+
const lexicalConstants = getOwnPropertyNames(globalLexicals);
156+
150157
// Collect all valid & immutable identifiers from the endowments.
151158
const localConstants = localNames.filter(
152159
name =>
153-
isValidIdentifierName(name) && isImmutableDataProperty(localObject, name),
160+
!lexicalConstants.includes(name) &&
161+
isValidIdentifierName(name) &&
162+
isImmutableDataProperty(localObject, name),
154163
);
155164

156165
// Collect all valid & immutable identifiers from the global that
@@ -164,5 +173,5 @@ export function getScopeConstants(globalObject, localObject = {}) {
164173
isImmutableDataProperty(globalObject, name),
165174
);
166175

167-
return [...globalConstants, ...localConstants];
176+
return [...globalConstants, ...localConstants, ...lexicalConstants];
168177
}

packages/ses/src/scope-handler.js

+11-3
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function createScopeHandler(
3838
realmRec,
3939
globalObject,
4040
endowments = {},
41-
{ sloppyGlobalsMode = false } = {},
41+
{ globalLexicals = {}, sloppyGlobalsMode = false } = {},
4242
) {
4343
return {
4444
// The scope handler throws if any trap other than get/set/has are run
@@ -67,6 +67,13 @@ export function createScopeHandler(
6767
// fall through
6868
}
6969

70+
// Global lexicals.
71+
if (prop in globalLexicals) {
72+
// Use reflect to defeat accessors that could be present on the
73+
// globalLexicals object itself as `this`. XXX?
74+
return reflectGet(globalLexicals, prop, globalObject);
75+
}
76+
7077
// Properties of the global.
7178
if (prop in endowments) {
7279
// Use reflect to defeat accessors that could be
@@ -96,7 +103,7 @@ export function createScopeHandler(
96103
return reflectSet(globalObject, prop, value);
97104
},
98105

99-
// we need has() to return false for some names to prevent the lookup from
106+
// we need has() to return false for some names to prevent the lookup from
100107
// climbing the scope chain and eventually reaching the unsafeGlobal
101108
// object (globalThis), which is bad.
102109

@@ -124,7 +131,8 @@ export function createScopeHandler(
124131
prop === 'eval' ||
125132
prop in endowments ||
126133
prop in globalObject ||
127-
prop in globalThis
134+
prop in globalThis ||
135+
prop in globalLexicals
128136
) {
129137
return true;
130138
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/* global Compartment */
2+
import test from 'tape';
3+
import '../src/main.js';
4+
5+
test('endowments own properties are mentionable', t => {
6+
t.plan(1);
7+
8+
const endowments = { hello: 'World!' };
9+
const modules = {};
10+
const compartment = new Compartment(endowments, modules);
11+
12+
const whom = compartment.evaluate('hello');
13+
t.equal(whom, 'World!');
14+
});
15+
16+
test('endowments own properties are enumerable', t => {
17+
t.plan(1);
18+
19+
const endowments = { hello: 'World!' };
20+
const modules = {};
21+
const compartment = new Compartment(endowments, modules);
22+
23+
const keys = compartment.evaluate('Object.keys(globalThis)');
24+
t.deepEqual(keys, ['hello']);
25+
});
26+
27+
test('endowments prototypically inherited properties are not mentionable', t => {
28+
t.plan(1);
29+
30+
const endowments = { __proto__: { hello: 'World!' } };
31+
const modules = {};
32+
const compartment = new Compartment(endowments, modules);
33+
34+
t.throws(() => compartment.evaluate('hello'), /hello is not defined/);
35+
});
36+
37+
test('endowments prototypically inherited properties are not enumerable', t => {
38+
t.plan(1);
39+
40+
const endowments = { __proto__: { hello: 'World!' } };
41+
const modules = {};
42+
const compartment = new Compartment(endowments, modules);
43+
44+
const keys = compartment.evaluate('Object.keys(globalThis)');
45+
t.deepEqual(keys, []);
46+
});
47+
48+
test('global lexicals are mentionable', t => {
49+
t.plan(1);
50+
51+
const endowments = {};
52+
const modules = {};
53+
const globalLexicals = { hello: 'World!' };
54+
const compartment = new Compartment(endowments, modules, { globalLexicals });
55+
56+
const whom = compartment.evaluate('hello');
57+
t.equal(whom, 'World!');
58+
});
59+
60+
test('global lexicals are not reachable from global object', t => {
61+
t.plan(1);
62+
63+
const endowments = {};
64+
const modules = {};
65+
const globalLexicals = { hello: 'World!' };
66+
const compartment = new Compartment(endowments, modules, { globalLexicals });
67+
68+
const keys = compartment.evaluate('Object.keys(globalThis)');
69+
t.deepEqual(keys, []);
70+
});
71+
72+
test('global lexicals prototypically inherited properties are not mentionable', t => {
73+
t.plan(1);
74+
75+
const endowments = {};
76+
const modules = {};
77+
const globalLexicals = { __proto__: { hello: 'World!' } };
78+
const compartment = new Compartment(endowments, modules, { globalLexicals });
79+
80+
t.throws(() => compartment.evaluate('hello'), /hello is not defined/);
81+
});
82+
83+
test('global lexicals prototypically inherited properties are not enumerable', t => {
84+
t.plan(1);
85+
86+
const endowments = {};
87+
const modules = {};
88+
const globalLexicals = { __proto__: { hello: 'World!' } };
89+
const compartment = new Compartment(endowments, modules, { globalLexicals });
90+
91+
const keys = compartment.evaluate('Object.keys(globalThis)');
92+
t.deepEqual(keys, []);
93+
});
94+
95+
test('global lexicals overshadow global object', t => {
96+
t.plan(1);
97+
98+
const endowments = { hello: 'Your name here' };
99+
const modules = {};
100+
const globalLexicals = { hello: 'World!' };
101+
const compartment = new Compartment(endowments, modules, { globalLexicals });
102+
103+
const whom = compartment.evaluate('hello');
104+
t.equal(whom, 'World!');
105+
});
106+
107+
test('global lexicals are constant', t => {
108+
t.plan(1);
109+
110+
const endowments = {};
111+
const modules = {};
112+
const globalLexicals = { hello: 'World!' };
113+
const compartment = new Compartment(endowments, modules, { globalLexicals });
114+
115+
t.throws(
116+
() => compartment.evaluate('hello = "Dave."'),
117+
/Assignment to constant/,
118+
);
119+
});
120+
121+
test('global lexicals are captured on construction', t => {
122+
t.plan(1);
123+
124+
const endowments = {};
125+
const modules = {};
126+
const globalLexicals = { hello: 'World!' };
127+
const compartment = new Compartment(endowments, modules, { globalLexicals });
128+
129+
// Psych!
130+
globalLexicals.hello = 'Something else';
131+
132+
const whom = compartment.evaluate('hello');
133+
t.equal(whom, 'World!');
134+
});

0 commit comments

Comments
 (0)