Compile module exports that pass this on with apply or call

This commit is contained in:
Chris Tate
2026-10-19 21:59:58 -05:00
parent 4bbf31b61f
commit 6dd82db8ff
2 changed files with 76 additions and 1 deletions
@@ -25,8 +25,19 @@ function exportedFunction(decl: ts.Node): ts.Node | null {
return null;
}
/** Whether `node` is `f.apply(this, ...)` or `f.call(this, ...)`, which
* passes the receiver on without reading it. */
function forwardsThis(node: ts.Node): node is ts.CallExpression {
if (!ts.isCallExpression(node)) return false;
const callee = node.expression;
if (!ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.name)) return false;
if (callee.name.text !== "apply" && callee.name.text !== "call") return false;
return node.arguments.nodes[0]?.kind === ts.SyntaxKind.ThisKeyword;
}
/** Whether a function's own body reads `this` (arrows share it; other
* functions and classes have their own). */
* functions and classes have their own). A `this` that `.apply` or `.call`
* only passes on is not a read. */
function readsOwnThis(fn: ts.Node): boolean {
let found = false;
const visit = (node: ts.Node): void => {
@@ -37,6 +48,11 @@ function readsOwnThis(fn: ts.Node): boolean {
}
if ((ts.isFunctionLike(node) && !ts.isArrowFunction(node)) || ts.isClassLikeDeclaration(node))
return;
if (forwardsThis(node)) {
visit(node.expression);
for (const arg of node.arguments.nodes.slice(1)) visit(arg);
return;
}
node.forEachChild(visit);
};
fn.forEachChild(visit);
+59
View File
@@ -1150,6 +1150,65 @@ export function plain(x) { return x + 1; }
/^Uncaught Error: calling a module export that reads 'this' through its module binding \('who': .*\) is not supported yet \[SC1090 at .*namespace-member-this\.mjs:3\]\n$/,
);
});
test("a namespace import's export that passes this on with apply or call compiles", async () => {
// Effect 4's provideService: `dual(...).apply(this, arguments)` hands the
// receiver to a function that never reads it, as here.
const r = await compileAndRun(
"namespace-member-forwards-this",
`import * as ns from './lib.mjs';
export const added = ns.provide(1, 2);
console.log(added, ns.sum(3, 4));
`,
"mjs",
{
"lib.mjs": `const dual = (body) => function () { return body.apply(this, arguments); };
export const provide = function () { return dual((a, b) => a + b).apply(this, arguments); };
const twice = function (a, b) { return (a + b) * 2; };
export const sum = function (a, b) { return twice.call(this, a, b); };
`,
},
);
expect(r).toEqual({ stdout: "3 14\n", stderr: "", exitCode: 0 });
});
test("a require binding's export that passes this on with apply compiles", async () => {
const r = await compileAndRun(
"require-member-forwards-this",
`'use strict';
const m = require('./lib.cjs');
console.log(m.scale(5));
`,
"cjs",
{
"lib.cjs": `'use strict';
const triple = function (x) { return x * 3; };
exports.scale = function () { return triple.apply(this, arguments); };
`,
},
);
expect(r).toEqual({ stdout: "15\n", stderr: "", exitCode: 0 });
});
test("an export that also reads this beside passing it on is a named deferred fence", async () => {
const r = await compileAndRun(
"namespace-member-forwards-and-reads-this",
`import * as ns from './lib.mjs';
console.log(ns.tag('x'));
`,
"mjs",
{
"lib.mjs": `const join = function (a, b) { return a + ':' + b; };
export function tag(x) { return join.call(this, x, typeof this); }
`,
},
);
expect(r.exitCode).toBe(1);
expect(r.stdout).toBe("");
expect(r.stderr).toMatch(
/^Uncaught Error: calling a module export that reads 'this' through its module binding \('tag': .*\) is not supported yet \[SC1090 at .*namespace-member-forwards-and-reads-this\.mjs:2\]\n$/,
);
});
});
// CommonJS forms around the module's exports that keep a refusal name the