diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5913-prototype-type-error-messages.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5913-prototype-type-error-messages.ts.json new file mode 100644 index 000000000..f7e3d10b9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5913-prototype-type-error-messages.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5913-prototype-type-error-messages.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index cdfb3d0f9..958112b66 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -1729,13 +1729,40 @@ static bool scr_dyn_typed_ref_prototype(const ScrDyn *value) { return view && (view->kind == SCR_DYN_OBJ || view->kind == SCR_DYN_ARR); } +/* True when `prototype` is null or an object or function, the values + * Object.create and Object.setPrototypeOf accept. Otherwise throws Node's + * TypeError, whose message ends with the value's string form. */ +static bool scr_dyn_prototype_valid(const ScrDyn *prototype) { + if (scr_dyn_is_object(prototype) || scr_dyn_is_callable(prototype)) return true; + if (scr_exc_pending()) return false; + ScrStr *text = prototype->kind == SCR_DYN_SYMBOL + ? prototype->v.symbol.render(prototype->v.symbol.value) : scr_dyn_string_coerce_js(prototype); + if (!text) return false; + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, "Object prototype may only be an Object or null: "); + scr_jb_put_str(&b, text); + scr_str_release(text); + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); + return false; +} + +/* A valid receiver or prototype whose kind this operation does not handle. */ +static void scr_dyn_prototype_unsupported(const char *operation) { + ScrJsonBuf b; + scr_jb_init(&b); + scr_jb_puts(&b, operation); + scr_jb_puts(&b, " with this value is not supported yet"); + scr_throw_error(SCR_ERR_ERROR, scr_jb_finish(&b)); +} + ScrDyn *scr_dyn_obj_create(ScrDyn *prototype) { + if (!scr_dyn_prototype_valid(prototype)) return NULL; if (prototype != scr_builtin_object_prototype && scr_dyn_class_reflection_fence(prototype)) return NULL; bool typed = scr_dyn_typed_ref_prototype(prototype); if (scr_exc_pending()) return NULL; if (prototype->kind != SCR_DYN_OBJ && prototype->kind != SCR_DYN_ARR && prototype->kind != SCR_DYN_NULL && !typed) { - static const char message[] = "Object prototype may only be an Object or null"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + scr_dyn_prototype_unsupported("Object.create"); return NULL; } ScrDyn *object = prototype->kind == SCR_DYN_NULL ? scr_dyn_new_obj_null_proto() : scr_dyn_new_obj(); @@ -1786,12 +1813,7 @@ ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { * only check. */ if (object->kind == SCR_DYN_BOOL || object->kind == SCR_DYN_NUM || object->kind == SCR_DYN_STR || object->kind == SCR_DYN_BIGINT || object->kind == SCR_DYN_SYMBOL) { - bool valid = scr_dyn_is_object(prototype) || scr_dyn_is_callable(prototype); - if (scr_exc_pending()) return NULL; - if (valid) return scr_dyn_retain(object); - static const char message[] = "Object prototype may only be an Object or null"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); - return NULL; + return scr_dyn_prototype_valid(prototype) ? scr_dyn_retain(object) : NULL; } if (scr_dyn_class_reflection_fence(object) || scr_dyn_class_reflection_fence(prototype)) return NULL; if (object->kind == SCR_DYN_TYPED_REF) { @@ -1821,12 +1843,12 @@ ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); return NULL; } + if (!scr_dyn_prototype_valid(prototype)) return NULL; bool typed = scr_dyn_typed_ref_prototype(prototype); if (scr_exc_pending()) return NULL; if ((object->kind != SCR_DYN_OBJ && object->kind != SCR_DYN_ARR) || (prototype->kind != SCR_DYN_OBJ && prototype->kind != SCR_DYN_ARR && prototype->kind != SCR_DYN_FUNC && prototype->kind != SCR_DYN_NULL && !typed)) { - static const char message[] = "Object prototype may only be an Object or null"; - scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + scr_dyn_prototype_unsupported("Object.setPrototypeOf"); return NULL; } for (const ScrDyn *current = scr_dyn_chain_link(prototype); current;) { diff --git a/tests/corpus/5913-prototype-type-error-messages.ts b/tests/corpus/5913-prototype-type-error-messages.ts new file mode 100644 index 000000000..cb78e4cb1 --- /dev/null +++ b/tests/corpus/5913-prototype-type-error-messages.ts @@ -0,0 +1,39 @@ +// Object.setPrototypeOf and Object.create reject a prototype that is +// neither an object nor null with Node's TypeError, whose message ends with +// the value's string form: numbers as String(n) (-0 as 0), bigints without +// the n suffix, symbols as Symbol(description), and the empty string as +// nothing. + +function message(run: () => unknown): string { + try { + run(); + return "no error"; + } catch (error) { + return `${error instanceof TypeError} ${(error as Error).message}`; + } +} + +const prototypes: [string, unknown][] = [ + ["number", 7], + ["fraction", 1.5], + ["negative zero", -0], + ["nan", NaN], + ["string", "proto"], + ["empty string", ""], + ["boolean", true], + ["undefined", undefined], + ["bigint", 10n], + ["symbol", Symbol("tag")], +]; + +for (const [label, prototype] of prototypes) { + const invalid = prototype as object | null; + const target: unknown = JSON.parse('{"own":1}'); + console.log(label, "object receiver |", message(() => Object.setPrototypeOf(target, invalid))); + console.log(label, "primitive receiver |", message(() => Object.setPrototypeOf(5, invalid))); + console.log(label, "create |", message(() => Object.create(invalid))); +} + +const valid: unknown = JSON.parse('{"own":1}'); +console.log("null prototype |", message(() => Object.setPrototypeOf(valid, null))); +console.log("object prototype |", message(() => Object.create({ inherited: true }))); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 8f6ba50bb..8877c5042 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -1841,6 +1841,9 @@ "5648-dns-lookup-forms.ts": 2992, "5649-dns-service-servers.ts": 2712, "5650-dns-lookup-errors-untyped.cjs": 2822, + "5910-set-prototype-of-primitive-object-kinds.ts": 2719, + "5911-dyn-class-to-primitive.ts": 2850, + "5913-prototype-type-error-messages.ts": 2484, "5939-js-let-object-reassigned-wider.js": 1945, "5950-tuple-predicate-methods.ts": 5990, "5951-result-never-widening.ts": 4710, @@ -4473,6 +4476,9 @@ "5648-dns-lookup-forms.ts": 3416, "5649-dns-service-servers.ts": 4916, "5650-dns-lookup-errors-untyped.cjs": 3889, + "5910-set-prototype-of-primitive-object-kinds.ts": 5033, + "5911-dyn-class-to-primitive.ts": 4732, + "5913-prototype-type-error-messages.ts": 4068, "5939-js-let-object-reassigned-wider.js": 4436, "5950-tuple-predicate-methods.ts": 7790, "5951-result-never-widening.ts": 6120,