Skip to content

runtime: validate Object.defineProperty descriptors and throw TypeErrors #2817

Closed

Description

@andrewtdiz

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: 1

Valid 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 with extract_obj_ptr(obj_value) and returns obj_value when it is null, so non-object/nullish targets do not throw.
  • The same helper extracts the descriptor with extract_obj_ptr(descriptor_value) and returns obj_value when 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/set presence; it does not throw when { value, get } or { writable, get } are combined.
  • The accessor branch stores whatever get / set fields exist without validating that present non-undefined values are callable functions.
  • crates/perry-runtime/src/object/object_ops.rs::js_object_define_properties() returns target when the descriptor object is null/invalid, so Object.defineProperties({}, null) and primitive descriptor bags do not throw.
  • Static descriptor literals in crates/perry-hir/src/lower/expr_call/native_module.rs are desugared to a sequence of ObjectDefineProperty calls, so they inherit the same missing validation.
  • crates/perry-codegen/src/expr/misc_methods.rs returns the original object expression after calling js_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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions