diff --git a/docs/content/limitations/builtin-apis/prototypes.mdx b/docs/content/limitations/builtin-apis/prototypes.mdx index 441884bfc..b349db3fb 100644 --- a/docs/content/limitations/builtin-apis/prototypes.mdx +++ b/docs/content/limitations/builtin-apis/prototypes.mdx @@ -1 +1 @@ -- **Prototypes:** Named properties and methods on program class prototypes support replacement, inheritance, and deletion. Bare prototype reflection, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported. Plain objects, arrays, functions, class instances, and Date, RegExp, Map, Set, WeakMap, WeakSet, and Promise values inherit `constructor`, `valueOf`, `toString`, `hasOwnProperty`, and `propertyIsEnumerable` from `Object.prototype` as in Node, so `value.constructor === Object` and `value.valueOf === Object.prototype.valueOf` compare identities exactly. The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called or constructed. +- **Prototypes:** Named properties and methods on program class prototypes support replacement, inheritance, and deletion. Bare prototype reflection, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported. Plain objects, arrays, functions, class instances, and Date, RegExp, Map, Set, WeakMap, WeakSet, and Promise values inherit `constructor`, `valueOf`, `toString`, `hasOwnProperty`, and `propertyIsEnumerable` from `Object.prototype` as in Node, so `value.constructor === Object` and `value.valueOf === Object.prototype.valueOf` compare identities exactly. `Object.prototype`'s own members read as values; its mutation and other reflection, such as `isPrototypeOf` and `__proto__`, are unsupported. The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called or constructed. diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6234-object-prototype-values.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6234-object-prototype-values.cjs.json new file mode 100644 index 000000000..bce305944 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6234-object-prototype-values.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6234-object-prototype-values.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 2d38f54e5..6d3b6bfc6 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -5713,6 +5713,8 @@ void scr_dyn_throw(ScrDyn *d) { scr_dyn_trace_v, scr_dyn_is_object(d)); } +static size_t scr_errdyn_find(const void *key, bool by_err); + ScrStr *scr_dyn_object_tag(const ScrDyn *d) { if (d->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Symbol.toStringTag lookup"); @@ -5736,7 +5738,7 @@ ScrStr *scr_dyn_object_tag(const ScrDyn *d) { case SCR_DYN_NUM: tag = "[object Number]"; break; case SCR_DYN_STR: tag = "[object String]"; break; case SCR_DYN_ARR: tag = "[object Array]"; break; - case SCR_DYN_OBJ: tag = "[object Object]"; break; + case SCR_DYN_OBJ: tag = scr_errdyn_find(d, false) != SIZE_MAX ? "[object Error]" : "[object Object]"; break; case SCR_DYN_HANDLE: if ((d->v.handle.tag >= SCR_DYNH_ABORT_SIGNAL && d->v.handle.tag <= SCR_DYNH_ABORT_CONTROLLER) || d->v.handle.tag == SCR_DYNH_EVENT_TARGET || @@ -10223,6 +10225,10 @@ bool scr_dyn_err_instanceof(const ScrDyn *d, double kind) { static ScrDyn *scr_dyn_obj_read_receiver(const ScrDyn *d, const char *key, size_t key_len, const ScrDyn *receiver) { for (const ScrDyn *current = d; current; current = scr_dyn_proto_next(current)) { if (current == scr_builtin_object_prototype) { + /* Object.prototype's own constructor and the methods its values + * share read as those values; other reflection stays fenced. */ + ScrDyn *member = scr_dyn_object_proto_inherited(key, key_len, SCR_INHERIT_ALL); + if (member) return member; scr_dyn_class_reflection_fence(current); return NULL; } @@ -11704,6 +11710,8 @@ bool scr_dyn_has_own(const ScrDyn *v, const ScrStr *key) { if (v->kind == SCR_DYN_OBJ) { + /* Object.prototype's own members (its value carries no entries). */ + if (v == scr_builtin_object_prototype) return scr_dyn_object_proto_name(key); return scr_dyn_obj_get(v, key->data, key->len) != NULL; } if (v->kind == SCR_DYN_ARR) { @@ -11736,6 +11744,8 @@ bool scr_dyn_has_own(const ScrDyn *v, const ScrStr *key) { } bool scr_dyn_property_is_enumerable(const ScrDyn *value, const ScrStr *key) { + /* Object.prototype's own members are all non-enumerable. */ + if (value == scr_builtin_object_prototype) return false; if (scr_dyn_class_reflection_fence(value)) return false; if (value->kind == SCR_DYN_TYPED_REF) { ScrDyn *view = scr_dyn_typed_ref_materialize(value); diff --git a/tests/corpus/6233-object-identity-dynamic.cjs b/tests/corpus/6233-object-identity-dynamic.cjs index b40a8493f..372e141b7 100644 --- a/tests/corpus/6233-object-identity-dynamic.cjs +++ b/tests/corpus/6233-object-identity-dynamic.cjs @@ -62,7 +62,8 @@ for (const name of Object.keys(values)) { console.log(name, hits.join("|") || "-", v.valueOf === Object.prototype.valueOf, name === "regexp" ? "-" : v.toString === Object.prototype.toString, v.hasOwnProperty === Object.prototype.hasOwnProperty, - v.propertyIsEnumerable === Object.prototype.propertyIsEnumerable); + v.propertyIsEnumerable === Object.prototype.propertyIsEnumerable, + Object.prototype.toString.call(v)); } const o = { a: 1 }; console.log(typeof Object, typeof Date, typeof Map, Object.name, Date.name, o.constructor.name, typeof o.constructor); @@ -78,9 +79,16 @@ function isPlainObj(value) { const prototype = Object.getPrototypeOf(value); return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null) && !(Symbol.toStringTag in value) && !(Symbol.iterator in value); } +function isPlainLodash(value) { + if (value == null || Object.prototype.toString.call(value) != "[object Object]") return false; + const proto = Object.getPrototypeOf(value); + if (proto === null) return true; + const Ctor = Object.prototype.hasOwnProperty.call(proto, "constructor") && proto.constructor; + return typeof Ctor == "function" && Ctor instanceof Ctor && Ctor === Object; +} function isPlainCtor(value) { return !!value && typeof value === "object" && (value.constructor === Object || value.constructor === undefined); } for (const name of ["plain", "parsed", "array", "nullProto", "created", "instance"]) { - console.log(name, isPlainObj(values[name]), isPlainCtor(values[name])); + console.log(name, isPlainObj(values[name]), isPlainLodash(values[name]), isPlainCtor(values[name])); } diff --git a/tests/corpus/6234-object-prototype-values.cjs b/tests/corpus/6234-object-prototype-values.cjs new file mode 100644 index 000000000..8e9cde0f3 --- /dev/null +++ b/tests/corpus/6234-object-prototype-values.cjs @@ -0,0 +1,26 @@ +// Object.prototype as a value: destructured and aliased methods, the +// prototype's own constructor and members, `{}.valueOf`, and the +// `Object.prototype.toString.call(x)` tag idiom (acorn, ini, qs, +// safe-stable-stringify, rfdc). +const { hasOwnProperty, propertyIsEnumerable } = Object.prototype; +const toStr = Object.prototype.toString; +const has = Object.prototype.hasOwnProperty; +const o = { a: 1 }; +console.log(hasOwnProperty.call(o, "a"), hasOwnProperty.call(o, "b"), has.call(o, "a"), propertyIsEnumerable.call(o, "a")); +console.log(toStr.call([]), toStr.call(null), toStr.call(undefined), toStr.call(new Date(0)), toStr.call(/x/), toStr.call(1), toStr.call("s"), toStr.call(true), toStr.call(o), toStr.call(new Error("e")), toStr.call(new RangeError("r")), toStr.call(() => 1), toStr.call(new Map())); +console.log({}.valueOf === Object.prototype.valueOf, ({}).toString === toStr, typeof Object.prototype.toString, Object.prototype.constructor === Object); +console.log(hasOwnProperty.call(Object.prototype, "constructor"), has.call(Object.prototype, "toString"), has.call(Object.prototype, "missing"), propertyIsEnumerable.call(Object.prototype, "toString")); +console.log(hasOwnProperty === Object.prototype.hasOwnProperty, o.hasOwnProperty === hasOwnProperty, [].hasOwnProperty === has); +function kind(value) { + switch (toStr.call(value)) { + case "[object Date]": return "date:" + value.getTime(); + case "[object Array]": return "array:" + value.length; + case "[object RegExp]": return "regexp:" + value.source; + case "[object Error]": return "error:" + value.message; + case "[object Object]": return "object:" + Object.keys(value).join(","); + default: return "other"; + } +} +console.log([new Date(5), [1, 2], /ab/, new Error("boom"), { p: 1, q: 2 }, 3].map(kind).join(" ")); +const config = JSON.parse('{"name":"app","db":{"host":"h"}}'); +for (const key in config) if (has.call(config, key)) console.log(key, typeof config[key], toStr.call(config[key]));