mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Reflect the prototype of function-local classes without prototype members (#1958)
This commit is contained in:
@@ -289,6 +289,16 @@ export function hasClassPrototypeData(info: ClassInfo): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** Methods and accessors that live on the class's prototype. */
|
||||
function declaresPrototypeMembers(decl: ts.ClassLikeDeclaration): boolean {
|
||||
return decl.members.nodes.some(
|
||||
(member) =>
|
||||
(ts.isMethodDeclaration(member) || ts.isAccessor(member)) &&
|
||||
!ts.isPrivateIdentifier(member.name) &&
|
||||
!ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword),
|
||||
);
|
||||
}
|
||||
|
||||
/** Populate the shared prototype view before exposing it to reflection.
|
||||
* Methods and accessors are non-enumerable and retain declaration identity. */
|
||||
export function reflectClassPrototype(
|
||||
@@ -318,7 +328,10 @@ export function reflectClassPrototype(
|
||||
// Local JavaScript factories initialize captured prototype members on
|
||||
// their constructor value, so the shared runtime view is already complete.
|
||||
if (info.localClass && info.runtimePrototypeMembers) return prototype;
|
||||
if (info.localClass)
|
||||
// A function-local class declaring no prototype members (zod 4's
|
||||
// `class Definition extends Parent {}`) has nothing to install: its
|
||||
// shared runtime view is complete once its base's is.
|
||||
if (info.localClass && declaresPrototypeMembers(info.decl))
|
||||
return refuse("prototype reflection on a function-local class is not supported yet");
|
||||
if (info.base && !info.base.builtinError) {
|
||||
reflectClassPrototype(
|
||||
@@ -544,6 +557,15 @@ export function lowerClassPrototypeData(
|
||||
if (name === "constructor") {
|
||||
return reflectClassPrototype(lowerer, info, locOf(expr), classValueRef(lowerer, info, expr));
|
||||
}
|
||||
// The whole prototype of a function-local class: the shared view
|
||||
// Object.getPrototypeOf answers for its instances.
|
||||
if (name === null && info.localClass) {
|
||||
const receiver = lowerer.lowerExprExpecting(expr.expression, {
|
||||
kind: "classval",
|
||||
className: info.def.name,
|
||||
});
|
||||
return reflectClassPrototype(lowerer, info, locOf(expr), receiver);
|
||||
}
|
||||
if (name === null || (isCompiledPrototypeMember(lowerer, info, name) && !method)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7613-local-class-prototype-reflection.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7615-local-class-prototype-reflection-ts.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -4416,6 +4416,11 @@ ScrDyn *scr_dyn_class_prototype(ScrDyn *value, ScrDyn *base) {
|
||||
* the class object and no registry keeps it alive, so evaluating a
|
||||
* function-local class again releases the previous one's prototype. */
|
||||
if (cls->name && cls->name->len) data->v.obj.class_name = cls->name->data;
|
||||
/* Like every class prototype, it starts with its own non-enumerable
|
||||
* `constructor`: the class value. The cycle it closes through the class
|
||||
* object is one the collector traces. */
|
||||
scr_dyn_obj_set(data, "constructor", 11, scr_dyn_retain(value));
|
||||
data->v.obj.entries[data->v.obj.len - 1].enumerable = false;
|
||||
cls->prototype_data = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v);
|
||||
scr_box_set_ref(cls->prototype_data, data);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// Prototype reflection on instances of a function-local class without
|
||||
// methods (zod 4's `$constructor` Definition): the prototype is the class's
|
||||
// shared prototype object, it chains to the parent's, and members defined
|
||||
// on it reach every instance.
|
||||
const installed = new WeakSet();
|
||||
function factory(name, Parent) {
|
||||
class Definition extends Parent {}
|
||||
Object.defineProperty(Definition, "name", { value: name });
|
||||
return function make(label) {
|
||||
const inst = new Definition();
|
||||
inst.label = label;
|
||||
const proto = Object.getPrototypeOf(inst);
|
||||
console.log(name, proto === Definition.prototype, proto === Object.getPrototypeOf(new Definition()));
|
||||
if (!installed.has(proto)) {
|
||||
installed.add(proto);
|
||||
Object.defineProperty(proto, "describe", {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
get() {
|
||||
return () => `${name}:${this.label}`;
|
||||
},
|
||||
});
|
||||
console.log("installed", name);
|
||||
}
|
||||
return inst;
|
||||
};
|
||||
}
|
||||
const makeError = factory("Failure", Error);
|
||||
const makePlain = factory("Thing", Object);
|
||||
const a = makeError("a");
|
||||
const b = makeError("b");
|
||||
const c = makePlain("c");
|
||||
console.log(a.describe(), b.describe(), c.describe());
|
||||
console.log(a instanceof Error, c instanceof Error, Object.keys(a).join(), Object.keys(c).join());
|
||||
console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(b), Object.getPrototypeOf(a) === Object.getPrototypeOf(c));
|
||||
console.log(typeof Object.getPrototypeOf(c).describe, "describe" in a, Object.hasOwn(a, "describe"));
|
||||
|
||||
// The prototype has its own non-enumerable `constructor`, the class value
|
||||
// of that evaluation, as every class prototype does.
|
||||
function local(Parent) {
|
||||
class Definition extends Parent {}
|
||||
const proto = Object.getPrototypeOf(new Definition());
|
||||
const descriptor = Object.getOwnPropertyDescriptor(proto, "constructor");
|
||||
console.log(
|
||||
proto.constructor === Definition,
|
||||
Object.hasOwn(proto, "constructor"),
|
||||
JSON.stringify(Object.getOwnPropertyNames(proto)),
|
||||
descriptor.value === Definition,
|
||||
descriptor.writable,
|
||||
descriptor.enumerable,
|
||||
descriptor.configurable,
|
||||
);
|
||||
return Definition;
|
||||
}
|
||||
console.log(local(Object) !== local(Error));
|
||||
@@ -0,0 +1,14 @@
|
||||
// Prototype reflection on an instance of a TypeScript function-local class
|
||||
// without methods: one shared prototype per class evaluation.
|
||||
function make(label: string): { label: string } {
|
||||
class Box {
|
||||
label = "";
|
||||
}
|
||||
const box = new Box();
|
||||
box.label = label;
|
||||
return box;
|
||||
}
|
||||
const a = make("a");
|
||||
const b = make("b");
|
||||
const pa = Object.getPrototypeOf(a);
|
||||
console.log(pa === Object.getPrototypeOf(a), pa === Object.getPrototypeOf(b), typeof pa, Object.keys(a).join());
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1967, "san": 2510 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1466, "san": 2143 }
|
||||
Reference in New Issue
Block a user