Repository navigation
runtime: validate Object.defineProperty descriptors and throw TypeErrors #2817
Description
Summary
Object.defineProperty and Object.defineProperties need the ES property-descriptor validation path. Node throws TypeError for non-object targets, non-object descriptors, invalid accessor/data descriptor combinations, and non-callable getters/setters.
Perry's current helper silently returns the target for several invalid inputs and accepts descriptor shapes that Node rejects.
Expected Node behavior
Local Node v25.9.0 probe:
Object.defineProperty({}, "x");
// TypeError: Property description must be an object: undefined
Object.defineProperty(1, "x", { value: 1 });
// TypeError: Object.defineProperty called on non-object
Object.defineProperty(null, "x", { value: 1 });
// TypeError: Object.defineProperty called on non-object
Object.defineProperty({}, "x", 1);
// TypeError: Property description must be an object: 1
Object.defineProperty({}, "x", { value: 1, get() { return 2; } });
// TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute
Object.defineProperty({}, "x", { get: 1 });
// TypeError: Getter must be a function: 1
Object.defineProperty({}, "x", { set: 1 });
// TypeError: Setter must be a function: 1
Object.defineProperties({}, null);
// TypeError: Cannot convert undefined or null to object
Object.defineProperties(1, { a: { value: 1 } });
// TypeError: Object.defineProperties called on non-object
Object.defineProperties({}, { x: 1 });
// TypeError: Property description must be an object: 1Valid descriptors should keep working, including enumerable/non-enumerable data properties and accessor descriptors.
Current Perry implementation
Source evidence from current tree:
crates/perry-runtime/src/object/object_ops.rs::js_object_define_property()extracts the target withextract_obj_ptr(obj_value)and returnsobj_valuewhen it is null, so non-object/nullish targets do not throw.- The same helper extracts the descriptor with
extract_obj_ptr(descriptor_value)and returnsobj_valuewhen it is null, so missing or primitive descriptors do not throw. - The helper comments that JS disallows mixing accessor and data descriptors, but it only checks for
get/setpresence; it does not throw when{ value, get }or{ writable, get }are combined. - The accessor branch stores whatever
get/setfields exist without validating that present non-undefinedvalues are callable functions. crates/perry-runtime/src/object/object_ops.rs::js_object_define_properties()returnstargetwhen the descriptor object is null/invalid, soObject.defineProperties({}, null)and primitive descriptor bags do not throw.- Static descriptor literals in
crates/perry-hir/src/lower/expr_call/native_module.rsare desugared to a sequence ofObjectDefinePropertycalls, so they inherit the same missing validation. crates/perry-codegen/src/expr/misc_methods.rsreturns the original object expression after callingjs_object_define_property, with no error/status channel visible at codegen.
Suggested test surface
Add parity tests for the invalid cases above plus controls for valid descriptors:
const o: any = {};
Object.defineProperty(o, "hidden", { value: 1, enumerable: false });
assert.deepEqual(Object.keys(o), []);
assert.equal(Object.getOwnPropertyDescriptor(o, "hidden")!.enumerable, false);
assert.throws(() => Object.defineProperty({}, "x"), TypeError);
assert.throws(() => Object.defineProperty(1 as any, "x", { value: 1 }), TypeError);
assert.throws(() => Object.defineProperty({}, "x", 1 as any), TypeError);
assert.throws(() => Object.defineProperty({}, "x", { value: 1, get() { return 2; } } as any), TypeError);
assert.throws(() => Object.defineProperty({}, "x", { get: 1 } as any), TypeError);
assert.throws(() => Object.defineProperties({}, null as any), TypeError);
assert.throws(() => Object.defineProperties({}, { x: 1 as any }), TypeError);Scope / non-goals
This is distinct from #2758, which tracks Reflect.defineProperty returning false on failed definitions and proxy trap behavior. This issue is specifically about Object static descriptor validation and the TypeError behavior Node exposes.
Activity