From: Lady Date: Sat, 25 Nov 2023 17:28:29 +0000 (-0500) Subject: Swap some things between value.js and object.js X-Git-Url: https://git.ladys.computer/Pisces/commitdiff_plain/c9e64e81cd9364775a61aa7f80e0b398286ac742 Swap some things between value.js and object.js - Property Descriptor record functions → object.js. Leaving `completePropertyDescriptor` in value.js for now, since it does not expect specifically a property descriptor record. - `toPropertyKey` → value.js --- diff --git a/object.js b/object.js index aa94bc5..bd310ca 100644 --- a/object.js +++ b/object.js @@ -13,8 +13,6 @@ import { SPECIES, toFunctionName, toLength, - toPrimitive, - toPropertyDescriptor, type, UNDEFINED, } from "./value.js"; @@ -46,11 +44,13 @@ const { const { apply: call, deleteProperty, + defineProperty: reflectDefineProperty, get, getOwnPropertyDescriptor: reflectGetOwnPropertyDescriptor, has, ownKeys, set, + setPrototypeOf: reflectSetPrototypeOf, } = Reflect; /** @@ -339,7 +339,9 @@ export const getMethod = (V, P) => { */ export const getOwnPropertyDescriptor = (O, P) => { const desc = objectGetOwnPropertyDescriptor(O, P); - return desc === UNDEFINED ? UNDEFINED : toPropertyDescriptor(desc); + return desc === UNDEFINED + ? UNDEFINED + : toPropertyDescriptorRecord(desc); }; /** @@ -472,6 +474,220 @@ export const isConcatSpreadableObject = ($) => { */ export const isExtensibleObject = (O) => isExtensible(O); +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. + */ + toPropertyDescriptorRecord, +} = (() => { + 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 = objectFreeze( + assign( + 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 = assign(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, [$]), + toPropertyDescriptorRecord: (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 = create(null); + if ("enumerable" in Obj) { + // An enumerable property is specified. + defineOwnDataProperty(desc, "enumerable", !!Obj.enumerable); + } else { + // An enumerable property is not specified. + /* do nothing */ + } + if ("configurable" in Obj) { + // A configurable property is specified. + defineOwnDataProperty( + desc, + "configurable", + !!Obj.configurable, + ); + } else { + // A configurable property is not specified. + /* do nothing */ + } + if ("value" in Obj) { + // A value property is specified. + defineOwnDataProperty(desc, "value", Obj.value); + } else { + // A value property is not specified. + /* do nothing */ + } + if ("writable" in Obj) { + // A writable property is specified. + defineOwnDataProperty(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 (getter !== UNDEFINED && typeof getter !== "function") { + // The getter is not callable. + throw new TypeError("Piscēs: Getters must be callable."); + } else { + // The getter is callable. + defineOwnDataProperty(desc, "get", Obj.get); + } + } 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. + defineOwnDataProperty(desc, "set", Obj.set); + } + } 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; + } + } + }, + }; +})(); + /** * Returns whether the provided value is an unfrozen object. * @@ -661,12 +877,3 @@ export const toObject = ($) => { return object($); } }; - -/** - * Returns the property key (symbol or string) corresponding to the - * provided value. - */ -export const toPropertyKey = ($) => { - const key = toPrimitive($, "string"); - return typeof key === "symbol" ? key : `${key}`; -}; diff --git a/object.test.js b/object.test.js index 3a0e1a7..348d06d 100644 --- a/object.test.js +++ b/object.test.js @@ -10,6 +10,7 @@ import { assert, assertEquals, + assertNotStrictEquals, assertSpyCall, assertSpyCalls, assertStrictEquals, @@ -39,6 +40,7 @@ import { isArraylikeObject, isConcatSpreadableObject, isExtensibleObject, + isPropertyDescriptorRecord, isUnfrozenObject, isUnsealedObject, LazyLoader, @@ -54,7 +56,7 @@ import { setPropertyValues, setPrototype, toObject, - toPropertyKey, + toPropertyDescriptorRecord, } from "./object.js"; describe("LazyLoader", () => { @@ -1245,6 +1247,45 @@ describe("isExtensibleObject", () => { }); }); +describe("isPropertyDescriptorRecord", () => { + it("[[Call]] returns true for objects created by toPropertyDescriptorRecord", () => { + assertStrictEquals( + isPropertyDescriptorRecord(toPropertyDescriptorRecord({})), + 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", + ); + }); + }); +}); + describe("isUnfrozenObject", () => { it("[[Call]] returns true for unfrozen objects", () => { assertStrictEquals(isUnfrozenObject({}), true); @@ -1890,43 +1931,274 @@ describe("toObject", () => { }); }); -describe("toPropertyKey", () => { - it("returns a string or symbol", () => { - const sym = Symbol(); - assertStrictEquals(toPropertyKey(sym), sym); - assertStrictEquals( - toPropertyKey(new String("success")), - "success", - ); +describe("toPropertyDescriptorRecord", () => { + it("[[Call]] creates a new property descriptor record", () => { + const obj = {}; + const desc = toPropertyDescriptorRecord(obj); + assertEquals(obj, desc); + assertNotStrictEquals(obj, desc); }); - it("favours the `toString` representation", () => { - assertStrictEquals( - toPropertyKey({ - toString() { - return "success"; - }, - valueOf() { - return "failure"; - }, + it("[[Call]] coerces the values", () => { + assertEquals( + toPropertyDescriptorRecord({ + configurable: undefined, + enumerable: undefined, + writable: undefined, }), - "success", + { configurable: false, enumerable: false, writable: false }, ); }); + it("[[Construct]] throws for primitives", () => { + assertThrows(() => toPropertyDescriptorRecord(undefined)); + assertThrows(() => toPropertyDescriptorRecord("failure")); + }); + it("[[Construct]] throws an error", () => { - assertThrows(() => new toPropertyKey("")); + assertThrows(() => new toPropertyDescriptorRecord({})); }); describe(".length", () => { it("[[Get]] returns the correct length", () => { - assertStrictEquals(toPropertyKey.length, 1); + assertStrictEquals(toPropertyDescriptorRecord.length, 1); }); }); describe(".name", () => { it("[[Get]] returns the correct name", () => { - assertStrictEquals(toPropertyKey.name, "toPropertyKey"); + assertStrictEquals( + toPropertyDescriptorRecord.name, + "toPropertyDescriptorRecord", + ); + }); + }); + + describe("~configurable", () => { + it("[[DefineOwnProperty]] coerces to a boolean", () => { + const desc = toPropertyDescriptorRecord({}); + Object.defineProperty(desc, "configurable", {}); + assertStrictEquals(desc.configurable, false); + }); + + it("[[DefineOwnProperty]] throws for accessor properties", () => { + const desc = toPropertyDescriptorRecord({}); + assertThrows(() => + Object.defineProperty(desc, "configurable", { get: undefined }) + ); + }); + + it("[[Set]] coerces to a boolean", () => { + const desc = toPropertyDescriptorRecord({}); + desc.configurable = undefined; + assertStrictEquals(desc.configurable, false); + }); + + it("[[Delete]] works", () => { + const desc = toPropertyDescriptorRecord({ configurable: false }); + delete desc.configurable; + assert(!("configurable" in desc)); + }); + }); + + describe("~enumerable", () => { + it("[[DefineOwnProperty]] coerces to a boolean", () => { + const desc = toPropertyDescriptorRecord({}); + Object.defineProperty(desc, "enumerable", {}); + assertStrictEquals(desc.enumerable, false); + }); + + it("[[DefineOwnProperty]] throws for accessor properties", () => { + const desc = toPropertyDescriptorRecord({}); + assertThrows(() => + Object.defineProperty(desc, "enumerable", { get: undefined }) + ); + }); + + it("[[Set]] coerces to a boolean", () => { + const desc = toPropertyDescriptorRecord({}); + desc.enumerable = undefined; + assertStrictEquals(desc.enumerable, false); + }); + + it("[[Delete]] works", () => { + const desc = toPropertyDescriptorRecord({ enumerable: false }); + delete desc.enumerable; + assert(!("enumerable" in desc)); + }); + }); + + describe("~get", () => { + it("[[DefineOwnProperty]] works", () => { + const desc = toPropertyDescriptorRecord({}); + Object.defineProperty(desc, "get", {}); + assertStrictEquals(desc.get, undefined); + }); + + it("[[DefineOwnProperty]] throws for accessor properties", () => { + const desc = toPropertyDescriptorRecord({}); + assertThrows(() => + Object.defineProperty(desc, "get", { get: undefined }) + ); + }); + + it("[[DefineOwnProperty]] throws if not callable or undefined", () => { + const desc = toPropertyDescriptorRecord({}); + assertThrows( + () => Object.defineProperty(desc, "get", { value: null }), + ); + }); + + it("[[DefineOwnProperty]] throws if a data property is defined", () => { + const desc = toPropertyDescriptorRecord({ value: undefined }); + assertThrows(() => Object.defineProperty(desc, "get", {})); + }); + + it("[[Set]] works", () => { + const desc = toPropertyDescriptorRecord({}); + const fn = () => {}; + desc.get = fn; + assertStrictEquals(desc.get, fn); + }); + + it("[[Set]] throws if not callable or undefined", () => { + const desc = toPropertyDescriptorRecord({}); + assertThrows(() => desc.get = null); + }); + + it("[[Set]] throws if a data property is defined", () => { + const desc = toPropertyDescriptorRecord({ value: undefined }); + assertThrows(() => desc.get = undefined); + }); + + it("[[Delete]] works", () => { + const desc = toPropertyDescriptorRecord({ get: undefined }); + delete desc.get; + assert(!("get" in desc)); + }); + }); + + describe("~set", () => { + it("[[DefineOwnProperty]] works", () => { + const desc = toPropertyDescriptorRecord({}); + Object.defineProperty(desc, "set", {}); + assertStrictEquals(desc.set, undefined); + }); + + it("[[DefineOwnProperty]] throws for accessor properties", () => { + const desc = toPropertyDescriptorRecord({}); + assertThrows(() => + Object.defineProperty(desc, "set", { get: undefined }) + ); + }); + + it("[[DefineOwnProperty]] throws if not callable or undefined", () => { + const desc = toPropertyDescriptorRecord({}); + assertThrows( + () => Object.defineProperty(desc, "set", { value: null }), + ); + }); + + it("[[DefineOwnProperty]] throws if a data property is defined", () => { + const desc = toPropertyDescriptorRecord({ value: undefined }); + assertThrows(() => Object.defineProperty(desc, "set", {})); + }); + + it("[[Set]] works", () => { + const desc = toPropertyDescriptorRecord({}); + const fn = (_) => {}; + desc.set = fn; + assertStrictEquals(desc.set, fn); + }); + + it("[[Set]] throws if not callable or undefined", () => { + const desc = toPropertyDescriptorRecord({}); + assertThrows(() => desc.set = null); + }); + + it("[[Set]] throws if a data property is defined", () => { + const desc = toPropertyDescriptorRecord({ value: undefined }); + assertThrows(() => desc.set = undefined); + }); + + it("[[Delete]] works", () => { + const desc = toPropertyDescriptorRecord({ set: undefined }); + delete desc.set; + assert(!("set" in desc)); + }); + }); + + describe("~value", () => { + it("[[DefineOwnProperty]] works", () => { + const desc = toPropertyDescriptorRecord({}); + Object.defineProperty(desc, "value", {}); + assertStrictEquals(desc.value, undefined); + }); + + it("[[DefineOwnProperty]] throws for accessor properties", () => { + const desc = toPropertyDescriptorRecord({}); + assertThrows(() => + Object.defineProperty(desc, "value", { get: undefined }) + ); + }); + + it("[[DefineOwnProperty]] throws if an accessor property is defined", () => { + const desc = toPropertyDescriptorRecord({ get: undefined }); + assertThrows(() => Object.defineProperty(desc, "value", {})); + }); + + it("[[Set]] works", () => { + const desc = toPropertyDescriptorRecord({}); + desc.value = "success"; + assertStrictEquals(desc.value, "success"); + }); + + it("[[Set]] throws if an accessor property is defined", () => { + const desc = toPropertyDescriptorRecord({ get: undefined }); + assertThrows(() => desc.value = null); + }); + + it("[[Delete]] works", () => { + const desc = toPropertyDescriptorRecord({ value: undefined }); + delete desc.value; + assert(!("value" in desc)); + }); + }); + + describe("~writable", () => { + it("[[DefineOwnProperty]] coerces to a boolean", () => { + const desc = toPropertyDescriptorRecord({}); + Object.defineProperty(desc, "writable", {}); + assertStrictEquals(desc.writable, false); + }); + + it("[[DefineOwnProperty]] throws for accessor properties", () => { + const desc = toPropertyDescriptorRecord({}); + assertThrows(() => + Object.defineProperty(desc, "writable", { get: undefined }) + ); + }); + + it("[[DefineOwnProperty]] throws if an accessor property is defined", () => { + const desc = toPropertyDescriptorRecord({ get: undefined }); + assertThrows(() => Object.defineProperty(desc, "writable", {})); + }); + + it("[[Set]] coerces to a boolean", () => { + const desc = toPropertyDescriptorRecord({}); + desc.writable = undefined; + assertStrictEquals(desc.writable, false); + }); + + it("[[Set]] throws if an accessor property is defined", () => { + const desc = toPropertyDescriptorRecord({ get: undefined }); + assertThrows(() => desc.writable = false); + }); + + it("[[Delete]] works", () => { + const desc = toPropertyDescriptorRecord({ writable: false }); + delete desc.writable; + assert(!("writable" in desc)); }); }); }); diff --git a/value.js b/value.js index 1039a3c..cf858ad 100644 --- a/value.js +++ b/value.js @@ -260,256 +260,6 @@ export const isGenericDescriptor = (Desc) => !("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 @@ -573,20 +323,20 @@ export const { "Piscēs: Unable to convert object to primitive", ); }, - toFunctionName: ($, prefix = undefined) => { + toFunctionName: ($, prefix = UNDEFINED) => { const key = toPrimitive($, "string"); const name = (() => { if (typeof key === "symbol") { // The provided value is a symbol; format its description. const description = call(getSymbolDescription, key, []); - return description === undefined ? "" : `[${description}]`; + return description === UNDEFINED ? "" : `[${description}]`; } else { // The provided value not a symbol; convert it to a string // property key. return `${key}`; } })(); - return prefix !== undefined ? `${prefix} ${name}` : name; + return prefix !== UNDEFINED ? `${prefix} ${name}` : name; }, toPrimitive: ($, preferredType = "default") => { const hint = `${preferredType}`; @@ -600,8 +350,8 @@ export const { ); } else if (type($) === "object") { // The provided value is an object. - const exoticToPrim = $[TO_PRIMITIVE] ?? undefined; - if (exoticToPrim !== undefined) { + const exoticToPrim = $[TO_PRIMITIVE] ?? UNDEFINED; + if (exoticToPrim !== UNDEFINED) { // The provided value has an exotic primitive conversion // method. if (typeof exoticToPrim !== "function") { @@ -698,6 +448,15 @@ export const { }; })(); +/** + * Returns the property key (symbol or string) corresponding to the + * provided value. + */ +export const toPropertyKey = ($) => { + const key = toPrimitive($, "string"); + return typeof key === "symbol" ? key : `${key}`; +}; + /** * Returns a lowercase string identifying the type of the provided * value. diff --git a/value.test.js b/value.test.js index 0f95f22..ce6464d 100644 --- a/value.test.js +++ b/value.test.js @@ -8,9 +8,7 @@ // file, You can obtain one at . import { - assert, assertEquals, - assertNotStrictEquals, assertStrictEquals, assertThrows, describe, @@ -25,7 +23,6 @@ import { isDataDescriptor, isFullyPopulatedDescriptor, isGenericDescriptor, - isPropertyDescriptorRecord, ITERATOR, LN10, LN2, @@ -57,7 +54,7 @@ import { toIndex, toLength, toPrimitive, - toPropertyDescriptor, + toPropertyKey, type, UNDEFINED, UNSCOPABLES, @@ -505,45 +502,6 @@ describe("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", - ); - }); - }); -}); - describe("ordinaryToPrimitive", () => { it("[[Call]] prefers `valueOf` by default", () => { const obj = { @@ -1034,274 +992,43 @@ describe("toPrimitive", () => { }); }); -describe("toPropertyDescriptor", () => { - it("[[Call]] creates a new property descriptor record", () => { - const obj = {}; - const desc = toPropertyDescriptor(obj); - assertEquals(obj, desc); - assertNotStrictEquals(obj, desc); +describe("toPropertyKey", () => { + it("returns a string or symbol", () => { + const sym = Symbol(); + assertStrictEquals(toPropertyKey(sym), sym); + assertStrictEquals( + toPropertyKey(new String("success")), + "success", + ); }); - it("[[Call]] coerces the values", () => { - assertEquals( - toPropertyDescriptor({ - configurable: undefined, - enumerable: undefined, - writable: undefined, + it("favours the `toString` representation", () => { + assertStrictEquals( + toPropertyKey({ + toString() { + return "success"; + }, + valueOf() { + return "failure"; + }, }), - { configurable: false, enumerable: false, writable: false }, + "success", ); }); - it("[[Construct]] throws for primitives", () => { - assertThrows(() => toPropertyDescriptor(undefined)); - assertThrows(() => toPropertyDescriptor("failure")); - }); - it("[[Construct]] throws an error", () => { - assertThrows(() => new toPropertyDescriptor({})); + assertThrows(() => new toPropertyKey("")); }); describe(".length", () => { it("[[Get]] returns the correct length", () => { - assertStrictEquals(toPropertyDescriptor.length, 1); + assertStrictEquals(toPropertyKey.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)); + assertStrictEquals(toPropertyKey.name, "toPropertyKey"); }); }); });