From: Lady Date: Fri, 24 Nov 2023 16:37:08 +0000 (-0500) Subject: De‐classify property descriptors; move to value.js X-Git-Url: https://git.ladys.computer/Pisces/commitdiff_plain/50ab30fc3a257e46da6b8bdc0dad054f729e16e1?ds=inline De‐classify property descriptors; move to value.js The conversion to and from property descriptors doesn’t only look at own properties, so it is convenient to create ones with a null prototype (to prevent attempts at hacking into, e·g, `Object.prototype`). This means the existing class‐based approach isn’t viable and a more functional approach is better. Many of the property descriptor functions explicitly handle undefined, so treating them as values and not objects makes sense, even if some (`completePropertyDescriptor`, `toPropertyDescriptor`) throw if their argument is not an object. --- diff --git a/dev-deps.js b/dev-deps.js index f2ed7c9..3c4981e 100644 --- a/dev-deps.js +++ b/dev-deps.js @@ -10,15 +10,16 @@ export { assert, assertEquals, + assertNotStrictEquals, assertStrictEquals, assertThrows, -} from "https://deno.land/std@0.188.0/testing/asserts.ts"; +} from "https://deno.land/std@0.208.0/testing/asserts.ts"; export { describe, it, -} from "https://deno.land/std@0.188.0/testing/bdd.ts"; +} from "https://deno.land/std@0.208.0/testing/bdd.ts"; export { assertSpyCall, assertSpyCalls, spy, -} from "https://deno.land/std@0.188.0/testing/mock.ts"; +} from "https://deno.land/std@0.208.0/testing/mock.ts"; diff --git a/object.js b/object.js index 87040ac..aa3538d 100644 --- a/object.js +++ b/object.js @@ -99,317 +99,6 @@ export class LazyLoader extends null { } } -/** - * A property descriptor object. - * - * Actually constructing a property descriptor object using this class - * is only necessary if you need strict guarantees about the types of - * its properties; the resulting object is proxied to ensure the types - * match what one would expect from composing FromPropertyDescriptor - * and ToPropertyDescriptor in the Ecmascript specification. - * - * Otherwise, the instance properties and methods are generic. - */ -export const { PropertyDescriptor } = (() => { - class PropertyDescriptor extends null { - /** - * Constructs a new property descriptor object from the provided - * object. - * - * The resulting object is proxied to enforce types (for example, - * its `.enumerable` property, if defined, will always be a - * boolean). - */ - constructor(O) { - if (type(O) !== "object") { - // The provided value is not an object. - throw new TypeError( - `Piscēs: Cannot convert primitive to property descriptor: ${O}.`, - ); - } else { - // The provided value is an object. - const desc = objectCreate(propertyDescriptorPrototype); - if ("enumerable" in O) { - // An enumerable property is specified. - desc.enumerable = !!O.enumerable; - } else { - // An enumerable property is not specified. - /* do nothing */ - } - if ("configurable" in O) { - // A configurable property is specified. - desc.configurable = !!O.configurable; - } else { - // A configurable property is not specified. - /* do nothing */ - } - if ("value" in O) { - // A value property is specified. - desc.value = O.value; - } else { - // A value property is not specified. - /* do nothing */ - } - if ("writable" in O) { - // A writable property is specified. - desc.writable = !!O.writable; - } else { - // A writable property is not specified. - /* do nothing */ - } - if ("get" in O) { - // A get property is specified. - const getter = O.get; - if (getter !== undefined && typeof getter !== "function") { - // The getter is not callable. - throw new TypeError("Piscēs: Getters must be callable."); - } else { - // The getter is callable. - desc.get = getter; - } - } else { - // A get property is not specified. - /* do nothing */ - } - if ("set" in O) { - // A set property is specified. - const setter = O.set; - if (setter !== undefined && typeof setter !== "function") { - // The setter is not callable. - throw new TypeError("Piscēs: Setters must be callable."); - } else { - // The setter is callable. - desc.set = setter; - } - } else { - // A set property is not specified. - /* do nothing */ - } - if ( - ("get" in desc || "set" in desc) && - ("value" in desc || "writable" in desc) - ) { - // Both accessor and data attributes have been defined. - throw new TypeError( - "Piscēs: Property descriptors cannot specify both accessor and data attributes.", - ); - } else { - // The property descriptor is valid. - return new Proxy(desc, propertyDescriptorProxyHandler); - } - } - } - - /** - * Completes this property descriptor by setting missing values to - * their defaults. - * - * This method modifies this object and returns undefined. - */ - complete() { - if (this !== undefined && !("get" in this || "set" in this)) { - // This is a generic or data descriptor. - if (!("value" in this)) { - // `value` is not defined on this. - this.value = undefined; - } else { - // `value` is already defined on this. - /* do nothing */ - } - if (!("writable" in this)) { - // `writable` is not defined on this. - this.writable = false; - } else { - // `writable` is already defined on this. - /* do nothing */ - } - } else { - // This is not a generic or data descriptor. - if (!("get" in this)) { - // `get` is not defined on this. - this.get = undefined; - } else { - // `get` is already defined on this. - /* do nothing */ - } - if (!("set" in this)) { - // `set` is not defined on this. - this.set = undefined; - } else { - // `set` is already defined on this. - /* do nothing */ - } - } - if (!("enumerable" in this)) { - // `enumerable` is not defined on this. - this.enumerable = false; - } else { - // `enumerable` is already defined on this. - /* do nothing */ - } - if (!("configurable" in this)) { - // `configurable` is not defined on this. - this.configurable = false; - } else { - // `configurable` is already defined on this. - /* do nothing */ - } - } - - /** Gets whether this is an accessor descrtiptor. */ - get isAccessorDescriptor() { - return this !== undefined && ("get" in this || "set" in this); - } - - /** Gets whether this is a data descrtiptor. */ - get isDataDescriptor() { - return this !== undefined && - ("value" in this || "writable" in this); - } - - /** Gets whether this is a fully‐populated property descriptor. */ - get isFullyPopulated() { - return this !== undefined && - ("value" in this && "writable" in this || - "get" in this && "set" in this) && - "enumerable" in this && "configurable" in this; - } - - /** - * Gets whether this is a generic (not accessor or data) - * descrtiptor. - */ - get isGenericDescriptor() { - return this !== undefined && - !("get" in this || "set" in this || "value" in this || - "writable" in this); - } - } - - const coercePropertyDescriptorValue = (P, V) => { - switch (P) { - case "configurable": - case "enumerable": - case "writable": - return !!V; - case "value": - return V; - case "get": - if (V !== undefined && typeof V !== "function") { - throw new TypeError( - "Piscēs: Getters must be callable.", - ); - } else { - return V; - } - case "set": - if (V !== undefined && typeof V !== "function") { - throw new TypeError( - "Piscēs: Setters must be callable.", - ); - } else { - return V; - } - default: - return V; - } - }; - - const { - prototype: propertyDescriptorPrototype, - } = PropertyDescriptor; - - const propertyDescriptorProxyHandler = Object.assign( - Object.create(null), - { - defineProperty(O, P, Desc) { - if ( - P === "configurable" || P === "enumerable" || - P === "writable" || P === "value" || - P === "get" || P === "set" - ) { - // P is a property descriptor attribute. - const desc = new PropertyDescriptor(Desc); - if ("get" in desc || "set" in desc) { - // Desc is an accessor property descriptor. - throw new TypeError( - "Piscēs: Property descriptor attributes must be data properties.", - ); - } else if ("value" in desc || !(P in O)) { - // Desc has a value or P does not already exist on O. - desc.value = coercePropertyDescriptorValue(P, desc.value); - } else { - // Desc is not an accessor property descriptor and has no - // value. - /* do nothing */ - } - const isAccessorDescriptor = "get" === P || "set" === P || - "get" in O || "set" in O; - const isDataDescriptor = "value" === P || "writable" === P || - "value" in O || "writable" in O; - if (isAccessorDescriptor && isDataDescriptor) { - // Both accessor and data attributes will be present on O - // after defining P. - throw new TypeError( - "Piscēs: Property descriptors cannot specify both accessor and data attributes.", - ); - } else { - // P can be safely defined on O. - return defineOwnProperty(O, P, desc); - } - } else { - // P is not a property descriptor attribute. - return defineOwnProperty(O, P, Desc); - } - }, - set(O, P, V, Receiver) { - if ( - P === "configurable" || P === "enumerable" || - P === "writable" || P === "value" || - P === "get" || P === "set" - ) { - // P is a property descriptor attribute. - const newValue = coercePropertyDescriptorValue(P, V); - const isAccessorDescriptor = "get" === P || "set" === P || - "get" in O || "set" in O; - const isDataDescriptor = "value" === P || "writable" === P || - "value" in O || "writable" in O; - if (isAccessorDescriptor && isDataDescriptor) { - // Both accessor and data attributes will be present on O - // after defining P. - throw new TypeError( - "Piscēs: Property descriptors cannot specify both accessor and data attributes.", - ); - } else { - // P can be safely defined on O. - // - // ☡ Receiver will be the *proxied* object, so passing it - // through to setPropertyValue here would produce an - // infinite loop. - // - // ☡ This has implications on objects with a proxied - // PropertyDescriptor in their prototype. - return setPropertyValue(O, P, newValue, O); - } - } else { - return setPropertyValue(O, P, V, Receiver); - } - }, - setPrototypeOf(O, V) { - if (V !== propertyDescriptorPrototype) { - // V is not the property descriptor prototype. - return false; - } else { - // V is the property descriptor prototype. - return setPrototype(O, V); - } - }, - }, - ); - - return { PropertyDescriptor }; -})(); - /** * Defines an own property on the provided object on the provided * property key using the provided property descriptor. diff --git a/object.test.js b/object.test.js index a2e718b..38d333f 100644 --- a/object.test.js +++ b/object.test.js @@ -47,7 +47,6 @@ import { objectCreate, objectFromEntries, preventExtensions, - PropertyDescriptor, seal, setPropertyValue, setPropertyValues, @@ -310,579 +309,6 @@ describe("LazyLoader", () => { }); }); -describe("PropertyDescriptor", () => { - it("[[Call]] throws an error", () => { - assertThrows(() => PropertyDescriptor({})); - }); - - it("[[Construct]] creates a new PropertyDescriptor", () => { - assertStrictEquals( - Object.getPrototypeOf(new PropertyDescriptor({})), - PropertyDescriptor.prototype, - ); - }); - - it("[[Construct]] throws for primitives", () => { - assertThrows(() => new PropertyDescriptor("failure")); - }); - - describe(".length", () => { - it("[[Get]] returns the correct length", () => { - assertStrictEquals(PropertyDescriptor.length, 1); - }); - }); - - describe(".name", () => { - it("[[Get]] returns the correct name", () => { - assertStrictEquals( - PropertyDescriptor.name, - "PropertyDescriptor", - ); - }); - }); - - describe("::complete", () => { - it("[[Call]] completes a generic descriptor", () => { - const desc = {}; - PropertyDescriptor.prototype.complete.call(desc); - assertEquals(desc, { - configurable: false, - enumerable: false, - value: undefined, - writable: false, - }); - }); - - it("[[Call]] completes a data descriptor", () => { - const desc = { value: undefined }; - PropertyDescriptor.prototype.complete.call(desc); - assertEquals(desc, { - configurable: false, - enumerable: false, - value: undefined, - writable: false, - }); - }); - - it("[[Call]] completes an accessor descriptor", () => { - const desc = { get: undefined }; - PropertyDescriptor.prototype.complete.call(desc); - assertEquals(desc, { - configurable: false, - enumerable: false, - get: undefined, - set: undefined, - }); - }); - - describe(".length", () => { - it("[[Get]] returns the correct length", () => { - assertStrictEquals( - PropertyDescriptor.prototype.complete.length, - 0, - ); - }); - }); - - describe(".name", () => { - it("[[Get]] returns the correct name", () => { - assertStrictEquals( - PropertyDescriptor.prototype.complete.name, - "complete", - ); - }); - }); - }); - - describe("::isAccessorDescriptor", () => { - it("[[Get]] returns false for a generic descriptor", () => { - assertStrictEquals( - Reflect.get( - PropertyDescriptor.prototype, - "isAccessorDescriptor", - {}, - ), - false, - ); - }); - - it("[[Get]] returns false for a data descriptor", () => { - assertStrictEquals( - Reflect.get( - PropertyDescriptor.prototype, - "isAccessorDescriptor", - { value: undefined }, - ), - false, - ); - }); - - it("[[Get]] returns true for an accessor descriptor", () => { - assertStrictEquals( - Reflect.get( - PropertyDescriptor.prototype, - "isAccessorDescriptor", - { get: undefined }, - ), - true, - ); - }); - - describe("[[GetOwnProperty]].get.length", () => { - it("[[Get]] returns the correct length", () => { - assertStrictEquals( - Object.getOwnPropertyDescriptor( - PropertyDescriptor.prototype, - "isAccessorDescriptor", - ).get.length, - 0, - ); - }); - }); - - describe("[[GetOwnProperty]].get.name", () => { - it("[[Get]] returns the correct name", () => { - assertStrictEquals( - Object.getOwnPropertyDescriptor( - PropertyDescriptor.prototype, - "isAccessorDescriptor", - ).get.name, - "get isAccessorDescriptor", - ); - }); - }); - }); - - describe("::isDataDescriptor", () => { - it("[[Get]] returns false for a generic descriptor", () => { - assertStrictEquals( - Reflect.get( - PropertyDescriptor.prototype, - "isDataDescriptor", - {}, - ), - false, - ); - }); - - it("[[Get]] returns true for a data descriptor", () => { - assertStrictEquals( - Reflect.get( - PropertyDescriptor.prototype, - "isDataDescriptor", - { value: undefined }, - ), - true, - ); - }); - - it("[[Get]] returns false for an accessor descriptor", () => { - assertStrictEquals( - Reflect.get( - PropertyDescriptor.prototype, - "isDataDescriptor", - { get: undefined }, - ), - false, - ); - }); - - describe("[[GetOwnProperty]].get.length", () => { - it("[[Get]] returns the correct length", () => { - assertStrictEquals( - Object.getOwnPropertyDescriptor( - PropertyDescriptor.prototype, - "isDataDescriptor", - ).get.length, - 0, - ); - }); - }); - - describe("[[GetOwnProperty]].get.name", () => { - it("[[Get]] returns the correct name", () => { - assertStrictEquals( - Object.getOwnPropertyDescriptor( - PropertyDescriptor.prototype, - "isDataDescriptor", - ).get.name, - "get isDataDescriptor", - ); - }); - }); - }); - - describe("::isFullyPopulated", () => { - it("[[Get]] returns false for a generic descriptor", () => { - assertStrictEquals( - Reflect.get( - PropertyDescriptor.prototype, - "isFullyPopulated", - {}, - ), - false, - ); - }); - - it("[[Get]] returns false for a non‐fully‐populated data descriptor", () => { - assertStrictEquals( - Reflect.get( - PropertyDescriptor.prototype, - "isFullyPopulated", - { value: undefined }, - ), - false, - ); - }); - - it("[[Get]] returns true for a fully‐populated data descriptor", () => { - assertStrictEquals( - Reflect.get(PropertyDescriptor.prototype, "isFullyPopulated", { - configurable: true, - enumerable: true, - value: undefined, - writable: true, - }), - true, - ); - }); - - it("[[Get]] returns false for a non‐fully‐populated accessor descriptor", () => { - assertStrictEquals( - Reflect.get( - PropertyDescriptor.prototype, - "isFullyPopulated", - { get: undefined }, - ), - false, - ); - }); - - it("[[Get]] returns true for a fully‐populated accessor descriptor", () => { - assertStrictEquals( - Reflect.get(PropertyDescriptor.prototype, "isFullyPopulated", { - configurable: true, - enumerable: true, - get: undefined, - set: undefined, - }), - true, - ); - }); - - describe("[[GetOwnProperty]].get.length", () => { - it("[[Get]] returns the correct length", () => { - assertStrictEquals( - Object.getOwnPropertyDescriptor( - PropertyDescriptor.prototype, - "isFullyPopulated", - ).get.length, - 0, - ); - }); - }); - - describe("[[GetOwnProperty]].get.name", () => { - it("[[Get]] returns the correct name", () => { - assertStrictEquals( - Object.getOwnPropertyDescriptor( - PropertyDescriptor.prototype, - "isFullyPopulated", - ).get.name, - "get isFullyPopulated", - ); - }); - }); - }); - - describe("::isGenericDescriptor", () => { - it("[[Get]] returns true for a generic descriptor", () => { - assertStrictEquals( - Reflect.get( - PropertyDescriptor.prototype, - "isGenericDescriptor", - {}, - ), - true, - ); - }); - - it("[[Get]] returns true for a data descriptor", () => { - assertStrictEquals( - Reflect.get( - PropertyDescriptor.prototype, - "isGenericDescriptor", - { value: undefined }, - ), - false, - ); - }); - - it("[[Get]] returns false for an accessor descriptor", () => { - assertStrictEquals( - Reflect.get( - PropertyDescriptor.prototype, - "isGenericDescriptor", - { get: undefined }, - ), - false, - ); - }); - - describe("[[GetOwnProperty]].get.length", () => { - it("[[Get]] returns the correct length", () => { - assertStrictEquals( - Object.getOwnPropertyDescriptor( - PropertyDescriptor.prototype, - "isGenericDescriptor", - ).get.length, - 0, - ); - }); - }); - - describe("[[GetOwnProperty]].get.name", () => { - it("[[Get]] returns the correct name", () => { - assertStrictEquals( - Object.getOwnPropertyDescriptor( - PropertyDescriptor.prototype, - "isGenericDescriptor", - ).get.name, - "get isGenericDescriptor", - ); - }); - }); - }); - - describe("~configurable", () => { - it("[[DefineOwnProperty]] coerces to a boolean", () => { - const desc = new PropertyDescriptor({}); - Object.defineProperty(desc, "configurable", {}); - assertStrictEquals(desc.configurable, false); - }); - - it("[[DefineOwnProperty]] throws for accessor properties", () => { - const desc = new PropertyDescriptor({}); - assertThrows(() => - Object.defineProperty(desc, "configurable", { get: undefined }) - ); - }); - - it("[[Set]] coerces to a boolean", () => { - const desc = new PropertyDescriptor({}); - desc.configurable = undefined; - assertStrictEquals(desc.configurable, false); - }); - - it("[[Delete]] works", () => { - const desc = new PropertyDescriptor({ configurable: false }); - delete desc.configurable; - assert(!("configurable" in desc)); - }); - }); - - describe("~enumerable", () => { - it("[[DefineOwnProperty]] coerces to a boolean", () => { - const desc = new PropertyDescriptor({}); - Object.defineProperty(desc, "enumerable", {}); - assertStrictEquals(desc.enumerable, false); - }); - - it("[[DefineOwnProperty]] throws for accessor properties", () => { - const desc = new PropertyDescriptor({}); - assertThrows(() => - Object.defineProperty(desc, "enumerable", { get: undefined }) - ); - }); - - it("[[Set]] coerces to a boolean", () => { - const desc = new PropertyDescriptor({}); - desc.enumerable = undefined; - assertStrictEquals(desc.enumerable, false); - }); - - it("[[Delete]] works", () => { - const desc = new PropertyDescriptor({ enumerable: false }); - delete desc.enumerable; - assert(!("enumerable" in desc)); - }); - }); - - describe("~get", () => { - it("[[DefineOwnProperty]] works", () => { - const desc = new PropertyDescriptor({}); - Object.defineProperty(desc, "get", {}); - assertStrictEquals(desc.get, undefined); - }); - - it("[[DefineOwnProperty]] throws for accessor properties", () => { - const desc = new PropertyDescriptor({}); - assertThrows(() => - Object.defineProperty(desc, "get", { get: undefined }) - ); - }); - - it("[[DefineOwnProperty]] throws if not callable or undefined", () => { - const desc = new PropertyDescriptor({}); - assertThrows( - () => Object.defineProperty(desc, "get", { value: null }), - ); - }); - - it("[[DefineOwnProperty]] throws if a data property is defined", () => { - const desc = new PropertyDescriptor({ value: undefined }); - assertThrows(() => Object.defineProperty(desc, "get", {})); - }); - - it("[[Set]] works", () => { - const desc = new PropertyDescriptor({}); - const fn = () => {}; - desc.get = fn; - assertStrictEquals(desc.get, fn); - }); - - it("[[Set]] throws if not callable or undefined", () => { - const desc = new PropertyDescriptor({}); - assertThrows(() => desc.get = null); - }); - - it("[[Set]] throws if a data property is defined", () => { - const desc = new PropertyDescriptor({ value: undefined }); - assertThrows(() => desc.get = undefined); - }); - - it("[[Delete]] works", () => { - const desc = new PropertyDescriptor({ get: undefined }); - delete desc.get; - assert(!("get" in desc)); - }); - }); - - describe("~set", () => { - it("[[DefineOwnProperty]] works", () => { - const desc = new PropertyDescriptor({}); - Object.defineProperty(desc, "set", {}); - assertStrictEquals(desc.set, undefined); - }); - - it("[[DefineOwnProperty]] throws for accessor properties", () => { - const desc = new PropertyDescriptor({}); - assertThrows(() => - Object.defineProperty(desc, "set", { get: undefined }) - ); - }); - - it("[[DefineOwnProperty]] throws if not callable or undefined", () => { - const desc = new PropertyDescriptor({}); - assertThrows( - () => Object.defineProperty(desc, "set", { value: null }), - ); - }); - - it("[[DefineOwnProperty]] throws if a data property is defined", () => { - const desc = new PropertyDescriptor({ value: undefined }); - assertThrows(() => Object.defineProperty(desc, "set", {})); - }); - - it("[[Set]] works", () => { - const desc = new PropertyDescriptor({}); - const fn = (_) => {}; - desc.set = fn; - assertStrictEquals(desc.set, fn); - }); - - it("[[Set]] throws if not callable or undefined", () => { - const desc = new PropertyDescriptor({}); - assertThrows(() => desc.set = null); - }); - - it("[[Set]] throws if a data property is defined", () => { - const desc = new PropertyDescriptor({ value: undefined }); - assertThrows(() => desc.set = undefined); - }); - - it("[[Delete]] works", () => { - const desc = new PropertyDescriptor({ set: undefined }); - delete desc.set; - assert(!("set" in desc)); - }); - }); - - describe("~value", () => { - it("[[DefineOwnProperty]] works", () => { - const desc = new PropertyDescriptor({}); - Object.defineProperty(desc, "value", {}); - assertStrictEquals(desc.value, undefined); - }); - - it("[[DefineOwnProperty]] throws for accessor properties", () => { - const desc = new PropertyDescriptor({}); - assertThrows(() => - Object.defineProperty(desc, "value", { get: undefined }) - ); - }); - - it("[[DefineOwnProperty]] throws if an accessor property is defined", () => { - const desc = new PropertyDescriptor({ get: undefined }); - assertThrows(() => Object.defineProperty(desc, "value", {})); - }); - - it("[[Set]] works", () => { - const desc = new PropertyDescriptor({}); - desc.value = "success"; - assertStrictEquals(desc.value, "success"); - }); - - it("[[Set]] throws if an accessor property is defined", () => { - const desc = new PropertyDescriptor({ get: undefined }); - assertThrows(() => desc.value = null); - }); - - it("[[Delete]] works", () => { - const desc = new PropertyDescriptor({ value: undefined }); - delete desc.value; - assert(!("value" in desc)); - }); - }); - - describe("~writable", () => { - it("[[DefineOwnProperty]] coerces to a boolean", () => { - const desc = new PropertyDescriptor({}); - Object.defineProperty(desc, "writable", {}); - assertStrictEquals(desc.writable, false); - }); - - it("[[DefineOwnProperty]] throws for accessor properties", () => { - const desc = new PropertyDescriptor({}); - assertThrows(() => - Object.defineProperty(desc, "writable", { get: undefined }) - ); - }); - - it("[[DefineOwnProperty]] throws if an accessor property is defined", () => { - const desc = new PropertyDescriptor({ get: undefined }); - assertThrows(() => Object.defineProperty(desc, "writable", {})); - }); - - it("[[Set]] coerces to a boolean", () => { - const desc = new PropertyDescriptor({}); - desc.writable = undefined; - assertStrictEquals(desc.writable, false); - }); - - it("[[Set]] throws if an accessor property is defined", () => { - const desc = new PropertyDescriptor({ get: undefined }); - assertThrows(() => desc.writable = false); - }); - - it("[[Delete]] works", () => { - const desc = new PropertyDescriptor({ writable: false }); - delete desc.writable; - assert(!("writable" in desc)); - }); - }); -}); - describe("defineOwnProperty", () => { it("[[Call]] defines the property", () => { const obj = {}; diff --git a/value.js b/value.js index 6471ea3..1039a3c 100644 --- a/value.js +++ b/value.js @@ -173,6 +173,343 @@ export const POSITIVE_ZERO = 0; /** The undefined primitive. */ export const UNDEFINED = undefined; +/** + * Completes the provided property descriptor by setting missing values + * to their defaults. + * + * ※ This method modifies the provided object and returns undefined. + */ +export const completePropertyDescriptor = (Desc) => { + if (Desc === UNDEFINED) { + throw new TypeError( + "Piscēs: Cannot complete undefined property descriptor.", + ); + } else if (!("get" in Desc || "set" in Desc)) { + // This is a generic or data descriptor. + if (!("value" in Desc)) { + // `value` is not defined on this. + Desc.value = UNDEFINED; + } else { + // `value` is already defined on this. + /* do nothing */ + } + if (!("writable" in Desc)) { + // `writable` is not defined on this. + Desc.writable = false; + } else { + // `writable` is already defined on this. + /* do nothing */ + } + } else { + // This is not a generic or data descriptor. + if (!("get" in Desc)) { + // `get` is not defined on this. + Desc.get = UNDEFINED; + } else { + // `get` is already defined on this. + /* do nothing */ + } + if (!("set" in Desc)) { + // `set` is not defined on this. + Desc.set = UNDEFINED; + } else { + // `set` is already defined on this. + /* do nothing */ + } + } + if (!("enumerable" in Desc)) { + // `enumerable` is not defined on this. + Desc.enumerable = false; + } else { + // `enumerable` is already defined on this. + /* do nothing */ + } + if (!("configurable" in Desc)) { + // `configurable` is not defined on this. + Desc.configurable = false; + } else { + // `configurable` is already defined on this. + /* do nothing */ + } +}; + +/** Gets whether the provided value is an accessor descrtiptor. */ +export const isAccessorDescriptor = (Desc) => + Desc !== UNDEFINED && ("get" in Desc || "set" in Desc); + +/** Gets whether the provided value is a data descrtiptor. */ +export const isDataDescriptor = (Desc) => + Desc !== UNDEFINED && ("value" in Desc || "writable" in Desc); + +/** + * Gets whether the provided value is a fully‐populated property + * descriptor. + */ +export const isFullyPopulatedDescriptor = (Desc) => + Desc !== UNDEFINED && + ("value" in Desc && "writable" in Desc || + "get" in Desc && "set" in Desc) && + "enumerable" in Desc && "configurable" in Desc; + +/** + * Gets whether the provided value is a generic (not accessor or data) + * descrtiptor. + */ +export const isGenericDescriptor = (Desc) => + Desc !== UNDEFINED && + !("get" in Desc || "set" in Desc || "value" in Desc || + "writable" in Desc); + +export const { + /** + * Returns whether the provided value is a property descriptor record + * as created by `toPropertyDescriptor`. + * + * ※ This function is provided to enable inspection of whether an + * object uses the property descriptor record proxy implementation, + * not as a general test of whether an object satisfies the + * requirements for property descriptors. In most cases, a more + * specific test, like `isAccessorDescriptor`, `isDataDescriptor`, or + * `isGenericDescriptor`, is preferrable. + */ + isPropertyDescriptorRecord, + + /** + * Converts the provided value to a property descriptor record. + * + * ※ The prototype of a property descriptor record is always `null`. + * + * ※ Actually constructing a property descriptor object using this + * class is only necessary if you need strict guarantees about the + * types of its properties; the resulting object is proxied to ensure + * the types match what one would expect from composing + * FromPropertyDescriptor and ToPropertyDescriptor in the Ecmascript + * specification. + */ + toPropertyDescriptor, +} = (() => { + const { + assign: setPropertyValues, + create: objectCreate, + defineProperty: defineOwnProperty, + } = Object; + const { + apply: call, + defineProperty: reflectDefineProperty, + setPrototypeOf: reflectSetPrototypeOf, + } = Reflect; + const proxyConstructor = Proxy; + const { add: weakSetAdd, has: weakSetHas } = WeakSet.prototype; + const propertyDescriptorRecords = new WeakSet(); + const coercePropertyDescriptorValue = (P, V) => { + switch (P) { + case "configurable": + case "enumerable": + case "writable": + return !!V; + case "value": + return V; + case "get": + if (V !== undefined && typeof V !== "function") { + throw new TypeError( + "Piscēs: Getters must be callable.", + ); + } else { + return V; + } + case "set": + if (V !== undefined && typeof V !== "function") { + throw new TypeError( + "Piscēs: Setters must be callable.", + ); + } else { + return V; + } + default: + return V; + } + }; + const propertyDescriptorProxyHandler = Object.freeze( + Object.assign( + Object.create(null), + { + defineProperty(O, P, Desc) { + if ( + P === "configurable" || P === "enumerable" || + P === "writable" || P === "value" || + P === "get" || P === "set" + ) { + // P is a property descriptor attribute. + const desc = setPropertyValues(objectCreate(null), Desc); + if ("get" in desc || "set" in desc) { + // Desc is an accessor property descriptor. + throw new TypeError( + "Piscēs: Property descriptor attributes must be data properties.", + ); + } else if ("value" in desc || !(P in O)) { + // Desc has a value or P does not already exist on O. + desc.value = coercePropertyDescriptorValue( + P, + desc.value, + ); + } else { + // Desc is not an accessor property descriptor and has no + // value, but an existing value is present on O. + /* do nothing */ + } + const isAccessorDescriptor = "get" === P || "set" === P || + "get" in O || "set" in O; + const isDataDescriptor = "value" === P || + "writable" === P || + "value" in O || "writable" in O; + if (isAccessorDescriptor && isDataDescriptor) { + // Both accessor and data attributes will be present on O + // after defining P. + throw new TypeError( + "Piscēs: Property descriptors cannot specify both accessor and data attributes.", + ); + } else { + // P can be safely defined on O. + return reflectDefineProperty(O, P, desc); + } + } else { + // P is not a property descriptor attribute. + return reflectDefineProperty(O, P, Desc); + } + }, + setPrototypeOf(O, V) { + if (V !== null) { + // V is not the property descriptor prototype. + return false; + } else { + // V is the property descriptor prototype. + return reflectSetPrototypeOf(O, V); + } + }, + }, + ), + ); + + return { + isPropertyDescriptorRecord: ($) => + call(weakSetHas, propertyDescriptorRecords, [$]), + toPropertyDescriptor: (Obj) => { + if (type(Obj) !== "object") { + // The provided value is not an object. + throw new TypeError( + `Piscēs: Cannot convert primitive to property descriptor: ${O}.`, + ); + } else { + // The provided value is an object. + const desc = objectCreate(null); + if ("enumerable" in Obj) { + // An enumerable property is specified. + defineOwnProperty(desc, "enumerable", { + configurable: true, + enumerable: true, + value: !!Obj.enumerable, + writable: true, + }); + } else { + // An enumerable property is not specified. + /* do nothing */ + } + if ("configurable" in Obj) { + // A configurable property is specified. + defineOwnProperty(desc, "configurable", { + configurable: true, + enumerable: true, + value: !!Obj.configurable, + writable: true, + }); + } else { + // A configurable property is not specified. + /* do nothing */ + } + if ("value" in Obj) { + // A value property is specified. + defineOwnProperty(desc, "value", { + configurable: true, + enumerable: true, + value: Obj.value, + writable: true, + }); + } else { + // A value property is not specified. + /* do nothing */ + } + if ("writable" in Obj) { + // A writable property is specified. + defineOwnProperty(desc, "writable", { + configurable: true, + enumerable: true, + value: !!Obj.writable, + writable: true, + }); + } else { + // A writable property is not specified. + /* do nothing */ + } + if ("get" in Obj) { + // A get property is specified. + const getter = Obj.get; + if (getter !== UNDEFINED && typeof getter !== "function") { + // The getter is not callable. + throw new TypeError("Piscēs: Getters must be callable."); + } else { + // The getter is callable. + defineOwnProperty(desc, "get", { + configurable: true, + enumerable: true, + value: getter, + writable: true, + }); + } + } else { + // A get property is not specified. + /* do nothing */ + } + if ("set" in Obj) { + // A set property is specified. + const setter = Obj.set; + if (setter !== UNDEFINED && typeof setter !== "function") { + // The setter is not callable. + throw new TypeError("Piscēs: Setters must be callable."); + } else { + // The setter is callable. + defineOwnProperty(desc, "set", { + configurable: true, + enumerable: true, + value: setter, + writable: true, + }); + } + } else { + // A set property is not specified. + /* do nothing */ + } + if ( + ("get" in desc || "set" in desc) && + ("value" in desc || "writable" in desc) + ) { + // Both accessor and data attributes have been defined. + throw new TypeError( + "Piscēs: Property descriptors cannot specify both accessor and data attributes.", + ); + } else { + // The property descriptor is valid. + const record = new proxyConstructor( + desc, + propertyDescriptorProxyHandler, + ); + call(weakSetAdd, propertyDescriptorRecords, [record]); + return record; + } + } + }, + }; +})(); + export const { /** * Returns the primitive value of the provided object per its diff --git a/value.test.js b/value.test.js index 9121af4..0f95f22 100644 --- a/value.test.js +++ b/value.test.js @@ -8,6 +8,9 @@ // file, You can obtain one at . import { + assert, + assertEquals, + assertNotStrictEquals, assertStrictEquals, assertThrows, describe, @@ -15,8 +18,14 @@ import { } from "./dev-deps.js"; import { ASYNC_ITERATOR, + completePropertyDescriptor, HAS_INSTANCE, IS_CONCAT_SPREADABLE, + isAccessorDescriptor, + isDataDescriptor, + isFullyPopulatedDescriptor, + isGenericDescriptor, + isPropertyDescriptorRecord, ITERATOR, LN10, LN2, @@ -48,6 +57,7 @@ import { toIndex, toLength, toPrimitive, + toPropertyDescriptor, type, UNDEFINED, UNSCOPABLES, @@ -239,21 +249,298 @@ describe("UNSCOPABLES", () => { }); }); -describe("Ε", () => { - it("[[Get]] is ε", () => { - assertStrictEquals(Ε, Number.EPSILON); +describe("completePropertyDescriptor", () => { + it("[[Call]] completes a generic descriptor", () => { + const desc = {}; + completePropertyDescriptor(desc); + assertEquals(desc, { + configurable: false, + enumerable: false, + value: undefined, + writable: false, + }); + }); + + it("[[Call]] completes a data descriptor", () => { + const desc = { value: undefined }; + completePropertyDescriptor(desc); + assertEquals(desc, { + configurable: false, + enumerable: false, + value: undefined, + writable: false, + }); + }); + + it("[[Call]] completes an accessor descriptor", () => { + const desc = { get: undefined }; + completePropertyDescriptor(desc); + assertEquals(desc, { + configurable: false, + enumerable: false, + get: undefined, + set: undefined, + }); + }); + + it("[[Call]] throws an error when the descriptor is undefined", () => { + assertThrows(() => new completePropertyDescriptor(undefined)); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new completePropertyDescriptor({})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(completePropertyDescriptor.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + completePropertyDescriptor.name, + "completePropertyDescriptor", + ); + }); }); }); -describe("Π", () => { - it("[[Get]] is π", () => { - assertStrictEquals(Π, Math.PI); +describe("isAccessorDescriptor", () => { + it("[[Call]] returns false for a generic descriptor", () => { + assertStrictEquals(isAccessorDescriptor({}), false); + }); + + it("[[Get]] returns false for a data descriptor", () => { + assertStrictEquals( + isAccessorDescriptor({ value: undefined }), + false, + ); + assertStrictEquals( + isAccessorDescriptor({ writable: undefined }), + false, + ); + }); + + it("[[Get]] returns true for an accessor descriptor", () => { + assertStrictEquals(isAccessorDescriptor({ get: undefined }), true); + assertStrictEquals(isAccessorDescriptor({ set: undefined }), true); + }); + + it("[[Get]] returns false for undefined", () => { + assertStrictEquals(isAccessorDescriptor(undefined), false); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new isAccessorDescriptor({})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(isAccessorDescriptor.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + isAccessorDescriptor.name, + "isAccessorDescriptor", + ); + }); }); }); -describe("ℇ", () => { - it("[[Get]] is ℇ", () => { - assertStrictEquals(ℇ, Math.E); +describe("isDataDescriptor", () => { + it("[[Call]] returns false for a generic descriptor", () => { + assertStrictEquals(isDataDescriptor({}), false); + }); + + it("[[Get]] returns true for a data descriptor", () => { + assertStrictEquals(isDataDescriptor({ value: undefined }), true); + assertStrictEquals(isDataDescriptor({ writable: true }), true); + }); + + it("[[Get]] returns false for an accessor descriptor", () => { + assertStrictEquals(isDataDescriptor({ get: undefined }), false); + assertStrictEquals(isDataDescriptor({ set: undefined }), false); + }); + + it("[[Get]] returns false for undefined", () => { + assertStrictEquals(isDataDescriptor(undefined), false); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new isDataDescriptor({})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(isDataDescriptor.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals(isDataDescriptor.name, "isDataDescriptor"); + }); + }); +}); + +describe("isFullyPopulatedDescriptor", () => { + it("[[Call]] returns false for a generic descriptor", () => { + assertStrictEquals(isFullyPopulatedDescriptor({}), false); + }); + + it("[[Get]] returns false for a non‐fully‐populated data descriptor", () => { + assertStrictEquals( + isFullyPopulatedDescriptor({ value: undefined }), + false, + ); + assertStrictEquals( + isFullyPopulatedDescriptor({ writable: true }), + false, + ); + }); + + it("[[Get]] returns true for a fully‐populated data descriptor", () => { + assertStrictEquals( + isFullyPopulatedDescriptor({ + configurable: true, + enumerable: true, + value: undefined, + writable: true, + }), + true, + ); + }); + + it("[[Get]] returns false for a non‐fully‐populated accessor descriptor", () => { + assertStrictEquals( + isFullyPopulatedDescriptor({ get: undefined }), + false, + ); + assertStrictEquals( + isFullyPopulatedDescriptor({ set: undefined }), + false, + ); + }); + + it("[[Get]] returns true for a fully‐populated accessor descriptor", () => { + assertStrictEquals( + isFullyPopulatedDescriptor({ + configurable: true, + enumerable: true, + get: undefined, + set: undefined, + }), + true, + ); + }); + + it("[[Get]] returns false for undefined", () => { + assertStrictEquals(isFullyPopulatedDescriptor(undefined), false); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new isFullyPopulatedDescriptor({})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(isFullyPopulatedDescriptor.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + isFullyPopulatedDescriptor.name, + "isFullyPopulatedDescriptor", + ); + }); + }); +}); + +describe("isGenericDescriptor", () => { + it("[[Call]] returns true for a generic descriptor", () => { + assertStrictEquals(isGenericDescriptor({}), true); + }); + + it("[[Get]] returns false for a data descriptor", () => { + assertStrictEquals( + isGenericDescriptor({ value: undefined }), + false, + ); + assertStrictEquals(isGenericDescriptor({ writable: true }), false); + }); + + it("[[Get]] returns false for an accessor descriptor", () => { + assertStrictEquals(isGenericDescriptor({ get: undefined }), false); + assertStrictEquals(isGenericDescriptor({ set: undefined }), false); + }); + + it("[[Get]] returns false for undefined", () => { + assertStrictEquals(isGenericDescriptor(undefined), false); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new isGenericDescriptor({})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(isGenericDescriptor.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + isGenericDescriptor.name, + "isGenericDescriptor", + ); + }); + }); +}); + +describe("isPropertyDescriptorRecord", () => { + it("[[Call]] returns true for objects created by toPropertyDescriptor", () => { + assertStrictEquals( + isPropertyDescriptorRecord(toPropertyDescriptor({})), + true, + ); + }); + + it("[[Get]] returns false for other objects", () => { + assertStrictEquals( + isPropertyDescriptorRecord(Object.create(null)), + false, + ); + }); + + it("[[Get]] returns false for undefined", () => { + assertStrictEquals(isPropertyDescriptorRecord(undefined), false); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new isPropertyDescriptorRecord({})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(isPropertyDescriptorRecord.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + isPropertyDescriptorRecord.name, + "isPropertyDescriptorRecord", + ); + }); }); }); @@ -747,6 +1034,278 @@ describe("toPrimitive", () => { }); }); +describe("toPropertyDescriptor", () => { + it("[[Call]] creates a new property descriptor record", () => { + const obj = {}; + const desc = toPropertyDescriptor(obj); + assertEquals(obj, desc); + assertNotStrictEquals(obj, desc); + }); + + it("[[Call]] coerces the values", () => { + assertEquals( + toPropertyDescriptor({ + configurable: undefined, + enumerable: undefined, + writable: undefined, + }), + { configurable: false, enumerable: false, writable: false }, + ); + }); + + it("[[Construct]] throws for primitives", () => { + assertThrows(() => toPropertyDescriptor(undefined)); + assertThrows(() => toPropertyDescriptor("failure")); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new toPropertyDescriptor({})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(toPropertyDescriptor.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + toPropertyDescriptor.name, + "toPropertyDescriptor", + ); + }); + }); + + describe("~configurable", () => { + it("[[DefineOwnProperty]] coerces to a boolean", () => { + const desc = toPropertyDescriptor({}); + Object.defineProperty(desc, "configurable", {}); + assertStrictEquals(desc.configurable, false); + }); + + it("[[DefineOwnProperty]] throws for accessor properties", () => { + const desc = toPropertyDescriptor({}); + assertThrows(() => + Object.defineProperty(desc, "configurable", { get: undefined }) + ); + }); + + it("[[Set]] coerces to a boolean", () => { + const desc = toPropertyDescriptor({}); + desc.configurable = undefined; + assertStrictEquals(desc.configurable, false); + }); + + it("[[Delete]] works", () => { + const desc = toPropertyDescriptor({ configurable: false }); + delete desc.configurable; + assert(!("configurable" in desc)); + }); + }); + + describe("~enumerable", () => { + it("[[DefineOwnProperty]] coerces to a boolean", () => { + const desc = toPropertyDescriptor({}); + Object.defineProperty(desc, "enumerable", {}); + assertStrictEquals(desc.enumerable, false); + }); + + it("[[DefineOwnProperty]] throws for accessor properties", () => { + const desc = toPropertyDescriptor({}); + assertThrows(() => + Object.defineProperty(desc, "enumerable", { get: undefined }) + ); + }); + + it("[[Set]] coerces to a boolean", () => { + const desc = toPropertyDescriptor({}); + desc.enumerable = undefined; + assertStrictEquals(desc.enumerable, false); + }); + + it("[[Delete]] works", () => { + const desc = toPropertyDescriptor({ enumerable: false }); + delete desc.enumerable; + assert(!("enumerable" in desc)); + }); + }); + + describe("~get", () => { + it("[[DefineOwnProperty]] works", () => { + const desc = toPropertyDescriptor({}); + Object.defineProperty(desc, "get", {}); + assertStrictEquals(desc.get, undefined); + }); + + it("[[DefineOwnProperty]] throws for accessor properties", () => { + const desc = toPropertyDescriptor({}); + assertThrows(() => + Object.defineProperty(desc, "get", { get: undefined }) + ); + }); + + it("[[DefineOwnProperty]] throws if not callable or undefined", () => { + const desc = toPropertyDescriptor({}); + assertThrows( + () => Object.defineProperty(desc, "get", { value: null }), + ); + }); + + it("[[DefineOwnProperty]] throws if a data property is defined", () => { + const desc = toPropertyDescriptor({ value: undefined }); + assertThrows(() => Object.defineProperty(desc, "get", {})); + }); + + it("[[Set]] works", () => { + const desc = toPropertyDescriptor({}); + const fn = () => {}; + desc.get = fn; + assertStrictEquals(desc.get, fn); + }); + + it("[[Set]] throws if not callable or undefined", () => { + const desc = toPropertyDescriptor({}); + assertThrows(() => desc.get = null); + }); + + it("[[Set]] throws if a data property is defined", () => { + const desc = toPropertyDescriptor({ value: undefined }); + assertThrows(() => desc.get = undefined); + }); + + it("[[Delete]] works", () => { + const desc = toPropertyDescriptor({ get: undefined }); + delete desc.get; + assert(!("get" in desc)); + }); + }); + + describe("~set", () => { + it("[[DefineOwnProperty]] works", () => { + const desc = toPropertyDescriptor({}); + Object.defineProperty(desc, "set", {}); + assertStrictEquals(desc.set, undefined); + }); + + it("[[DefineOwnProperty]] throws for accessor properties", () => { + const desc = toPropertyDescriptor({}); + assertThrows(() => + Object.defineProperty(desc, "set", { get: undefined }) + ); + }); + + it("[[DefineOwnProperty]] throws if not callable or undefined", () => { + const desc = toPropertyDescriptor({}); + assertThrows( + () => Object.defineProperty(desc, "set", { value: null }), + ); + }); + + it("[[DefineOwnProperty]] throws if a data property is defined", () => { + const desc = toPropertyDescriptor({ value: undefined }); + assertThrows(() => Object.defineProperty(desc, "set", {})); + }); + + it("[[Set]] works", () => { + const desc = toPropertyDescriptor({}); + const fn = (_) => {}; + desc.set = fn; + assertStrictEquals(desc.set, fn); + }); + + it("[[Set]] throws if not callable or undefined", () => { + const desc = toPropertyDescriptor({}); + assertThrows(() => desc.set = null); + }); + + it("[[Set]] throws if a data property is defined", () => { + const desc = toPropertyDescriptor({ value: undefined }); + assertThrows(() => desc.set = undefined); + }); + + it("[[Delete]] works", () => { + const desc = toPropertyDescriptor({ set: undefined }); + delete desc.set; + assert(!("set" in desc)); + }); + }); + + describe("~value", () => { + it("[[DefineOwnProperty]] works", () => { + const desc = toPropertyDescriptor({}); + Object.defineProperty(desc, "value", {}); + assertStrictEquals(desc.value, undefined); + }); + + it("[[DefineOwnProperty]] throws for accessor properties", () => { + const desc = toPropertyDescriptor({}); + assertThrows(() => + Object.defineProperty(desc, "value", { get: undefined }) + ); + }); + + it("[[DefineOwnProperty]] throws if an accessor property is defined", () => { + const desc = toPropertyDescriptor({ get: undefined }); + assertThrows(() => Object.defineProperty(desc, "value", {})); + }); + + it("[[Set]] works", () => { + const desc = toPropertyDescriptor({}); + desc.value = "success"; + assertStrictEquals(desc.value, "success"); + }); + + it("[[Set]] throws if an accessor property is defined", () => { + const desc = toPropertyDescriptor({ get: undefined }); + assertThrows(() => desc.value = null); + }); + + it("[[Delete]] works", () => { + const desc = toPropertyDescriptor({ value: undefined }); + delete desc.value; + assert(!("value" in desc)); + }); + }); + + describe("~writable", () => { + it("[[DefineOwnProperty]] coerces to a boolean", () => { + const desc = toPropertyDescriptor({}); + Object.defineProperty(desc, "writable", {}); + assertStrictEquals(desc.writable, false); + }); + + it("[[DefineOwnProperty]] throws for accessor properties", () => { + const desc = toPropertyDescriptor({}); + assertThrows(() => + Object.defineProperty(desc, "writable", { get: undefined }) + ); + }); + + it("[[DefineOwnProperty]] throws if an accessor property is defined", () => { + const desc = toPropertyDescriptor({ get: undefined }); + assertThrows(() => Object.defineProperty(desc, "writable", {})); + }); + + it("[[Set]] coerces to a boolean", () => { + const desc = toPropertyDescriptor({}); + desc.writable = undefined; + assertStrictEquals(desc.writable, false); + }); + + it("[[Set]] throws if an accessor property is defined", () => { + const desc = toPropertyDescriptor({ get: undefined }); + assertThrows(() => desc.writable = false); + }); + + it("[[Delete]] works", () => { + const desc = toPropertyDescriptor({ writable: false }); + delete desc.writable; + assert(!("writable" in desc)); + }); + }); +}); + describe("type", () => { it('[[Call]] returns "null" for null', () => { assertStrictEquals(type(null), "null"); @@ -784,3 +1343,21 @@ describe("type", () => { }); }); }); + +describe("Ε", () => { + it("[[Get]] is ε", () => { + assertStrictEquals(Ε, Number.EPSILON); + }); +}); + +describe("Π", () => { + it("[[Get]] is π", () => { + assertStrictEquals(Π, Math.PI); + }); +}); + +describe("ℇ", () => { + it("[[Get]] is ℇ", () => { + assertStrictEquals(ℇ, Math.E); + }); +});