Skip to content
Open
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
8 changes: 2 additions & 6 deletions src/jsc/bindings/NodeVM.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1593,16 +1593,14 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleCompileFunction, (JSGlobalObject * globalObject
// Create the source origin
SourceOrigin sourceOrigin { WTF::URL::fileURLWithFileSystemPath(options.filename), *fetcher };

// Process contextExtensions if they exist
JSScope* functionScope = options.parsingContext ? options.parsingContext : globalObject;
JSScope* functionScope = options.parsingContext->globalScope();

if (!options.contextExtensions.isUndefinedOrNull() && !options.contextExtensions.isEmpty() && options.contextExtensions.isObject() && isArray(globalObject, options.contextExtensions)) {
auto* contextExtensionsArray = dynamicDowncast<JSArray>(options.contextExtensions);
unsigned length = contextExtensionsArray ? contextExtensionsArray->length() : 0;

if (length > 0) {
// Get the global scope from the parsing context
JSScope* currentScope = options.parsingContext->globalScope();
JSScope* currentScope = functionScope;

// Create JSWithScope objects for each context extension
for (unsigned i = 0; i < length; i++) {
Expand All @@ -1619,8 +1617,6 @@ JSC_DEFINE_HOST_FUNCTION(vmModuleCompileFunction, (JSGlobalObject * globalObject
}
}

options.parsingContext->setGlobalScopeExtension(functionScope);

// Create the function using constructAnonymousFunction with the appropriate scope chain
JSFunction* function = constructAnonymousFunction(globalObject, ArgList(constructFunctionArgs), sourceOrigin, WTF::move(options), JSC::SourceTaintedOrigin::Untainted, functionScope);
RETURN_IF_EXCEPTION(scope, {});
Expand Down
68 changes: 68 additions & 0 deletions test/js/node/vm/vm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,74 @@ describe("vm", () => {
expect(e).toBeTruthy();
}
});

// Top-level let/const/class declared by a script live in the context's
// global lexical environment, not on its global object. Like Node, the
// compiled function resolves names through it, whether it is compiled in
// the caller's context or in a parsingContext.
describe.each([
[
"this context",
() => ({
run: (code: string) => runInThisContext(code),
options: {},
globalObject: globalThis as object,
}),
],
[
"a parsingContext",
() => {
// Undeclared sloppy-mode assignments made inside the context land on
// the contextified object, the way they land on globalThis above.
const context = createContext({});
return {
run: (code: string) => runInContext(code, context),
options: { parsingContext: context },
globalObject: context as object,
};
},
],
])("global lexical bindings of %s", (_label, setup) => {
test("are visible to the compiled function", () => {
const { run, options } = setup();
const [v, l, c, k] = randomProps(4);
run(`var ${v} = "var"; let ${l} = "let"; const ${c} = "const"; class ${k} {}`);
const fn = compileFunction(`return [${v}, ${l}, ${c}, typeof ${k}];`, [], options);
expect(fn()).toEqual(["var", "let", "const", "function"]);
});

test("are visible once declared, even after the function was compiled and called", () => {
const { run, options } = setup();
const [name] = randomProps(1);
const fn = compileFunction(`return ${name};`, [], options);
expect(fn).toThrow(`${name} is not defined`);
run(`let ${name} = "declared later";`);
expect(fn()).toBe("declared later");
});

test("receive assignments instead of a new global property being created", () => {
const { run, options, globalObject } = setup();
const [name] = randomProps(1);
run(`let ${name} = "initial";`);
compileFunction(`${name} = "assigned";`, [], options)();
expect(run(name)).toBe("assigned");
expect(Object.hasOwn(globalObject, name)).toBe(false);
});

test("are shadowed by contextExtensions, which only the compiled function sees", () => {
const { run, options } = setup();
const [shadowed, visible, extensionOnly] = randomProps(3);
run(`let ${shadowed} = "lexical"; let ${visible} = "lexical";`);
const fn = compileFunction(`return [${shadowed}, ${visible}, ${extensionOnly}];`, [], {
...options,
contextExtensions: [{ [shadowed]: "extension", [extensionOnly]: "extension" }],
});
expect(fn()).toEqual(["extension", "lexical", "extension"]);
// The scope chain built for fn must not be installed on the context itself.
expect(run(`[${shadowed}, typeof ${extensionOnly}]`)).toEqual(["lexical", "undefined"]);
expect(compileFunction(`return typeof ${extensionOnly};`, [], options)()).toBe("undefined");
});
});
});
});

Expand Down