From: Lady Date: Sat, 30 Apr 2022 01:18:02 +0000 (-0700) Subject: PropertyDescriptor and frozenCopy X-Git-Tag: 0.1.0~13 X-Git-Url: https://git.ladys.computer/Pisces/commitdiff_plain/32e1e0ba06627f2ed6c3f99c415ef6b9c0f7de4d PropertyDescriptor and frozenCopy --- diff --git a/object.js b/object.js index d08f43a..8f5c490 100644 --- a/object.js +++ b/object.js @@ -7,6 +7,369 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at . +/** + * 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). + */ + //deno-lint-ignore constructor-super + constructor(Obj) { + if (!isObject(Obj)) { + // The provided value is not an object. + throw new TypeError( + "Piscēs: Cannot convert primitive to property descriptor.", + ); + } else { + // The provided value is an object. + const desc = Object.create(propertyDescriptorPrototype); + if ("enumerable" in Obj) { + // An enumerable property is specified. + desc.enumerable = !!Obj.enumerable; + } else { + // An enumerable property is not specified. + /* do nothing */ + } + if ("configurable" in Obj) { + // A configurable property is specified. + desc.configurable = !!Obj.configurable; + } else { + // A configurable property is not specified. + /* do nothing */ + } + if ("value" in Obj) { + // A value property is specified. + desc.value = Obj.value; + } else { + // A value property is not specified. + /* do nothing */ + } + if ("writable" in Obj) { + // A writable property is specified. + desc.writable = !!Obj.writable; + } else { + // A writable property is not specified. + /* do nothing */ + } + if ("get" in Obj) { + // A get property is specified. + const getter = Obj.get; + if (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 Obj) { + // A set property is specified. + const setter = Obj.set; + if (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 */ + } + } + + /** Returns whether this is an accessor descrtiptor. */ + get isAccessorDescriptor() { + return this !== undefined && ("get" in this || "set" in this); + } + + /** Returns whether this is a data descrtiptor. */ + get isDataDescriptor() { + return this !== undefined && + ("value" in this || "writable" in this); + } + + /** + * Returns 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; + } + + /** + * Returns 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 coercePropretyDescriptorValue = (P, V) => { + switch (P) { + case "configurable": + case "enumerable": + case "writable": + return !!V; + case "value": + return V; + case "get": + if (typeof V != "function") { + throw new TypeError( + "Piscēs: Getters must be callable.", + ); + } else { + return V; + } + case "set": + if (typeof V != "function") { + throw new TypeError( + "Piscēs: Setters must be callable.", + ); + } else { + return V; + } + default: + return V; + } + }; + + const propertyDescriptorPrototype = PropertyDescriptor.prototype; + + 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) { + // Desc has a value. + desc.value = coercePropretyDescriptorValue(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 Reflect.defineProperty(O, P, desc); + } + } else { + // P is not a property descriptor attribute. + return Reflect.defineProperty(O, P, Desc); + } + }, + set(O, P, V, Receiver) { + 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. + return Reflect.set(O, prop, newValue, Receiver); + } + }, + setPrototypeOf(O, V) { + if (V !== propertyDescriptorPrototype) { + // V is not the property descriptor prototype. + return false; + } else { + // V is the property descriptor prototype. + return Reflect.setPrototypeOf(O, V); + } + }, + }, + ); + + return PropertyDescriptor; +})(); + +/** + * Returns a new frozen shallow copy of the enumerable own properties + * of the provided object, according to the following rules :— + * + * - For data properties, create a nonconfigurable, nonwritable + * property with the same value. + * + * - For accessor properties, create a nonconfigurable accessor + * property with the same getter *and* setter. + * + * The prototype for the resulting object will be taken from the + * `prototype` property of the provided constructor, or the `prototype` + * of the `constructor` of the provided object if the provided + * constructor is undefined. If the used constructor has a nonnullish + * `Symbol.species`, that will be used instead. + */ +export const frozenCopy = (O, constructor = O?.constructor) => { + if (O == null) { + // O is null or undefined. + throw new TypeError( + "Piscēs: Cannot copy properties of null or undefined.", + ); + } else { + // O is not null or undefined. + // + // (If not provided, the constructor will be the value of getting + // the `constructor` property of O.) + const species = constructor?.[Symbol.species] ?? constructor; + return Object.preventExtensions( + Object.create( + species == null || !("prototype" in species) + ? null + : species.prototype, + Object.fromEntries( + function* () { + for (const P of Reflect.ownKeys(O)) { + const Desc = Object.getOwnPropertyDescriptor(O, P); + if (Desc.enumerable) { + // P is an enumerable property. + yield [ + P, + "get" in Desc || "set" in Desc + ? { + configurable: false, + enumerable: true, + get: Desc.get, + set: Desc.set, + } + : { + configurable: false, + enumerable: true, + value: Desc.value, + writable: false, + }, + ]; + } else { + // P is not an enumerable property. + /* do nothing */ + } + } + }(), + ), + ), + ); + } +}; + /** Returns whether the provided value is a constructor. */ export const isConstructor = ($) => { if (!isObject($)) {