diff --git a/lib/serialize/setsMaps.js b/lib/serialize/setsMaps.js index 28fb4f14..bb814a17 100644 --- a/lib/serialize/setsMaps.js +++ b/lib/serialize/setsMaps.js @@ -11,6 +11,7 @@ const t = require('@babel/types'); // Imports const {createDependency, createAssignment} = require('./records.js'), {weakSets, weakMaps} = require('../shared/internal.js'), + {SET_TYPE, WEAK_SET_TYPE, registerSerializer} = require('./types.js'), {recordIsCircular} = require('./utils.js'); // Exports @@ -19,11 +20,24 @@ const setValues = Set.prototype.values, mapEntries = Map.prototype.entries; module.exports = { - serializeSet(set, record) { - return this.serializeSetLike(set, [...setValues.call(set)], Set, record); + /** + * Trace Set. + * @param {Set} set - Set object + * @param {Object} record - Record + * @returns {number} - Type ID + */ + traceSet(set, record) { + traceSetLike.call(this, set, [...setValues.call(set)], record); + return SET_TYPE; }, - serializeWeakSet(set, record) { + /** + * Trace WeakSet. + * @param {Set} set - Set object + * @param {Object} record - Record + * @returns {number} - Type ID + */ + traceWeakSet(set, record) { const {refs} = weakSets.get(set); const entries = []; for (const ref of refs) { @@ -31,37 +45,8 @@ module.exports = { if (value) entries.push(value); } - return this.serializeSetLike(set, entries, WeakSet, record); - }, - - serializeSetLike(set, entries, ctor, record) { - const {varNode} = record, - varName = varNode.name; - let isCircular = false; - const entryNodes = []; - entries.forEach((val, index) => { - const valRecord = this.serializeValue(val, `${varName}_${index}`, ``); - if (!isCircular && (recordIsCircular(valRecord))) isCircular = true; - - if (isCircular) { - const arr = [valRecord.varNode]; - const memberNode = t.memberExpression(varNode, t.identifier('add')); - const assignmentNode = t.callExpression(memberNode, arr); - const assignment = createAssignment(record, assignmentNode, memberNode, 'object'); - createDependency(assignment, valRecord, arr, 0); - } else { - entryNodes[index] = valRecord.varNode; - createDependency(record, valRecord, entryNodes, index); - } - }); - - const ctorRecord = this.serializeValue(ctor); - const node = t.newExpression( - ctorRecord.varNode, - entryNodes.length > 0 ? [t.arrayExpression(entryNodes)] : [] - ); - createDependency(record, ctorRecord, node, 'callee'); - return this.wrapWithProperties(set, record, node, ctor.prototype); + traceSetLike.call(this, set, entries, record); + return WEAK_SET_TYPE; }, serializeMap(map, record) { @@ -113,3 +98,77 @@ module.exports = { return this.wrapWithProperties(map, record, node, ctor.prototype); } }; + +/** + * Trace Set or WeakSet. + * @this {Object} Serializer + * @param {Set|WeakSet} set - Set/WeakSet object + * @param {Array<*>} entries - Set/WeakSet entries + * @param {Object} record - Record + * @returns {undefined} + */ +function traceSetLike(set, entries, record) { + const entryRecords = entries.map( + (val, index) => this.traceDependency(val, `${record.name}_${index}`, ``, record) + ); + record.extra = {entryRecords}; + this.traceProperties(set, record, undefined); +} + +/** + * Serialize Set. + * @this {Object} Serializer + * @param {Object} record - Record + * @returns {Object} - AST node + */ +function serializeSet(record) { + return serializeSetLike.call(this, record, Set, this.setPrototypeRecord, true); +} +registerSerializer(SET_TYPE, serializeSet); + +/** + * Serialize WeakSet. + * @this {Object} Serializer + * @param {Object} record - Record + * @returns {Object} - AST node + */ +function serializeWeakSet(record) { + return serializeSetLike.call(this, record, WeakSet, this.weakSetPrototypeRecord, false); +} +registerSerializer(WEAK_SET_TYPE, serializeWeakSet); + +/** + * Serialize Set or WeakSet. + * @this {Object} Serializer + * @param {Object} record - Record + * @param {Function} ctor - Constructor - `Set` or `WeakSet` + * @param {Object} protoRecord - Record for prototype + * @param {boolean} isOrdered - `true` if entries are ordered (`true` for Sets, `false` for WeakSets) + * @returns {undefined} + */ +function serializeSetLike(record, ctor, protoRecord, isOrdered) { + const {varNode} = record; + let isCircular = false; + const entryNodes = []; + for (const entryRecord of record.extra.entryRecords) { + if (!isOrdered) { + isCircular = entryRecord.isCircular; + } else if (!isCircular && entryRecord.isCircular) { + isCircular = true; + } + + const entryNode = this.serializeValue(entryRecord); + if (isCircular) { + // `set.add(...)` + this.assignmentNodes.push(t.expressionStatement( + t.callExpression(t.memberExpression(varNode, t.identifier('add')), [entryNode]) + )); + } else { + entryNodes.push(entryNode); + } + } + + const ctorNode = this.traceAndSerializeGlobal(ctor), + node = t.newExpression(ctorNode, entryNodes.length > 0 ? [t.arrayExpression(entryNodes)] : []); + return this.wrapWithProperties(node, record, protoRecord, null); +} diff --git a/lib/serialize/trace.js b/lib/serialize/trace.js index 733b77ad..1553dbd9 100644 --- a/lib/serialize/trace.js +++ b/lib/serialize/trace.js @@ -42,6 +42,8 @@ module.exports = { this.arrayPrototypeRecord = this.traceValue(Array.prototype, null, null); this.regexpPrototypeRecord = this.traceValue(RegExp.prototype, null, null); this.datePrototypeRecord = this.traceValue(Date.prototype, null, null); + this.setPrototypeRecord = this.traceValue(Set.prototype, null, null); + this.weakSetPrototypeRecord = this.traceValue(WeakSet.prototype, null, null); this.urlPrototypeRecord = this.traceValue(URL.prototype, null, null); this.urlSearchParamsPrototypeRecord = this.traceValue(URLSearchParams.prototype, null, null); @@ -144,9 +146,9 @@ module.exports = { if (objType === 'Array') return this.traceArray(val, record); if (objType === 'RegExp') return this.traceRegexp(val, record); if (objType === 'Date') return this.traceDate(val, record); - // if (objType === 'Set') return this.traceSet(val, record); + if (objType === 'Set') return this.traceSet(val, record); // if (objType === 'Map') return this.traceMap(val, record); - // if (objType === 'WeakSet') return this.traceWeakSet(val, record); + if (objType === 'WeakSet') return this.traceWeakSet(val, record); // if (objType === 'WeakMap') return this.traceWeakMap(val, record); if (objType === 'WeakRef') return this.traceWeakRef(val, record); if (objType === 'FinalizationRegistry') return this.traceFinalizationRegistry(val, record); diff --git a/lib/serialize/types.js b/lib/serialize/types.js index 2746f917..d56e0956 100644 --- a/lib/serialize/types.js +++ b/lib/serialize/types.js @@ -12,7 +12,7 @@ const assert = require('simple-invariant'); /* eslint-disable no-bitwise */ const NO_TYPE = 0, - PRIMITIVE_TYPE = 8, + PRIMITIVE_TYPE = 16, STRING_TYPE = PRIMITIVE_TYPE | 0, BOOLEAN_TYPE = PRIMITIVE_TYPE | 1, NUMBER_TYPE = PRIMITIVE_TYPE | 2, @@ -20,15 +20,19 @@ const NO_TYPE = 0, NULL_TYPE = PRIMITIVE_TYPE | 4, UNDEFINED_TYPE = PRIMITIVE_TYPE | 5, NEGATIVE_TYPE = PRIMITIVE_TYPE | 6, // TODO: Should this be a primitive? - OBJECT_TYPE = 16, + OBJECT_TYPE = 32, ARRAY_TYPE = OBJECT_TYPE | 1, REGEXP_TYPE = OBJECT_TYPE | 2, DATE_TYPE = OBJECT_TYPE | 3, - URL_TYPE = OBJECT_TYPE | 4, - URL_SEARCH_PARAMS_TYPE = OBJECT_TYPE | 5, - FUNCTION_TYPE = 32, + SET_TYPE = OBJECT_TYPE | 4, + MAP_TYPE = OBJECT_TYPE | 5, + WEAK_SET_TYPE = OBJECT_TYPE | 6, + WEAK_MAP_TYPE = OBJECT_TYPE | 7, + URL_TYPE = OBJECT_TYPE | 8, + URL_SEARCH_PARAMS_TYPE = OBJECT_TYPE | 9, + FUNCTION_TYPE = 64, METHOD_TYPE = FUNCTION_TYPE | 1, - GLOBAL_TYPE = 64, + GLOBAL_TYPE = 128, GLOBAL_TOP_LEVEL_TYPE = GLOBAL_TYPE | 0, GLOBAL_MODULE_TYPE = GLOBAL_TYPE | 1, GLOBAL_PROPERTY_TYPE = GLOBAL_TYPE | 2, @@ -36,7 +40,7 @@ const NO_TYPE = 0, GLOBAL_GETTER_TYPE = GLOBAL_TYPE | 4, GLOBAL_SETTER_TYPE = GLOBAL_TYPE | 5, GLOBAL_MINUS_INFINITY_TYPE = GLOBAL_TYPE | 6, - SYMBOL_TYPE = 128, + SYMBOL_TYPE = 256, SYMBOL_FOR_TYPE = SYMBOL_TYPE | 1, EXPORT_JS_TYPE = 1, EXPORT_COMMONJS_TYPE = 2, @@ -60,6 +64,10 @@ module.exports = { ARRAY_TYPE, REGEXP_TYPE, DATE_TYPE, + SET_TYPE, + MAP_TYPE, + WEAK_SET_TYPE, + WEAK_MAP_TYPE, URL_TYPE, URL_SEARCH_PARAMS_TYPE, FUNCTION_TYPE, diff --git a/test/sets.test.js b/test/sets.test.js index 3077b119..53435f3b 100644 --- a/test/sets.test.js +++ b/test/sets.test.js @@ -10,7 +10,7 @@ const {itSerializes, itSerializesEqual} = require('./support/index.js'); // Tests -describe.skip('Sets', () => { +describe('Sets', () => { itSerializesEqual('no entries', { in: () => new Set(), out: 'new Set', @@ -89,7 +89,7 @@ describe.skip('Sets', () => { }); }); - describe('set subclass', () => { + describe.skip('set subclass', () => { itSerializes('no entries', { in() { class S extends Set {} @@ -187,7 +187,7 @@ describe.skip('Sets', () => { }); }); -describe.skip('WeakSets', () => { +describe('WeakSets', () => { it('calling `WeakSet()` without `new` throws error', () => { expect(() => WeakSet()).toThrowWithMessage( TypeError, "Class constructor WeakSet cannot be invoked without 'new'" @@ -234,4 +234,25 @@ describe.skip('WeakSets', () => { expect(weakSet.has(weakSet)).toBeTrue(); } }); + + itSerializes('with circular contents followed by non-circular', { + in() { + const weakSet = new WeakSet(); + weakSet.add(weakSet); + const obj = {x: 1}; + weakSet.add(obj); + return {weakSet, obj}; + }, + out: `(()=>{ + const a={x:1}, + b=new WeakSet([a]); + b.add(b); + return{weakSet:b,obj:a} + })()`, + validate({weakSet, obj}) { + expect(weakSet).toBeInstanceOf(WeakSet); + expect(weakSet.has(weakSet)).toBeTrue(); + expect(weakSet.has(obj)).toBeTrue(); + } + }); });