From 6dd82db8ff2eed564497b56c5bd78bde75b08092 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 21:59:58 -0500 Subject: [PATCH 1/3] Compile module exports that pass this on with apply or call --- .../frontend/lowering/module-member-this.ts | 18 +++++- tests/harness/errors.test.ts | 59 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/lowering/module-member-this.ts b/packages/compiler/src/frontend/lowering/module-member-this.ts index 205cc63e9..ce020d197 100644 --- a/packages/compiler/src/frontend/lowering/module-member-this.ts +++ b/packages/compiler/src/frontend/lowering/module-member-this.ts @@ -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); diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 26dfd3a5c..6a779528d 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -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 From 751d22ae6e64f7c6aff2e4543e4748ffd37820e2 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 23:09:08 -0500 Subject: [PATCH 2/3] Pass this on to module exports only where every function that gets it ignores it The exemption for `f.apply(this, ...)` and `f.call(this, ...)` dropped SC1090 for every module export that passes `this` on. When the function it reaches reads `this`, Node gives it the module object, and the direct module call has none, so the result differed from Node. A `this` that is passed on now counts as unread only when the function it goes to provably ignores its `this`: an arrow, a function that uses `this` only to pass it on to such functions, or what a known function returns, with its parameters bound to the call's arguments and never assigned. The analysis follows at most six calls and functions, and refuses otherwise. Effect 4's provideService still compiles: it passes `this` to what dual(3, body) returns, which passes it on only to the arrow it was given. Passing `this` on to a function that reads it now refuses, directly, through such a factory, or when the factory reassigns its parameter. --- .../frontend/lowering/module-member-this.ts | 156 ++++++++++++++++-- tests/harness/errors.test.ts | 123 +++++++++++++- 2 files changed, 263 insertions(+), 16 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/module-member-this.ts b/packages/compiler/src/frontend/lowering/module-member-this.ts index ce020d197..faa1341d8 100644 --- a/packages/compiler/src/frontend/lowering/module-member-this.ts +++ b/packages/compiler/src/frontend/lowering/module-member-this.ts @@ -26,8 +26,10 @@ function exportedFunction(decl: ts.Node): ts.Node | 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 { + * passes the receiver on to `f`. */ +function forwardsThis(node: ts.Node): node is ts.CallExpression & { + expression: ts.PropertyAccessExpression; +} { if (!ts.isCallExpression(node)) return false; const callee = node.expression; if (!ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.name)) return false; @@ -35,30 +37,162 @@ function forwardsThis(node: ts.Node): node is ts.CallExpression { 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). A `this` that `.apply` or `.call` - * only passes on is not a read. */ -function readsOwnThis(fn: ts.Node): boolean { - let found = false; +/* The arguments a call gave a function's parameters, while the analysis + * follows the call into the function: each with the bindings of its own + * scope. */ +type Bindings = ReadonlyMap; +const noBindings: Bindings = new Map(); + +// How many calls and functions deep the analysis follows a forwarded +// `this`: enough for a wrapper over a function factory, as Effect's +// `dual(3, body).apply(this, arguments)`. +const maxDepth = 6; + +/** Whether `fn`'s own body uses `this` only to pass it on with `.apply` or + * `.call` to functions that ignore it (and so also when it doesn't use + * `this` at all). */ +function ignoresOwnThis(lowerer: Lowerer, fn: ts.Node, bindings: Bindings, depth: number): boolean { + let ignores = true; const visit = (node: ts.Node): void => { - if (found) return; + if (!ignores) return; if (node.kind === ts.SyntaxKind.ThisKeyword) { - found = true; + ignores = false; return; } if ((ts.isFunctionLike(node) && !ts.isArrowFunction(node)) || ts.isClassLikeDeclaration(node)) return; if (forwardsThis(node)) { - visit(node.expression); + if (!ignoresThis(lowerer, node.expression.expression, bindings, depth + 1)) { + ignores = false; + return; + } + visit(node.expression.expression); for (const arg of node.arguments.nodes.slice(1)) visit(arg); return; } node.forEachChild(visit); }; fn.forEachChild(visit); + return ignores; +} + +/** The function `name` provably holds: a function declaration, a const + * initialized with a function, or an import of one. */ +function functionOfName( + lowerer: Lowerer, + name: ts.Identifier, + bindings: Bindings, +): { fn: ts.Node | null; bound?: { expr: ts.Expression; bindings: Bindings } } { + let symbol = lowerer.checker.getSymbolAtLocation(name); + if (symbol === undefined) return { fn: null }; + const bound = bindings.get(symbol); + if (bound) return { fn: null, bound }; + if ((symbol.flags & ts.SymbolFlags.Alias) !== 0) + symbol = lowerer.checker.getAliasedSymbol(symbol); + const decls = symbol.declarations; + if (decls.length !== 1) return { fn: null }; + const decl = decls[0]!; + if (ts.isFunctionDeclaration(decl)) return { fn: decl }; + if ( + ts.isVariableDeclaration(decl) && + decl.initializer && + ts.isVariableDeclarationList(decl.parent) && + (decl.parent.flags & ts.NodeFlags.Const) !== 0 + ) { + let init: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (ts.isFunctionExpression(init) || ts.isArrowFunction(init)) return { fn: init }; + } + return { fn: null }; +} + +/** Whether `fn` (with its nested functions) assigns the binding `symbol`. */ +function assigns(lowerer: Lowerer, fn: ts.Node, symbol: ts.Symbol): boolean { + let found = false; + const isTarget = (node: ts.Node): boolean => + ts.isIdentifier(node) && lowerer.checker.getSymbolAtLocation(node) === symbol; + const visit = (node: ts.Node): void => { + if (found) return; + if ( + (ts.isBinaryExpression(node) && + node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + node.operatorToken.kind <= ts.SyntaxKind.LastAssignment && + isTarget(node.left)) || + ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) && + (node.operator === ts.SyntaxKind.PlusPlusToken || + node.operator === ts.SyntaxKind.MinusMinusToken) && + isTarget(node.operand)) + ) { + found = true; + return; + } + node.forEachChild(visit); + }; + fn.forEachChild(visit); return found; } +/** The expressions `fn` returns from its own body. */ +function returnedExpressions(fn: ts.Node): ts.Expression[] | null { + const body = (fn as { body?: ts.Node }).body; + if (body === undefined) return null; + if (!ts.isBlock(body)) return [body as ts.Expression]; + const result: ts.Expression[] = []; + const visit = (node: ts.Node): void => { + if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return; + if (ts.isReturnStatement(node) && node.expression) result.push(node.expression); + node.forEachChild(visit); + }; + body.forEachChild(visit); + return result; +} + +/** Whether the function `expr` evaluates to provably ignores the `this` + * it is called with. */ +function ignoresThis( + lowerer: Lowerer, + expr: ts.Expression, + bindings: Bindings, + depth: number, +): boolean { + if (depth > maxDepth) return false; + while (ts.isParenthesizedExpression(expr)) expr = expr.expression; + if (ts.isArrowFunction(expr)) return true; + if (ts.isFunctionExpression(expr)) return ignoresOwnThis(lowerer, expr, bindings, depth); + if (ts.isIdentifier(expr)) { + const { fn, bound } = functionOfName(lowerer, expr, bindings); + if (bound) return ignoresThis(lowerer, bound.expr, bound.bindings, depth + 1); + if (fn === null) return false; + return ts.isArrowFunction(fn) || ignoresOwnThis(lowerer, fn, noBindings, depth + 1); + } + // A call of a known function: what it returns, with its parameters bound + // to this call's arguments. + if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression)) { + const { fn } = functionOfName(lowerer, expr.expression, bindings); + if (fn === null) return false; + const params = (fn as { parameters?: ts.NodeArray }).parameters; + const inner = new Map(bindings); + for (const [i, param] of (params?.nodes ?? []).entries()) { + if (!ts.isIdentifier(param.name) || param.dotDotDotToken) return false; + const arg = expr.arguments.nodes[i]; + const symbol = lowerer.checker.getSymbolAtLocation(param.name); + if (arg === undefined || ts.isSpreadElement(arg) || symbol === undefined) return false; + // The parameter holds the argument for as long as the function runs. + if (assigns(lowerer, fn, symbol)) return false; + inner.set(symbol, { expr: arg, bindings }); + } + const returned = returnedExpressions(fn); + return returned !== null && returned.every((r) => ignoresThis(lowerer, r, inner, depth + 1)); + } + return false; +} + +/** Whether calling `fn` with another `this` than Node's can change what it + * does: it reads `this`, or passes it on to a function that may. */ +function readsOwnThis(lowerer: Lowerer, fn: ts.Node): boolean { + return !ignoresOwnThis(lowerer, fn, noBindings, 0); +} + export function refuseModuleMemberThis( lowerer: Lowerer, call: ts.CallExpression, @@ -70,7 +204,7 @@ export function refuseModuleMemberThis( if (symbol === undefined) return; for (const decl of symbol.declarations) { const fn = exportedFunction(decl); - if (fn === null || !readsOwnThis(fn)) continue; + if (fn === null || !readsOwnThis(lowerer, fn)) continue; lowerer.unsupported( "SC1090", call, diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 6a779528d..b2d5358b3 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -1153,23 +1153,31 @@ export function plain(x) { return x + 1; } 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. + // receiver to a function that `dual` returns, which reads it only to + // pass it on to an arrow, as here. const r = await compileAndRun( "namespace-member-forwards-this", `import * as ns from './lib.mjs'; -export const added = ns.provide(1, 2); +export const added = ns.provide(1, 2, 3); 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); }; + "lib.mjs": `const dual = function (arity, body) { + switch (arity) { + case 2: + return function (a, b) { return arguments.length >= 2 ? body(a, b) : (self) => body(self, a); }; + default: + return function () { return body.apply(this, arguments); }; + } +}; +export const provide = function () { return dual(3, (a, b, c) => a + b + c).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 }); + expect(r).toEqual({ stdout: "6 14\n", stderr: "", exitCode: 0 }); }); test("a require binding's export that passes this on with apply compiles", async () => { @@ -1190,6 +1198,111 @@ exports.scale = function () { return triple.apply(this, arguments); }; expect(r).toEqual({ stdout: "15\n", stderr: "", exitCode: 0 }); }); + test.each([ + [ + "a namespace import's export that passes this on to a function that reads it", + "namespace-member-forwards-to-reader", + "mjs", + `import * as ns from './lib.mjs'; +console.log(ns.plain(1)); +console.log(ns.viaCall('?')); +`, + { + "lib.mjs": `export const tag = 'ns'; +function read(suffix) { return this.tag + suffix; } +export function viaCall(suffix) { return read.call(this, suffix); } +export function plain(x) { return x + 1; } +`, + }, + "2\n", + "viaCall", + 3, + ], + [ + "a namespace import's export that passes this on through a factory to a function that reads it", + "namespace-member-forwards-through-factory", + "mjs", + `import * as ns from './lib.mjs'; +console.log(ns.plain(1)); +console.log(ns.viaDual('?')); +`, + { + "lib.mjs": `export const tag = 'ns'; +const dual = function (arity, body) { + switch (arity) { + case 2: + return function (a, b) { return arguments.length >= 2 ? body(a, b) : (self) => body(self, a); }; + default: + return function () { return body.apply(this, arguments); }; + } +}; +export const viaDual = function () { return dual(9, function (s) { return this.tag + s; }).apply(this, arguments); }; +export function plain(x) { return x + 1; } +`, + }, + "2\n", + "viaDual", + 3, + ], + [ + "a namespace import's export that passes this on through a factory that reassigns its parameter", + "namespace-member-forwards-reassigned", + "mjs", + `import * as ns from './lib.mjs'; +console.log(ns.plain(1)); +console.log(ns.viaWrap('?')); +`, + { + "lib.mjs": `export const tag = 'ns'; +const wrap = function (body) { + body = function (s) { return this.tag + s; }; + return function () { return body.apply(this, arguments); }; +}; +export const viaWrap = function () { return wrap((s) => s).apply(this, arguments); }; +export function plain(x) { return x + 1; } +`, + }, + "2\n", + "viaWrap", + 3, + ], + [ + "a require binding's export that passes this on to a function that reads it", + "require-member-forwards-to-reader", + "cjs", + `'use strict'; +const m = require('./lib.cjs'); +console.log(m.plain(21)); +console.log(m.viaApply('!')); +`, + { + "lib.cjs": `'use strict'; +exports.name = 'lib'; +const read = function (suffix) { return this.name + suffix; }; +exports.viaApply = function () { return read.apply(this, arguments); }; +exports.plain = function (x) { return x * 2; }; +`, + }, + "42\n", + "viaApply", + 4, + ], + ] as const)( + "%s is a named deferred fence", + async (_, name, ext, source, files, stdout, member, line) => { + // Node passes the module object on to `read`, which reads it; the + // direct call has none to pass. + const r = await compileAndRun(name, source, ext, files); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe(stdout); + expect(r.stderr).toMatch( + new RegExp( + `^Uncaught Error: calling a module export that reads 'this' through its module binding \\('${member}': .*\\) is not supported yet \\[SC1090 at .*${name}\\.${ext}:${line}\\]\\n$`, + ), + ); + }, + ); + 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", From b690f8f8f493d63231dfb752a31a913a3013be1b Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Mon, 19 Oct 2026 23:56:22 -0500 Subject: [PATCH 3/3] Count destructuring and for-of writes of a factory's parameter A factory's parameter counts as the argument it was called with only while the factory can't change it. The check saw assignments and updates of the name, but not a write through a destructuring pattern (`[body] = ...`, `({ body } = ...)`) or a for-in or for-of target, so a factory could replace its callback with one that reads `this` and the call still compiled without the module object. The parameter now counts as changed by any assignment, update, destructuring or for-in/for-of target that writes it, by a declaration of the same name, by a write to an element of `arguments`, and by a call of `eval`. --- .../frontend/lowering/module-member-this.ts | 70 ++++++++++++++++--- tests/harness/errors.test.ts | 66 +++++++++++++++++ 2 files changed, 128 insertions(+), 8 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/module-member-this.ts b/packages/compiler/src/frontend/lowering/module-member-this.ts index faa1341d8..592136bec 100644 --- a/packages/compiler/src/frontend/lowering/module-member-this.ts +++ b/packages/compiler/src/frontend/lowering/module-member-this.ts @@ -106,29 +106,83 @@ function functionOfName( return { fn: null }; } -/** Whether `fn` (with its nested functions) assigns the binding `symbol`. */ -function assigns(lowerer: Lowerer, fn: ts.Node, symbol: ts.Symbol): boolean { +/** + * Whether `fn` (with its nested functions) may change what its parameter + * `param` holds: an assignment, update, destructuring or for-in/for-of + * target that writes it, a declaration of the same name, a write to an + * element of `arguments` (which aliases parameters outside strict mode), or + * a call of `eval`. + */ +function mayRebind(lowerer: Lowerer, fn: ts.Node, param: ts.Identifier): boolean { + const symbol = lowerer.checker.getSymbolAtLocation(param); + const refersTo = (node: ts.Node): boolean => + ts.isIdentifier(node) && + (node.text === param.text || lowerer.checker.getSymbolAtLocation(node) === symbol); + // Whether writing `target` writes the parameter: the name itself, or the + // name inside a destructuring pattern. + const writes = (target: ts.Node): boolean => { + while (ts.isParenthesizedExpression(target)) target = target.expression; + if (refersTo(target)) return true; + if ( + ts.isElementAccessExpression(target) && + ts.isIdentifier(target.expression) && + target.expression.text === "arguments" + ) + return true; + if (ts.isArrayLiteralExpression(target)) return target.elements.nodes.some(writes); + if (ts.isObjectLiteralExpression(target)) + return target.properties.nodes.some((p) => + ts.isShorthandPropertyAssignment(p) + ? refersTo(p.name) + : ts.isPropertyAssignment(p) + ? writes(p.initializer) + : ts.isSpreadAssignment(p) && writes(p.expression), + ); + if (ts.isSpreadElement(target)) return writes(target.expression); + // A default in a pattern: `[body = other] = values`. + if (ts.isBinaryExpression(target) && target.operatorToken.kind === ts.SyntaxKind.EqualsToken) + return writes(target.left); + return false; + }; + const declaresName = (node: ts.Node): boolean => { + const name = (node as { name?: ts.Node }).name; + return name !== undefined && ts.isIdentifier(name) && name.text === param.text; + }; let found = false; - const isTarget = (node: ts.Node): boolean => - ts.isIdentifier(node) && lowerer.checker.getSymbolAtLocation(node) === symbol; const visit = (node: ts.Node): void => { if (found) return; if ( (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment && - isTarget(node.left)) || + writes(node.left)) || ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) && (node.operator === ts.SyntaxKind.PlusPlusToken || node.operator === ts.SyntaxKind.MinusMinusToken) && - isTarget(node.operand)) + writes(node.operand)) || + ((ts.isForOfStatement(node) || ts.isForInStatement(node)) && + !ts.isVariableDeclarationList(node.initializer) && + writes(node.initializer)) || + ((ts.isVariableDeclaration(node) || + ts.isBindingElement(node) || + ts.isParameter(node) || + ts.isFunctionDeclaration(node) || + ts.isClassDeclaration(node)) && + declaresName(node)) || + (ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === "eval") ) { found = true; return; } node.forEachChild(visit); }; - fn.forEachChild(visit); + // The function's own parameters declare the name; their defaults can write it. + const params = (fn as { parameters?: ts.NodeArray }).parameters; + for (const p of params?.nodes ?? []) if (p.initializer) visit(p.initializer); + const body = (fn as { body?: ts.Node }).body; + if (body !== undefined) visit(body); return found; } @@ -178,7 +232,7 @@ function ignoresThis( const symbol = lowerer.checker.getSymbolAtLocation(param.name); if (arg === undefined || ts.isSpreadElement(arg) || symbol === undefined) return false; // The parameter holds the argument for as long as the function runs. - if (assigns(lowerer, fn, symbol)) return false; + if (mayRebind(lowerer, fn, param.name)) return false; inner.set(symbol, { expr: arg, bindings }); } const returned = returnedExpressions(fn); diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index b2d5358b3..eea541912 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -1260,6 +1260,72 @@ const wrap = function (body) { }; export const viaWrap = function () { return wrap((s) => s).apply(this, arguments); }; export function plain(x) { return x + 1; } +`, + }, + "2\n", + "viaWrap", + 3, + ], + [ + "a namespace import's export that passes this on through a factory that replaces its parameter as a destructuring", + "namespace-member-forwards-destructured", + "mjs", + `import * as ns from './lib.mjs'; +console.log(ns.plain(1)); +console.log(ns.viaWrap('?')); +`, + { + "lib.mjs": `export const tag = 'ns'; +const wrap = function (body) { + [body] = [function (s) { return this.tag + s; }]; + return function () { return body.apply(this, arguments); }; +}; +export const viaWrap = function () { return wrap((s) => s).apply(this, arguments); }; +export function plain(x) { return x + 1; } +`, + }, + "2\n", + "viaWrap", + 3, + ], + [ + "a namespace import's export that passes this on through a factory that replaces its parameter as a object destructuring", + "namespace-member-forwards-object-destructured", + "mjs", + `import * as ns from './lib.mjs'; +console.log(ns.plain(1)); +console.log(ns.viaWrap('?')); +`, + { + "lib.mjs": `export const tag = 'ns'; +const wrap = function (body) { + ({ body } = { body: function (s) { return this.tag + s; } }); + return function () { return body.apply(this, arguments); }; +}; +export const viaWrap = function () { return wrap((s) => s).apply(this, arguments); }; +export function plain(x) { return x + 1; } +`, + }, + "2\n", + "viaWrap", + 3, + ], + [ + "a namespace import's export that passes this on through a factory that replaces its parameter as a for-of target", + "namespace-member-forwards-for-of", + "mjs", + `import * as ns from './lib.mjs'; +console.log(ns.plain(1)); +console.log(ns.viaWrap('?')); +`, + { + "lib.mjs": `export const tag = 'ns'; +const wrap = function (body) { + for (body of [function (s) { return this.tag + s; }]); + return function () { return body.apply(this, arguments); }; +}; +export const viaWrap = function () { return wrap((s) => s).apply(this, arguments); }; +export function plain(x) { return x + 1; } `, }, "2\n",