From 6ca5652d0750d93642d0655f2c8044eb42e55f7f Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Sat, 30 Sep 2023 16:58:40 +0100 Subject: [PATCH] Support boxed primitives --- lib/serialize/boxed.js | 223 ++++++++++++++++++++++++++++++----------- lib/serialize/trace.js | 15 ++- lib/serialize/types.js | 10 ++ test/boxed.test.js | 43 +++++--- 4 files changed, 216 insertions(+), 75 deletions(-) diff --git a/lib/serialize/boxed.js b/lib/serialize/boxed.js index 55690db2..ce6b1e53 100644 --- a/lib/serialize/boxed.js +++ b/lib/serialize/boxed.js @@ -6,82 +6,189 @@ 'use strict'; // Modules -const t = require('@babel/types'); +const t = require('@babel/types'), + upperFirst = require('lodash/upperFirst'), + {isNumber} = require('is-it-type'); // Imports -const {createDependency} = require('./records.js'), - {isNumberKey} = require('./utils.js'); +const { + BOXED_STRING_TYPE, BOXED_BOOLEAN_TYPE, BOXED_NUMBER_TYPE, BOXED_BIGINT_TYPE, BOXED_SYMBOL_TYPE, + registerSerializer +} = require('./types.js'); // Exports const stringToString = String.prototype.toString, booleanValueOf = Boolean.prototype.valueOf, numberValueOf = Number.prototype.valueOf, + bigIntValueOf = BigInt.prototype.valueOf, symbolToPrimitive = Symbol.prototype[Symbol.toPrimitive]; module.exports = { - serializeBoxedString(str, record) { - const StringRecord = this.serializeValue(String), - len = str.length; - const node = t.newExpression( - StringRecord.varNode, - len > 0 ? [t.stringLiteral(stringToString.call(str))] : [] - ); - createDependency(record, StringRecord, node, 'callee'); - - return this.wrapWithProperties( - str, record, node, String.prototype, undefined, - (key) => { - if (key === 'length') return true; - // All numerical keys can be skipped, even ones above max safe integer key as boxed strings - // only allow writing to numerical keys within bounds of string length - if (!isNumberKey(key)) return false; - return key * 1 < len; - } - ); + /** + * Trace boxed String. + * @param {Object} str - Boxed String + * @param {Object} record - Record + * @returns {number} - Type ID + */ + traceBoxedString(str, record) { + const val = stringToString.call(str), + len = val.length; + // Skip `length` property and integer-keyed properties representing characters of the string. + // These properties are all non-writable and non-configurable, so cannot be changed. + this.traceProperties(str, record, key => key === 'length' || (isNumber(key) && key < len)); + record.extra = {valRecord: this.traceValue(val, null, null), len}; + record.name = `boxed${upperFirst(val)}`; + return BOXED_STRING_TYPE; }, - serializeBoxedBoolean(bool, record) { - const BooleanRecord = this.serializeValue(Boolean); - const node = t.newExpression( - BooleanRecord.varNode, - booleanValueOf.call(bool) ? [t.numericLiteral(1)] : [] - ); - createDependency(record, BooleanRecord, node, 'callee'); - return this.wrapWithProperties(bool, record, node, Boolean.prototype); + /** + * Trace boxed Boolean. + * @param {Object} bool - Boxed Boolean + * @param {Object} record - Record + * @returns {number} - Type ID + */ + traceBoxedBoolean(bool, record) { + const val = booleanValueOf.call(bool); + this.traceProperties(bool, record, null); + record.extra = {val}; + record.name = `boxed${upperFirst(`${val}`)}`; + return BOXED_BOOLEAN_TYPE; }, - serializeBoxedNumber(num, record) { - const NumberRecord = this.serializeValue(Number), - unwrappedNum = numberValueOf.call(num), - numRecord = this.serializeValue(unwrappedNum); - const node = t.newExpression( - NumberRecord.varNode, - !Object.is(unwrappedNum, 0) ? [numRecord.varNode] : [] - ); - createDependency(record, NumberRecord, node, 'callee'); - if (numRecord.node) createDependency(record, numRecord, node.arguments, 0); - return this.wrapWithProperties(num, record, node, Number.prototype); + /** + * Trace boxed Number. + * @param {Object} num - Boxed Number + * @param {Object} record - Record + * @returns {number} - Type ID + */ + traceBoxedNumber(num, record) { + const val = numberValueOf.call(num); + this.traceProperties(num, record, null); + record.extra = {val}; + record.name = `boxed${val < 0 || Object.is(val, -0) ? 'Minus' : ''}${val}`; + return BOXED_NUMBER_TYPE; }, - serializeBoxedBigInt(bigInt, record) { - const ObjectRecord = this.serializeValue(Object); - const node = t.callExpression(ObjectRecord.varNode, [this.serializeValue(bigInt)]); - createDependency(record, ObjectRecord, node, 'callee'); - return this.wrapWithProperties(bigInt, record, node, BigInt.prototype); + /** + * Trace boxed BigInt. + * @param {Object} bigInt - Boxed BigInt + * @param {Object} record - Record + * @returns {number} - Type ID + */ + traceBoxedBigInt(bigInt, record) { + const val = bigIntValueOf.call(bigInt), + valRecord = this.traceValue(val, null, null); + this.traceProperties(bigInt, record, null); + record.extra = {valRecord}; + record.name = `boxed${upperFirst(valRecord.name)}`; + return BOXED_BIGINT_TYPE; }, - serializeBoxedSymbol(symbol, record) { - const ObjectRecord = this.serializeValue(Object), - unwrappedSymbol = symbolToPrimitive.call(symbol), - symbolRecord = this.serializeValue(unwrappedSymbol, `${record.varNode.name}Unboxed`); - - const {description} = unwrappedSymbol; - if (description) record.varNode.name = `${description}Boxed`; - - const node = t.callExpression(ObjectRecord.varNode, [symbolRecord.varNode]); - createDependency(record, ObjectRecord, node, 'callee'); - createDependency(record, symbolRecord, node.arguments, 0); - return this.wrapWithProperties(symbol, record, node, Symbol.prototype); + /** + * Trace boxed Symbol. + * @param {Object} symbol - Boxed Symbol + * @param {Object} record - Record + * @returns {number} - Type ID + */ + traceBoxedSymbol(symbol, record) { + const val = symbolToPrimitive.call(symbol), + valRecord = this.traceDependency(val, `${record.name}Unboxed`, '', record); + this.traceProperties(symbol, record, null); + record.extra = {valRecord}; + record.name = `boxed${upperFirst(valRecord.name)}`; + return BOXED_SYMBOL_TYPE; } }; + +/** + * Serialize boxed String. + * @this {Object} Serializer + * @param {Object} record - Record + * @param {Object} record.extra - Extra props object + * @param {Object} record.extra.valRecord - Record for boxed value + * @param {number} record.extra.len - String length + * @returns {Object} - AST node + */ +function serializeBoxedString(record) { + // `new String('xyz')` or `new String` (for empty string) + const node = t.newExpression( + this.traceAndSerializeGlobal(String), + record.extra.len > 0 ? [this.serializeValue(record.extra.valRecord)] : [] + ); + return this.wrapWithProperties(node, record, this.stringPrototypeRecord, null); +} +registerSerializer(BOXED_STRING_TYPE, serializeBoxedString); + +/** + * Serialize boxed Boolean. + * @this {Object} Serializer + * @param {Object} record - Record + * @param {Object} record.extra - Extra props object + * @param {boolean} record.extra.val - `true` or `false` + * @returns {Object} - AST node + */ +function serializeBoxedBoolean(record) { + // `new Boolean` (for false) or `new Boolean(1)` (for true) + const node = t.newExpression( + this.traceAndSerializeGlobal(Boolean), + record.extra.val ? [this.traceAndSerializeGlobal(1)] : [] + ); + return this.wrapWithProperties(node, record, this.booleanPrototypeRecord, null); +} +registerSerializer(BOXED_BOOLEAN_TYPE, serializeBoxedBoolean); + +/** + * Serialize boxed Number. + * @this {Object} Serializer + * @param {Object} record - Record + * @param {Object} record.extra - Extra props object + * @param {number} record.extra.val - Value + * @returns {Object} - AST node + */ +function serializeBoxedNumber(record) { + // `new Number(123)` or `new Number` (for 0) + const {val} = record.extra, + node = t.newExpression( + this.traceAndSerializeGlobal(Number), + Object.is(val, 0) ? [] : [this.traceAndSerializeGlobal(val)] + ); + return this.wrapWithProperties(node, record, this.numberPrototypeRecord, null); +} +registerSerializer(BOXED_NUMBER_TYPE, serializeBoxedNumber); + +/** + * Serialize boxed BigInt. + * @this {Object} Serializer + * @param {Object} record - Record + * @param {Object} record.extra - Extra props object + * @param {Object} record.extra.valRecord - Record for boxed value + * @returns {Object} - AST node + */ +function serializeBoxedBigInt(record) { + // `Object(123n)` + const node = t.callExpression( + this.traceAndSerializeGlobal(Object), + [this.serializeValue(record.extra.valRecord)] + ); + return this.wrapWithProperties(node, record, this.bigIntPrototypeRecord, null); +} +registerSerializer(BOXED_BIGINT_TYPE, serializeBoxedBigInt); + +/** + * Serialize boxed Symbol. + * @this {Object} Serializer + * @param {Object} record - Record + * @param {Object} record.extra - Extra props object + * @param {Object} record.extra.valRecord - Record for boxed value + * @returns {Object} - AST node + */ +function serializeBoxedSymbol(record) { + // `Object(Symbol('x'))` + const node = t.callExpression( + this.traceAndSerializeGlobal(Object), + [this.serializeValue(record.extra.valRecord)] + ); + return this.wrapWithProperties(node, record, this.symbolPrototypeRecord, null); +} +registerSerializer(BOXED_SYMBOL_TYPE, serializeBoxedSymbol); diff --git a/lib/serialize/trace.js b/lib/serialize/trace.js index ac61b233..6e5edf1e 100644 --- a/lib/serialize/trace.js +++ b/lib/serialize/trace.js @@ -48,6 +48,11 @@ module.exports = { this.weakMapPrototypeRecord = this.traceValue(WeakMap.prototype, null, null); this.urlPrototypeRecord = this.traceValue(URL.prototype, null, null); this.urlSearchParamsPrototypeRecord = this.traceValue(URLSearchParams.prototype, null, null); + this.stringPrototypeRecord = this.traceValue(String.prototype, null, null); + this.booleanPrototypeRecord = this.traceValue(Boolean.prototype, null, null); + this.numberPrototypeRecord = this.traceValue(Number.prototype, null, null); + this.bigIntPrototypeRecord = this.traceValue(BigInt.prototype, null, null); + this.symbolPrototypeRecord = this.traceValue(Symbol.prototype, null, null); this.minusZeroRecord = null; @@ -157,11 +162,11 @@ module.exports = { // if (typedArrayRegex.test(objType)) return this.traceBuffer(val, objType, record); // if (objType === 'ArrayBuffer') return this.traceArrayBuffer(val, record); // if (objType === 'SharedArrayBuffer') return this.traceSharedArrayBuffer(val, record); - // if (objType === 'String') return this.traceBoxedString(val, record); - // if (objType === 'Boolean') return this.traceBoxedBoolean(val, record); - // if (objType === 'Number') return this.traceBoxedNumber(val, record); - // if (objType === 'BigInt') return this.traceBoxedBigInt(val, record); - // if (objType === 'Symbol') return this.traceBoxedSymbol(val, record); + if (objType === 'String') return this.traceBoxedString(val, record); + if (objType === 'Boolean') return this.traceBoxedBoolean(val, record); + if (objType === 'Number') return this.traceBoxedNumber(val, record); + if (objType === 'BigInt') return this.traceBoxedBigInt(val, record); + if (objType === 'Symbol') return this.traceBoxedSymbol(val, record); // if (objType === 'Arguments') return this.traceArguments(val, record); throw new Error(`Cannot serialize ${objType}s`); }, diff --git a/lib/serialize/types.js b/lib/serialize/types.js index d56e0956..2e2958a6 100644 --- a/lib/serialize/types.js +++ b/lib/serialize/types.js @@ -30,6 +30,11 @@ const NO_TYPE = 0, WEAK_MAP_TYPE = OBJECT_TYPE | 7, URL_TYPE = OBJECT_TYPE | 8, URL_SEARCH_PARAMS_TYPE = OBJECT_TYPE | 9, + BOXED_STRING_TYPE = OBJECT_TYPE | 10, + BOXED_BOOLEAN_TYPE = OBJECT_TYPE | 11, + BOXED_NUMBER_TYPE = OBJECT_TYPE | 12, + BOXED_BIGINT_TYPE = OBJECT_TYPE | 13, + BOXED_SYMBOL_TYPE = OBJECT_TYPE | 14, FUNCTION_TYPE = 64, METHOD_TYPE = FUNCTION_TYPE | 1, GLOBAL_TYPE = 128, @@ -70,6 +75,11 @@ module.exports = { WEAK_MAP_TYPE, URL_TYPE, URL_SEARCH_PARAMS_TYPE, + BOXED_STRING_TYPE, + BOXED_BOOLEAN_TYPE, + BOXED_NUMBER_TYPE, + BOXED_BIGINT_TYPE, + BOXED_SYMBOL_TYPE, FUNCTION_TYPE, METHOD_TYPE, GLOBAL_TYPE, diff --git a/test/boxed.test.js b/test/boxed.test.js index 6d2d6915..85827427 100644 --- a/test/boxed.test.js +++ b/test/boxed.test.js @@ -12,7 +12,7 @@ const {itSerializes, itSerializesEqual} = require('./support/index.js'); // Tests -describe.skip('Boxed Strings', () => { +describe('Boxed Strings', () => { itSerializes('non-empty string', { in: () => new String('abc'), out: 'new String("abc")', @@ -52,7 +52,7 @@ describe.skip('Boxed Strings', () => { } }); - itSerializes('with `toString` property', { + itSerializes.skip('with `toString` property', { in() { const str = new String('abc'); str.toString = () => 'e'; @@ -71,7 +71,7 @@ describe.skip('Boxed Strings', () => { } }); - itSerializes('String subclass', { + itSerializes.skip('String subclass', { in() { class S extends String {} return new S('abc'); @@ -99,7 +99,7 @@ describe.skip('Boxed Strings', () => { }); }); -describe.skip('Boxed Booleans', () => { +describe('Boxed Booleans', () => { itSerializesEqual('true', { in: () => new Boolean(true), out: 'new Boolean(1)', @@ -120,7 +120,7 @@ describe.skip('Boxed Booleans', () => { } }); - itSerializes('with `valueOf` property', { + itSerializes.skip('with `valueOf` property', { in() { const bool = new Boolean(true); bool.valueOf = () => false; @@ -138,7 +138,7 @@ describe.skip('Boxed Booleans', () => { } }); - itSerializesEqual('Boolean subclass', { + itSerializesEqual.skip('Boolean subclass', { in() { class B extends Boolean {} return new B(true); @@ -165,7 +165,7 @@ describe.skip('Boxed Booleans', () => { }); }); -describe.skip('Boxed Numbers', () => { +describe('Boxed Numbers', () => { itSerializesEqual('positive integer', { in: () => new Number(1), out: 'new Number(1)', @@ -256,7 +256,7 @@ describe.skip('Boxed Numbers', () => { } }); - itSerializes('with `valueOf` property', { + itSerializes.skip('with `valueOf` property', { in() { const num = new Number(1); num.valueOf = () => 2; @@ -274,7 +274,7 @@ describe.skip('Boxed Numbers', () => { } }); - itSerializesEqual('Number subclass', { + itSerializesEqual.skip('Number subclass', { in() { class N extends Number {} return new N(1); @@ -301,7 +301,7 @@ describe.skip('Boxed Numbers', () => { }); }); -describe.skip('Boxed BigInts', () => { +describe('Boxed BigInts', () => { itSerializesEqual('zero', { in: () => Object(BigInt(0)), out: 'Object(0n)', @@ -357,7 +357,26 @@ describe.skip('Boxed BigInts', () => { } }); - itSerializesEqual('BigInt subclass', { + itSerializes.skip('with `valueOf` property', { + in() { + const bigInt = Object(1n); + bigInt.valueOf = () => 2n; + return bigInt; + }, + out: 'Object.assign(Object(1n),{valueOf:(0,()=>2n)})', + validate(bigInt) { + expect(typeof bigInt).toBe('object'); + expect(typeof bigInt.valueOf()).toBe('bigint'); + expect(bigInt).toHavePrototype(BigInt.prototype); + expect(BigInt.prototype.valueOf.call(bigInt)).toBe(1n); + expect(BigInt(bigInt)).toBe(2n); + expect(bigInt.valueOf).toBeFunction(); + expect(bigInt).toHaveDescriptorModifiersFor('valueOf', true, true, true); + expect(bigInt.valueOf()).toBe(2n); + } + }); + + itSerializesEqual.skip('BigInt subclass', { in() { class B extends BigInt {} const bigInt = Object(BigInt(100)); @@ -388,7 +407,7 @@ describe.skip('Boxed BigInts', () => { }); }); -describe.skip('Boxed Symbols', () => { +describe('Boxed Symbols', () => { itSerializesEqual('without description', { in: () => Object(Symbol()), // eslint-disable-line symbol-description out: 'Object(Symbol())',