diff --git a/packages/compiler/src/frontend/lowering/lower-node-island.ts b/packages/compiler/src/frontend/lowering/lower-node-island.ts index 655c772687..8a9e5a34db 100644 --- a/packages/compiler/src/frontend/lowering/lower-node-island.ts +++ b/packages/compiler/src/frontend/lowering/lower-node-island.ts @@ -642,6 +642,14 @@ function toIsland(lowerer: Lowerer, node: ts.Expression): IrExpr { } } +/** Whether an island reads the lowered method `name` by that name: a + * public, string-keyed prototype method. The constructor, #private methods, + * accessor halves (`get:x`, `set:x`) and symbol-keyed slots (`sym:iterator`) + * are not properties an island can name. */ +function islandMethodName(name: string): boolean { + return name !== "constructor" && !name.startsWith("#") && !/^(?:get|set|sym):/.test(name); +} + /** A class instance crossing into an island: its capsule answers island * reads of its fields, and its prototype methods come from a table the * runtime builds once per class (nodeIsland.classMethods): the unbound @@ -661,7 +669,7 @@ function withClassMethods( c = c.def.base ? lowerer.classes.get(c.def.base) : undefined ) { for (const m of c.def.methods ?? []) - if (!names.includes(m) && m !== "constructor") names.push(m); + if (!names.includes(m) && islandMethodName(m)) names.push(m); } if (names.length === 0) return value; const loc = value.loc; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6838-node-island-private-methods/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6838-node-island-private-methods/main.ts.json new file mode 100644 index 0000000000..269c788081 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6838-node-island-private-methods/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6838-node-island-private-methods/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_node_island_export.c b/packages/runtime/src/scr_node_island_export.c index 6d17e8cb1c..a24cb64288 100644 --- a/packages/runtime/src/scr_node_island_export.c +++ b/packages/runtime/src/scr_node_island_export.c @@ -373,11 +373,17 @@ ScrDyn *scr_node_island_class_methods(ScrDyn *value, ScrDyn *builder) { return scr_dyn_retain(value); } -/* A prototype method of a class capsule (+1), or NULL. */ +/* A prototype method of a class capsule (+1), or NULL. An own property the + * island wrote under the method's name (kept in the capsule's materialized + * view) hides the method, as an own property hides a prototype's in Node. */ static ScrDyn *ni_class_method(const ScrDyn *target, const ScrDyn *key) { if (key->kind != SCR_DYN_STR) return NULL; ScrDyn *table = ni_methods_of(target); if (table == NULL) return NULL; + const ScrDyn *view = target->v.typed_ref.materialized; + if (view != NULL && view->kind == SCR_DYN_OBJ && + scr_dyn_obj_get(view, key->v.str->data, key->v.str->len) != NULL) + return NULL; ScrDyn *m = scr_dyn_obj_get(table, key->v.str->data, key->v.str->len); return m != NULL ? scr_dyn_retain(m) : NULL; } diff --git a/tests/corpus/6838-node-island-private-methods/main.ts b/tests/corpus/6838-node-island-private-methods/main.ts new file mode 100644 index 0000000000..c9d350f5f8 --- /dev/null +++ b/tests/corpus/6838-node-island-private-methods/main.ts @@ -0,0 +1,30 @@ +// @requires-libnode +// A native class instance passed into a Node island shows the island only +// its public members: its #private methods, and the names the compiler +// gives accessors and symbol-keyed methods, read as undefined, as in Node. +import { probe, shadow } from "private-pkg"; + +class Account { + #balance = 10; + owner: string; + constructor(owner: string) { + this.owner = owner; + } + #secret(): string { + return `secret of ${this.owner}: ${this.#balance}`; + } + get label(): string { + return `account ${this.owner}`; + } + describe(): string { + return `${this.owner} (${this.#secret().length})`; + } + *[Symbol.iterator](): Generator { + yield this.owner; + } +} + +const a = new Account("ada"); +console.log(probe(a)); +console.log(a.describe(), [...a].join(",")); +console.log(shadow(new Account("bob"))); diff --git a/tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/index.d.ts b/tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/index.d.ts new file mode 100644 index 0000000000..950911e747 --- /dev/null +++ b/tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/index.d.ts @@ -0,0 +1,2 @@ +export declare function probe(o: object): string; +export declare function shadow(o: object): string; diff --git a/tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/index.js b/tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/index.js new file mode 100644 index 0000000000..e1f4cbf8cd --- /dev/null +++ b/tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/index.js @@ -0,0 +1,22 @@ +"use strict"; + +// Reads a native instance's #private method and internal member names by +// string: an island sees only its public members. +function probe(o) { + const out = [typeof o["#secret"], "#secret" in o]; + // A property with the private method's spelling, holding undefined. + o["#secret"] = undefined; + const m = o["#secret"]; + out.push(typeof m, "#secret" in o); + if (typeof m === "function") out.push(m.call(o)); + out.push(typeof o["get:label"], typeof o["sym:iterator"], typeof o.describe); + return out.join(" "); +} + +// An own property holding undefined hides the prototype method. +function shadow(o) { + o.describe = undefined; + return `${typeof o.describe} ${Object.keys(o).includes("describe")}`; +} + +module.exports = { probe, shadow }; diff --git a/tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/package.json b/tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/package.json new file mode 100644 index 0000000000..780b3e0b63 --- /dev/null +++ b/tests/corpus/6838-node-island-private-methods/node_modules/private-pkg/package.json @@ -0,0 +1 @@ +{ "name": "private-pkg", "version": "1.0.0", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/6838-node-island-private-methods/package.json b/tests/corpus/6838-node-island-private-methods/package.json new file mode 100644 index 0000000000..e9e6cb5799 --- /dev/null +++ b/tests/corpus/6838-node-island-private-methods/package.json @@ -0,0 +1 @@ +{ "name": "island-private-methods", "private": true, "type": "module", "scriptc": { "packages": { "private-pkg": "node" } } } diff --git a/tests/harness/corpus-costs/6838-node-island-private-methods/main.ts.json b/tests/harness/corpus-costs/6838-node-island-private-methods/main.ts.json new file mode 100644 index 0000000000..d2c83f9675 --- /dev/null +++ b/tests/harness/corpus-costs/6838-node-island-private-methods/main.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3300 }