]> Lady’s Gitweb - Pisces/blob - object.js
Make getOwnPropertyDescriptor(s) safer
[Pisces] / object.js
1 // ♓🌟 Piscēs ∷ object.js
2 // ====================================================================
3 //
4 // Copyright © 2022–2023 Lady [@ Lady’s Computer].
5 //
6 // This Source Code Form is subject to the terms of the Mozilla Public
7 // License, v. 2.0. If a copy of the MPL was not distributed with this
8 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
9
10 import { bind, call, createArrowFunction } from "./function.js";
11 import {
12 IS_CONCAT_SPREADABLE,
13 ITERATOR,
14 SPECIES,
15 toFunctionName,
16 toLength,
17 toPrimitive,
18 toPropertyDescriptor,
19 type,
20 UNDEFINED,
21 } from "./value.js";
22
23 /**
24 * An object whose properties are lazy‐loaded from the methods on the
25 * own properties of the provided object.
26 *
27 * This is useful when you are looking to reference properties on
28 * objects which, due to module dependency graphs, cannot be guaranteed
29 * to have been initialized yet.
30 *
31 * The resulting properties will have the same attributes (regarding
32 * configurability, enumerability, and writability) as the
33 * corresponding properties on the methods object. If a property is
34 * marked as writable, the method will never be called if it is set
35 * before it is gotten. By necessity, the resulting properties are all
36 * configurable before they are accessed for the first time.
37 *
38 * Methods will be called with the resulting object as their this
39 * value.
40 *
41 * `LazyLoader` objects have the same prototype as the passed methods
42 * object.
43 */
44 export class LazyLoader extends null {
45 /**
46 * Constructs a new `LazyLoader` object.
47 *
48 * ☡ This function throws if the provided value is not an object.
49 */
50 constructor(loadMethods) {
51 if (type(loadMethods) !== "object") {
52 // The provided value is not an object; throw an error.
53 throw new TypeError(
54 `Piscēs: Cannot construct LazyLoader: Provided argument is not an object: ${loadMethods}.`,
55 );
56 } else {
57 // The provided value is an object; process it and build the
58 // result.
59 const result = objectCreate(getPrototype(loadMethods));
60 const methodKeys = getOwnPropertyKeys(loadMethods);
61 for (let index = 0; index < methodKeys.length; ++index) {
62 // Iterate over the property keys of the provided object and
63 // define getters and setters appropriately on the result.
64 const methodKey = methodKeys[index];
65 const { configurable, enumerable, writable } =
66 getOwnPropertyDescriptor(loadMethods, methodKey);
67 defineOwnProperty(result, methodKey, {
68 configurable: true,
69 enumerable,
70 get: defineOwnProperty(
71 () => {
72 const value = call(loadMethods[methodKey], result, []);
73 defineOwnProperty(result, methodKey, {
74 configurable,
75 enumerable,
76 value,
77 writable,
78 });
79 return value;
80 },
81 "name",
82 { value: toFunctionName(methodKey, "get") },
83 ),
84 set: writable
85 ? defineOwnProperty(
86 ($) =>
87 defineOwnProperty(result, methodKey, {
88 configurable,
89 enumerable,
90 value: $,
91 writable,
92 }),
93 "name",
94 { value: toFunctionName(methodKey, "set") },
95 )
96 : void {},
97 });
98 }
99 return result;
100 }
101 }
102 }
103
104 /**
105 * Defines an own property on the provided object on the provided
106 * property key using the provided property descriptor.
107 *
108 * ※ This is effectively an alias for `Object.defineProperty`.
109 */
110 export const defineOwnProperty = createArrowFunction(
111 Object.defineProperty,
112 { name: "defineOwnProperty" },
113 );
114
115 export const {
116 /**
117 * Defines own properties on the provided object using the
118 * descriptors on the enumerable own properties of the provided
119 * additional objects.
120 *
121 * ※ This differs from `Object.defineProperties` in that it can take
122 * multiple source objects.
123 */
124 defineOwnProperties,
125
126 /**
127 * Returns a new frozen shallow copy of the enumerable own properties
128 * of the provided object, according to the following rules :—
129 *
130 * - For data properties, create a nonconfigurable, nonwritable
131 * property with the same value.
132 *
133 * - For accessor properties, create a nonconfigurable accessor
134 * property with the same getter *and* setter.
135 *
136 * The prototype for the resulting object will be taken from the
137 * `.prototype` property of the provided constructor, or the
138 * `.prototype` of the `.constructor` of the provided object if the
139 * provided constructor is undefined. If the used constructor has a
140 * nonnullish `.[Symbol.species]`, that will be used instead. If the
141 * used constructor or species is nullish or does not have a
142 * `.prototype` property, the prototype is set to null.
143 *
144 * ※ The prototype of the provided object itself is ignored.
145 */
146 frozenCopy,
147
148 /**
149 * Returns the property descriptor record for the own property with
150 * the provided property key on the provided object, or null if none
151 * exists.
152 *
153 * ※ This is effectively an alias for
154 * `Object.getOwnPropertyDescriptor`, but the return value is a
155 * proxied object with null prototype.
156 */
157 getOwnPropertyDescriptor,
158
159 /**
160 * Returns the property descriptors for the own properties on the
161 * provided object.
162 *
163 * ※ This is effectively an alias for
164 * `Object.getOwnPropertyDescriptors`, but the values on the
165 * resulting object are proxied objects with null prototypes.
166 */
167 getOwnPropertyDescriptors,
168
169 /**
170 * Returns whether the provided object is frozen.
171 *
172 * ※ This function returns false for nonobjects.
173 *
174 * ※ This is effectively an alias for `!Object.isFrozen`.
175 */
176 isUnfrozenObject,
177
178 /**
179 * Returns whether the provided object is sealed.
180 *
181 * ※ This function returns false for nonobjects.
182 *
183 * ※ This is effectively an alias for `!Object.isSealed`.
184 */
185 isUnsealedObject,
186
187 /**
188 * Sets the prototype of the provided object to the provided value
189 * and returns the object.
190 *
191 * ※ This is effectively an alias for `Object.setPrototypeOf`.
192 */
193 setPrototype,
194
195 /**
196 * Returns the provided value converted to an object.
197 *
198 * Existing objects are returned with no modification.
199 *
200 * ☡ This function throws if its argument is null or undefined.
201 */
202 toObject,
203 } = (() => {
204 const createObject = Object;
205 const {
206 create,
207 defineProperties,
208 getOwnPropertyDescriptor: objectGetOwnPropertyDescriptor,
209 getPrototypeOf,
210 isFrozen,
211 isSealed,
212 setPrototypeOf,
213 } = Object;
214 const {
215 next: generatorIteratorNext,
216 } = getPrototypeOf(function* () {}.prototype);
217 const propertyDescriptorEntryIterablePrototype = {
218 [ITERATOR]() {
219 return {
220 next: bind(generatorIteratorNext, this.generator(), []),
221 };
222 },
223 };
224 const propertyDescriptorEntryIterable = ($) =>
225 create(propertyDescriptorEntryIterablePrototype, {
226 generator: { value: $ },
227 });
228
229 return {
230 defineOwnProperties: (O, ...sources) => {
231 const { length } = sources;
232 for (let index = 0; index < length; ++index) {
233 defineProperties(O, sources[index]);
234 }
235 return O;
236 },
237 frozenCopy: (O, constructor = O?.constructor) => {
238 if (O == null) {
239 // O is null or undefined.
240 throw new TypeError(
241 "Piscēs: Cannot copy properties of null or undefined.",
242 );
243 } else {
244 // O is not null or undefined.
245 //
246 // (If not provided, the constructor will be the value of
247 // getting the `.constructor` property of O.)
248 const species = constructor?.[SPECIES] ?? constructor;
249 return preventExtensions(
250 objectCreate(
251 species == null || !("prototype" in species)
252 ? null
253 : species.prototype,
254 objectFromEntries(
255 propertyDescriptorEntryIterable(function* () {
256 const ownPropertyKeys = getOwnPropertyKeys(O);
257 for (
258 let i = 0;
259 i < ownPropertyKeys.length;
260 ++i
261 ) {
262 const P = ownPropertyKeys[i];
263 const Desc = getOwnPropertyDescriptor(O, P);
264 if (Desc.enumerable) {
265 // P is an enumerable property.
266 yield [
267 P,
268 "get" in Desc || "set" in Desc
269 ? {
270 configurable: false,
271 enumerable: true,
272 get: Desc.get,
273 set: Desc.set,
274 }
275 : {
276 configurable: false,
277 enumerable: true,
278 value: Desc.value,
279 writable: false,
280 },
281 ];
282 } else {
283 // P is not an enumerable property.
284 /* do nothing */
285 }
286 }
287 }),
288 ),
289 ),
290 );
291 }
292 },
293 getOwnPropertyDescriptor: (O, P) => {
294 const desc = objectGetOwnPropertyDescriptor(O, P);
295 return desc === UNDEFINED
296 ? UNDEFINED
297 : toPropertyDescriptor(desc);
298 },
299 getOwnPropertyDescriptors: (O) => {
300 const obj = toObject(O);
301 const ownKeys = getOwnPropertyKeys(obj);
302 const descriptors = {};
303 for (let k = 0; k < ownKeys.length; ++k) {
304 const key = ownKeys[k];
305 defineOwnProperty(descriptors, key, {
306 configurable: true,
307 enumerable: true,
308 value: getOwnPropertyDescriptor(O, key),
309 writable: true,
310 });
311 }
312 return descriptors;
313 },
314 isUnfrozenObject: (O) => !isFrozen(O),
315 isUnsealedObject: (O) => !isSealed(O),
316 setPrototype: (O, proto) => {
317 const obj = toObject(O);
318 if (O === obj) {
319 // The provided value is an object; set its prototype normally.
320 return setPrototypeOf(O, proto);
321 } else {
322 // The provided value is not an object; attempt to set the
323 // prototype on a coerced version with extensions prevented,
324 // then return the provided value.
325 //
326 // This will throw if the given prototype does not match the
327 // existing one on the coerced object.
328 setPrototypeOf(preventExtensions(obj), proto);
329 return O;
330 }
331 },
332 toObject: ($) => {
333 if ($ == null) {
334 // The provided value is nullish; this is an error.
335 throw new TypeError(
336 `Piscēs: Cannot convert ${$} into an object.`,
337 );
338 } else {
339 // The provided value is not nullish; coerce it to an object.
340 return createObject($);
341 }
342 },
343 };
344 })();
345
346 export const {
347 /**
348 * Removes the provided property key from the provided object and
349 * returns the object.
350 *
351 * ※ This function differs from `Reflect.deleteProperty` and the
352 * `delete` operator in that it throws if the deletion is
353 * unsuccessful.
354 *
355 * ☡ This function throws if the first argument is not an object.
356 */
357 deleteOwnProperty,
358
359 /**
360 * Returns an array of property keys on the provided object.
361 *
362 * ※ This is effectively an alias for `Reflect.ownKeys`, except that
363 * it does not require that the argument be an object.
364 */
365 getOwnPropertyKeys,
366
367 /**
368 * Returns the value of the provided property key on the provided
369 * object.
370 *
371 * ※ This is effectively an alias for `Reflect.get`, except that it
372 * does not require that the argument be an object.
373 */
374 getPropertyValue,
375
376 /**
377 * Returns whether the provided property key exists on the provided
378 * object.
379 *
380 * ※ This is effectively an alias for `Reflect.has`, except that it
381 * does not require that the argument be an object.
382 *
383 * ※ This includes properties present on the prototype chain.
384 */
385 hasProperty,
386
387 /**
388 * Sets the provided property key to the provided value on the
389 * provided object and returns the object.
390 *
391 * ※ This function differs from `Reflect.set` in that it throws if
392 * the setting is unsuccessful.
393 *
394 * ☡ This function throws if the first argument is not an object.
395 */
396 setPropertyValue,
397 } = (() => {
398 const { deleteProperty, get, has, ownKeys, set } = Reflect;
399
400 return {
401 deleteOwnProperty: (O, P) => {
402 if (type(O) !== "object") {
403 throw new TypeError(
404 `Piscēs: Tried to set property but provided value was not an object: ${V}`,
405 );
406 } else if (!deleteProperty(O, P)) {
407 throw new TypeError(
408 `Piscēs: Tried to delete property from object but [[Delete]] returned false: ${P}`,
409 );
410 } else {
411 return O;
412 }
413 },
414 getOwnPropertyKeys: (O) => ownKeys(toObject(O)),
415 getPropertyValue: (O, P, Receiver = O) =>
416 get(toObject(O), P, Receiver),
417 hasProperty: (O, P) => has(toObject(O), P),
418 setPropertyValue: (O, P, V, Receiver = O) => {
419 if (type(O) !== "object") {
420 throw new TypeError(
421 `Piscēs: Tried to set property but provided value was not an object: ${V}`,
422 );
423 } else if (!set(O, P, V, Receiver)) {
424 throw new TypeError(
425 `Piscēs: Tried to set property on object but [[Set]] returned false: ${P}`,
426 );
427 } else {
428 return O;
429 }
430 },
431 };
432 })();
433
434 /**
435 * Marks the provided object as non·extensible and marks all its
436 * properties as nonconfigurable and (if data properties) nonwritable,
437 * and returns the object.
438 *
439 * ※ This is effectively an alias for `Object.freeze`.
440 */
441 export const freeze = createArrowFunction(Object.freeze);
442
443 /**
444 * Returns the function on the provided value at the provided property
445 * key.
446 *
447 * ☡ This function throws if the provided property key does not have an
448 * associated value which is callable.
449 */
450 export const getMethod = (V, P) => {
451 const func = getPropertyValue(V, P);
452 if (func == null) {
453 return undefined;
454 } else if (typeof func !== "function") {
455 throw new TypeError(`Piscēs: Method not callable: ${P}`);
456 } else {
457 return func;
458 }
459 };
460
461 /**
462 * Returns an array of string‐valued own property keys on the
463 * provided object.
464 *
465 * ☡ This includes both enumerable and non·enumerable properties.
466 *
467 * ※ This is effectively an alias for `Object.getOwnPropertyNames`.
468 */
469 export const getOwnPropertyStrings = createArrowFunction(
470 Object.getOwnPropertyNames,
471 { name: "getOwnPropertyStrings" },
472 );
473
474 /**
475 * Returns an array of symbol‐valued own property keys on the
476 * provided object.
477 *
478 * ☡ This includes both enumerable and non·enumerable properties.
479 *
480 * ※ This is effectively an alias for
481 * `Object.getOwnPropertySymbols`.
482 */
483 export const getOwnPropertySymbols = createArrowFunction(
484 Object.getOwnPropertySymbols,
485 );
486
487 /**
488 * Returns the prototype of the provided object.
489 *
490 * ※ This is effectively an alias for `Object.getPrototypeOf`.
491 */
492 export const getPrototype = createArrowFunction(
493 Object.getPrototypeOf,
494 { name: "getPrototype" },
495 );
496
497 /**
498 * Returns whether the provided object has an own property with the
499 * provided property key.
500 *
501 * ※ This is effectively an alias for `Object.hasOwn`.
502 */
503 export const hasOwnProperty = createArrowFunction(Object.hasOwn, {
504 name: "hasOwnProperty",
505 });
506
507 /** Returns whether the provided value is an arraylike object. */
508 export const isArraylikeObject = ($) => {
509 if (type($) !== "object") {
510 return false;
511 } else {
512 try {
513 lengthOfArraylike($); // throws if not arraylike
514 return true;
515 } catch {
516 return false;
517 }
518 }
519 };
520
521 export const {
522 /**
523 * Returns whether the provided value is spreadable during array
524 * concatenation.
525 *
526 * This is also used to determine which things should be treated as
527 * collections.
528 */
529 isConcatSpreadableObject,
530 } = (() => {
531 const { isArray } = Array;
532
533 return {
534 isConcatSpreadableObject: ($) => {
535 if (type($) !== "object") {
536 // The provided value is not an object.
537 return false;
538 } else {
539 // The provided value is an object.
540 const spreadable = $[IS_CONCAT_SPREADABLE];
541 return spreadable !== undefined ? !!spreadable : isArray($);
542 }
543 },
544 };
545 })();
546
547 /**
548 * Returns whether the provided object is extensible.
549 *
550 * ※ This function returns false for nonobjects.
551 *
552 * ※ This is effectively an alias for `Object.isExtensible`.
553 */
554 export const isExtensibleObject = createArrowFunction(
555 Object.isExtensible,
556 { name: "isExtensibleObject" },
557 );
558
559 /**
560 * Returns the length of the provided arraylike value.
561 *
562 * This can produce larger lengths than can actually be stored in
563 * arrays, because no such restrictions exist on arraylike methods.
564 *
565 * ☡ This function throws if the provided value is not arraylike.
566 */
567 export const lengthOfArraylike = ({ length }) => toLength(length);
568
569 /**
570 * Returns an array of key~value pairs for the enumerable,
571 * string‐valued property keys on the provided object.
572 *
573 * ※ This is effectively an alias for `Object.entries`.
574 */
575 export const namedEntries = createArrowFunction(Object.entries, {
576 name: "namedEntries",
577 });
578
579 /**
580 * Returns an array of the enumerable, string‐valued property keys on
581 * the provided object.
582 *
583 * ※ This is effectively an alias for `Object.keys`.
584 */
585 export const namedKeys = createArrowFunction(Object.keys, {
586 name: "namedKeys",
587 });
588
589 /**
590 * Returns an array of property values for the enumerable,
591 * string‐valued property keys on the provided object.
592 *
593 * ※ This is effectively an alias for `Object.values`.
594 */
595 export const namedValues = createArrowFunction(Object.values, {
596 name: "namedValues",
597 });
598
599 /**
600 * Returns a new object with the provided prototype and property
601 * descriptors.
602 *
603 * ※ This is effectively an alias for `Object.create`.
604 */
605 export const objectCreate = createArrowFunction(Object.create, {
606 name: "objectCreate",
607 });
608
609 /**
610 * Returns a new object with the provided property keys and values.
611 *
612 * ※ This is effectively an alias for `Object.fromEntries`.
613 */
614 export const objectFromEntries = createArrowFunction(
615 Object.fromEntries,
616 { name: "objectFromEntries" },
617 );
618
619 /**
620 * Marks the provided object as non·extensible, and returns the
621 * object.
622 *
623 * ※ This is effectively an alias for `Object.preventExtensions`.
624 */
625 export const preventExtensions = createArrowFunction(
626 Object.preventExtensions,
627 );
628
629 /**
630 * Marks the provided object as non·extensible and marks all its
631 * properties as nonconfigurable, and returns the object.
632 *
633 * ※ This is effectively an alias for `Object.seal`.
634 */
635 export const seal = createArrowFunction(Object.seal);
636
637 /**
638 * Sets the values of the enumerable own properties of the provided
639 * additional objects on the provided object.
640 *
641 * ※ This is effectively an alias for `Object.assign`.
642 */
643 export const setPropertyValues = createArrowFunction(Object.assign, {
644 name: "setPropertyValues",
645 });
646
647 /**
648 * Returns the property key (symbol or string) corresponding to the
649 * provided value.
650 */
651 export const toPropertyKey = ($) => {
652 const key = toPrimitive($, "string");
653 return typeof key === "symbol" ? key : `${key}`;
654 };
This page took 0.175762 seconds and 5 git commands to generate.