]> Lady’s Gitweb - Pisces/blob - value.js
Make getOwnPropertyDescriptor(s) safer
[Pisces] / value.js
1 // ♓🌟 Piscēs ∷ value.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 export const {
11 /** The welknown `@@asyncIterator` symbol. */
12 asyncIterator: ASYNC_ITERATOR,
13
14 /** The welknown `@@hasInstance` symbol. */
15 hasInstance: HAS_INSTANCE,
16
17 /** The welknown `@@isConcatSpreadable` symbol. */
18 isConcatSpreadable: IS_CONCAT_SPREADABLE,
19
20 /** The welknown `@@iterator` symbol. */
21 iterator: ITERATOR,
22
23 /** The welknown `@@match` symbol. */
24 match: MATCH,
25
26 /** The welknown `@@matchAll` symbol. */
27 matchAll: MATCH_ALL,
28
29 /** The welknown `@@replace` symbol. */
30 replace: REPLACE,
31
32 /** The welknown `@@species` symbol. */
33 species: SPECIES,
34
35 /** The welknown `@@split` symbol. */
36 split: SPLIT,
37
38 /** The welknown `@@toPrimitive` symbol. */
39 toPrimitive: TO_PRIMITIVE,
40
41 /** The welknown `@@toStringTag` symbol. */
42 toStringTag: TO_STRING_TAG,
43
44 /** The welknown `@@unscopables` symbol. */
45 unscopables: UNSCOPABLES,
46 } = Symbol;
47
48 export const {
49 /**
50 * ln(10).
51 *
52 * ※ This is an alias for `Math.LN10`.
53 */
54 LN10,
55
56 /**
57 * ln(2).
58 *
59 * ※ This is an alias for `Math.LN2`.
60 */
61 LN2,
62
63 /**
64 * log10(ℇ).
65 *
66 * ※ This is an alias for `Math.LOG10E`.
67 */
68 LOG10E: LOG10ℇ,
69
70 /**
71 * log2(ℇ).
72 *
73 * ※ This is an alias for `Math.LOG2E`.
74 */
75 LOG2E: LOG2ℇ,
76
77 /**
78 * sqrt(½).
79 *
80 * ※ This is an alias for `Math.SQRT1_2`.
81 */
82 SQRT1_2: RECIPROCAL_SQRT2,
83
84 /**
85 * sqrt(2).
86 *
87 * ※ This is an alias for `Math.SQRT2`.
88 */
89 SQRT2,
90
91 /**
92 * The mathematical constant π.
93 *
94 * ※ This is an alias for `Math.PI`.
95 */
96 PI: Π,
97
98 /**
99 * The Euler number.
100 *
101 * ※ This is an alias for `Math.E`.
102 */
103 E: ℇ,
104 } = Math;
105
106 export const {
107 /**
108 * The largest number value less than infinity.
109 *
110 * ※ This is an alias for `Number.MAX_VALUE`.
111 */
112 MAX_VALUE: MAXIMUM_NUMBER,
113
114 /**
115 * 2**53 - 1.
116 *
117 * ※ This is an alias for `Number.MAX_SAFE_INTEGER`.
118 */
119 MAX_SAFE_INTEGER: MAXIMUM_SAFE_INTEGRAL_NUMBER,
120
121 /**
122 * The smallest number value greater than negative infinity.
123 *
124 * ※ This is an alias for `Number.MIN_VALUE`.
125 */
126 MIN_VALUE: MINIMUM_NUMBER,
127
128 /**
129 * -(2**53 - 1).
130 *
131 * ※ This is an alias for `Number.MIN_SAFE_INTEGER`.
132 */
133 MIN_SAFE_INTEGER: MINIMUM_SAFE_INTEGRAL_NUMBER,
134
135 /**
136 * Negative infinity.
137 *
138 * ※ This is an alias for `Number.NEGATIVE_INFINITY`.
139 */
140 NEGATIVE_INFINITY,
141
142 /**
143 * Nan.
144 *
145 * ※ This is an alias for `Number.NaN`.
146 */
147 NaN: NAN,
148
149 /**
150 * Positive infinity.
151 *
152 * ※ This is an alias for `Number.POSITIVE_INFINITY`.
153 */
154 POSITIVE_INFINITY,
155
156 /**
157 * The difference between 1 and the smallest number greater than 1.
158 *
159 * ※ This is an alias for `Number.EPSILON`.
160 */
161 EPSILON: Ε,
162 } = Number;
163
164 /** Negative zero. */
165 export const NEGATIVE_ZERO = -0;
166
167 /** The null primitive. */
168 export const NULL = null;
169
170 /** Positive zero. */
171 export const POSITIVE_ZERO = 0;
172
173 /** The undefined primitive. */
174 export const UNDEFINED = undefined;
175
176 /**
177 * Completes the provided property descriptor by setting missing values
178 * to their defaults.
179 *
180 * ※ This method modifies the provided object and returns undefined.
181 */
182 export const completePropertyDescriptor = (Desc) => {
183 if (Desc === UNDEFINED) {
184 throw new TypeError(
185 "Piscēs: Cannot complete undefined property descriptor.",
186 );
187 } else if (!("get" in Desc || "set" in Desc)) {
188 // This is a generic or data descriptor.
189 if (!("value" in Desc)) {
190 // `value` is not defined on this.
191 Desc.value = UNDEFINED;
192 } else {
193 // `value` is already defined on this.
194 /* do nothing */
195 }
196 if (!("writable" in Desc)) {
197 // `writable` is not defined on this.
198 Desc.writable = false;
199 } else {
200 // `writable` is already defined on this.
201 /* do nothing */
202 }
203 } else {
204 // This is not a generic or data descriptor.
205 if (!("get" in Desc)) {
206 // `get` is not defined on this.
207 Desc.get = UNDEFINED;
208 } else {
209 // `get` is already defined on this.
210 /* do nothing */
211 }
212 if (!("set" in Desc)) {
213 // `set` is not defined on this.
214 Desc.set = UNDEFINED;
215 } else {
216 // `set` is already defined on this.
217 /* do nothing */
218 }
219 }
220 if (!("enumerable" in Desc)) {
221 // `enumerable` is not defined on this.
222 Desc.enumerable = false;
223 } else {
224 // `enumerable` is already defined on this.
225 /* do nothing */
226 }
227 if (!("configurable" in Desc)) {
228 // `configurable` is not defined on this.
229 Desc.configurable = false;
230 } else {
231 // `configurable` is already defined on this.
232 /* do nothing */
233 }
234 };
235
236 /** Gets whether the provided value is an accessor descrtiptor. */
237 export const isAccessorDescriptor = (Desc) =>
238 Desc !== UNDEFINED && ("get" in Desc || "set" in Desc);
239
240 /** Gets whether the provided value is a data descrtiptor. */
241 export const isDataDescriptor = (Desc) =>
242 Desc !== UNDEFINED && ("value" in Desc || "writable" in Desc);
243
244 /**
245 * Gets whether the provided value is a fully‐populated property
246 * descriptor.
247 */
248 export const isFullyPopulatedDescriptor = (Desc) =>
249 Desc !== UNDEFINED &&
250 ("value" in Desc && "writable" in Desc ||
251 "get" in Desc && "set" in Desc) &&
252 "enumerable" in Desc && "configurable" in Desc;
253
254 /**
255 * Gets whether the provided value is a generic (not accessor or data)
256 * descrtiptor.
257 */
258 export const isGenericDescriptor = (Desc) =>
259 Desc !== UNDEFINED &&
260 !("get" in Desc || "set" in Desc || "value" in Desc ||
261 "writable" in Desc);
262
263 export const {
264 /**
265 * Returns whether the provided value is a property descriptor record
266 * as created by `toPropertyDescriptor`.
267 *
268 * ※ This function is provided to enable inspection of whether an
269 * object uses the property descriptor record proxy implementation,
270 * not as a general test of whether an object satisfies the
271 * requirements for property descriptors. In most cases, a more
272 * specific test, like `isAccessorDescriptor`, `isDataDescriptor`, or
273 * `isGenericDescriptor`, is preferrable.
274 */
275 isPropertyDescriptorRecord,
276
277 /**
278 * Converts the provided value to a property descriptor record.
279 *
280 * ※ The prototype of a property descriptor record is always `null`.
281 *
282 * ※ Actually constructing a property descriptor object using this
283 * class is only necessary if you need strict guarantees about the
284 * types of its properties; the resulting object is proxied to ensure
285 * the types match what one would expect from composing
286 * FromPropertyDescriptor and ToPropertyDescriptor in the Ecmascript
287 * specification.
288 */
289 toPropertyDescriptor,
290 } = (() => {
291 const {
292 assign: setPropertyValues,
293 create: objectCreate,
294 defineProperty: defineOwnProperty,
295 } = Object;
296 const {
297 apply: call,
298 defineProperty: reflectDefineProperty,
299 setPrototypeOf: reflectSetPrototypeOf,
300 } = Reflect;
301 const proxyConstructor = Proxy;
302 const { add: weakSetAdd, has: weakSetHas } = WeakSet.prototype;
303 const propertyDescriptorRecords = new WeakSet();
304 const coercePropertyDescriptorValue = (P, V) => {
305 switch (P) {
306 case "configurable":
307 case "enumerable":
308 case "writable":
309 return !!V;
310 case "value":
311 return V;
312 case "get":
313 if (V !== undefined && typeof V !== "function") {
314 throw new TypeError(
315 "Piscēs: Getters must be callable.",
316 );
317 } else {
318 return V;
319 }
320 case "set":
321 if (V !== undefined && typeof V !== "function") {
322 throw new TypeError(
323 "Piscēs: Setters must be callable.",
324 );
325 } else {
326 return V;
327 }
328 default:
329 return V;
330 }
331 };
332 const propertyDescriptorProxyHandler = Object.freeze(
333 Object.assign(
334 Object.create(null),
335 {
336 defineProperty(O, P, Desc) {
337 if (
338 P === "configurable" || P === "enumerable" ||
339 P === "writable" || P === "value" ||
340 P === "get" || P === "set"
341 ) {
342 // P is a property descriptor attribute.
343 const desc = setPropertyValues(objectCreate(null), Desc);
344 if ("get" in desc || "set" in desc) {
345 // Desc is an accessor property descriptor.
346 throw new TypeError(
347 "Piscēs: Property descriptor attributes must be data properties.",
348 );
349 } else if ("value" in desc || !(P in O)) {
350 // Desc has a value or P does not already exist on O.
351 desc.value = coercePropertyDescriptorValue(
352 P,
353 desc.value,
354 );
355 } else {
356 // Desc is not an accessor property descriptor and has no
357 // value, but an existing value is present on O.
358 /* do nothing */
359 }
360 const isAccessorDescriptor = "get" === P || "set" === P ||
361 "get" in O || "set" in O;
362 const isDataDescriptor = "value" === P ||
363 "writable" === P ||
364 "value" in O || "writable" in O;
365 if (isAccessorDescriptor && isDataDescriptor) {
366 // Both accessor and data attributes will be present on O
367 // after defining P.
368 throw new TypeError(
369 "Piscēs: Property descriptors cannot specify both accessor and data attributes.",
370 );
371 } else {
372 // P can be safely defined on O.
373 return reflectDefineProperty(O, P, desc);
374 }
375 } else {
376 // P is not a property descriptor attribute.
377 return reflectDefineProperty(O, P, Desc);
378 }
379 },
380 setPrototypeOf(O, V) {
381 if (V !== null) {
382 // V is not the property descriptor prototype.
383 return false;
384 } else {
385 // V is the property descriptor prototype.
386 return reflectSetPrototypeOf(O, V);
387 }
388 },
389 },
390 ),
391 );
392
393 return {
394 isPropertyDescriptorRecord: ($) =>
395 call(weakSetHas, propertyDescriptorRecords, [$]),
396 toPropertyDescriptor: (Obj) => {
397 if (type(Obj) !== "object") {
398 // The provided value is not an object.
399 throw new TypeError(
400 `Piscēs: Cannot convert primitive to property descriptor: ${O}.`,
401 );
402 } else {
403 // The provided value is an object.
404 const desc = objectCreate(null);
405 if ("enumerable" in Obj) {
406 // An enumerable property is specified.
407 defineOwnProperty(desc, "enumerable", {
408 configurable: true,
409 enumerable: true,
410 value: !!Obj.enumerable,
411 writable: true,
412 });
413 } else {
414 // An enumerable property is not specified.
415 /* do nothing */
416 }
417 if ("configurable" in Obj) {
418 // A configurable property is specified.
419 defineOwnProperty(desc, "configurable", {
420 configurable: true,
421 enumerable: true,
422 value: !!Obj.configurable,
423 writable: true,
424 });
425 } else {
426 // A configurable property is not specified.
427 /* do nothing */
428 }
429 if ("value" in Obj) {
430 // A value property is specified.
431 defineOwnProperty(desc, "value", {
432 configurable: true,
433 enumerable: true,
434 value: Obj.value,
435 writable: true,
436 });
437 } else {
438 // A value property is not specified.
439 /* do nothing */
440 }
441 if ("writable" in Obj) {
442 // A writable property is specified.
443 defineOwnProperty(desc, "writable", {
444 configurable: true,
445 enumerable: true,
446 value: !!Obj.writable,
447 writable: true,
448 });
449 } else {
450 // A writable property is not specified.
451 /* do nothing */
452 }
453 if ("get" in Obj) {
454 // A get property is specified.
455 const getter = Obj.get;
456 if (getter !== UNDEFINED && typeof getter !== "function") {
457 // The getter is not callable.
458 throw new TypeError("Piscēs: Getters must be callable.");
459 } else {
460 // The getter is callable.
461 defineOwnProperty(desc, "get", {
462 configurable: true,
463 enumerable: true,
464 value: getter,
465 writable: true,
466 });
467 }
468 } else {
469 // A get property is not specified.
470 /* do nothing */
471 }
472 if ("set" in Obj) {
473 // A set property is specified.
474 const setter = Obj.set;
475 if (setter !== UNDEFINED && typeof setter !== "function") {
476 // The setter is not callable.
477 throw new TypeError("Piscēs: Setters must be callable.");
478 } else {
479 // The setter is callable.
480 defineOwnProperty(desc, "set", {
481 configurable: true,
482 enumerable: true,
483 value: setter,
484 writable: true,
485 });
486 }
487 } else {
488 // A set property is not specified.
489 /* do nothing */
490 }
491 if (
492 ("get" in desc || "set" in desc) &&
493 ("value" in desc || "writable" in desc)
494 ) {
495 // Both accessor and data attributes have been defined.
496 throw new TypeError(
497 "Piscēs: Property descriptors cannot specify both accessor and data attributes.",
498 );
499 } else {
500 // The property descriptor is valid.
501 const record = new proxyConstructor(
502 desc,
503 propertyDescriptorProxyHandler,
504 );
505 call(weakSetAdd, propertyDescriptorRecords, [record]);
506 return record;
507 }
508 }
509 },
510 };
511 })();
512
513 export const {
514 /**
515 * Returns the primitive value of the provided object per its
516 * `.toString` and `.valueOf` methods.
517 *
518 * If the provided hint is "string", then `.toString` takes
519 * precedence; otherwise, `.valueOf` does.
520 *
521 * Throws an error if both of these methods are not callable or do
522 * not return a primitive.
523 */
524 ordinaryToPrimitive,
525
526 /**
527 * Returns a string function name generated from the provided value
528 * and optional prefix.
529 */
530 toFunctionName,
531
532 /**
533 * Returns the provided value converted to a primitive, or throws if
534 * no such conversion is possible.
535 *
536 * The provided preferred type, if specified, should be "string",
537 * "number", or "default". If the provided input has a
538 * `.[Symbol.toPrimitive]` method, this function will throw rather
539 * than calling that method with a preferred type other than one of
540 * the above.
541 */
542 toPrimitive,
543 } = (() => {
544 const { apply: call } = Reflect;
545 const getSymbolDescription = Object.getOwnPropertyDescriptor(
546 Symbol.prototype,
547 "description",
548 ).get;
549
550 return {
551 ordinaryToPrimitive: (O, hint) => {
552 const methodNames = hint == "string"
553 ? ["toString", "valueOf"]
554 : ["valueOf", "toString"];
555 for (let index = 0; index < methodNames.length; ++index) {
556 const method = O[methodNames[index]];
557 if (typeof method === "function") {
558 // Method is callable.
559 const result = call(method, O, []);
560 if (type(result) !== "object") {
561 // Method returns a primitive.
562 return result;
563 } else {
564 // Method returns an object.
565 continue;
566 }
567 } else {
568 // Method is not callable.
569 continue;
570 }
571 }
572 throw new TypeError(
573 "Piscēs: Unable to convert object to primitive",
574 );
575 },
576 toFunctionName: ($, prefix = undefined) => {
577 const key = toPrimitive($, "string");
578 const name = (() => {
579 if (typeof key === "symbol") {
580 // The provided value is a symbol; format its description.
581 const description = call(getSymbolDescription, key, []);
582 return description === undefined ? "" : `[${description}]`;
583 } else {
584 // The provided value not a symbol; convert it to a string
585 // property key.
586 return `${key}`;
587 }
588 })();
589 return prefix !== undefined ? `${prefix} ${name}` : name;
590 },
591 toPrimitive: ($, preferredType = "default") => {
592 const hint = `${preferredType}`;
593 if (
594 "default" !== hint && "string" !== hint &&
595 "number" !== hint
596 ) {
597 // An invalid preferred type was specified.
598 throw new TypeError(
599 `Piscēs: Invalid preferred type: ${preferredType}.`,
600 );
601 } else if (type($) === "object") {
602 // The provided value is an object.
603 const exoticToPrim = $[TO_PRIMITIVE] ?? undefined;
604 if (exoticToPrim !== undefined) {
605 // The provided value has an exotic primitive conversion
606 // method.
607 if (typeof exoticToPrim !== "function") {
608 // The method is not callable.
609 throw new TypeError(
610 "Piscēs: `.[Symbol.toPrimitive]` was neither nullish nor callable.",
611 );
612 } else {
613 // The method is callable.
614 return call(exoticToPrim, $, [hint]);
615 }
616 } else {
617 // Use the ordinary primitive conversion function.
618 return ordinaryToPrimitive($, hint);
619 }
620 } else {
621 // The provided value is already a primitive.
622 return $;
623 }
624 },
625 };
626 })();
627
628 export const {
629 /**
630 * Returns whether the provided values are the same value.
631 *
632 * ※ This differs from `===` in the cases of nan and zero.
633 */
634 sameValue,
635
636 /**
637 * Returns whether the provided values are either the same value or
638 * both zero (either positive or negative).
639 *
640 * ※ This differs from `===` in the case of nan.
641 */
642 sameValueZero,
643
644 /**
645 * Returns the result of converting the provided value to an index,
646 * or throws an error if it is out of range.
647 */
648 toIndex,
649
650 /**
651 * Returns the result of converting the provided value to a length.
652 */
653 toLength,
654 } = (() => {
655 const { floor, max, min } = Math;
656 const { isNaN: isNan } = Number;
657 const { is } = Object;
658 return {
659 sameValue: (a, b) => is(a, b),
660 sameValueZero: ($1, $2) => {
661 const type1 = type($1);
662 const type2 = type($2);
663 if (type1 !== type2) {
664 // The provided values are not of the same type.
665 return false;
666 } else if (type1 === "number") {
667 // The provided values are numbers; check if they are nan and
668 // use strict equality otherwise.
669 return isNan($1) && isNan($2) || $1 === $2;
670 } else {
671 // The provided values are not numbers; use strict equality.
672 return $1 === $2;
673 }
674 },
675 toIndex: ($) => {
676 const integer = floor($);
677 if (isNan(integer) || integer == 0) {
678 // The value is zero·like.
679 return 0;
680 } else {
681 // The value is not zero·like.
682 const clamped = toLength(integer);
683 if (clamped !== integer) {
684 // Clamping the value changes it.
685 throw new RangeError(`Piscēs: Index out of range: ${$}.`);
686 } else {
687 // The value is within appropriate bounds.
688 return integer;
689 }
690 }
691 },
692 toLength: ($) => {
693 const len = floor($);
694 return isNan(len) || len == 0
695 ? 0
696 : max(min(len, MAXIMUM_SAFE_INTEGRAL_NUMBER), 0);
697 },
698 };
699 })();
700
701 /**
702 * Returns a lowercase string identifying the type of the provided
703 * value.
704 *
705 * This differs from the value of the `typeof` operator only in the
706 * cases of objects and null.
707 */
708 export const type = ($) => {
709 if ($ === null) {
710 // The provided value is null.
711 return "null";
712 } else {
713 // The provided value is not null.
714 const type·of = typeof $;
715 return type·of === "function" ? "object" : type·of;
716 }
717 };
This page took 0.269931 seconds and 5 git commands to generate.