Skip to content

Commit

Permalink
Support Sets and WeakSets
Browse files Browse the repository at this point in the history
  • Loading branch information
overlookmotel committed Sep 28, 2023
1 parent fcf9ed5 commit c346e2c
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 46 deletions.
127 changes: 93 additions & 34 deletions lib/serialize/setsMaps.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,49 +20,33 @@ 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) {
const value = ref.deref();
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}`, `<Set value ${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) {
Expand Down Expand Up @@ -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}`, `<Set value ${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);
}
6 changes: 4 additions & 2 deletions lib/serialize/trace.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand Down
22 changes: 15 additions & 7 deletions lib/serialize/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,35 @@ 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,
BIGINT_TYPE = PRIMITIVE_TYPE | 3,
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,
GLOBAL_PROTOTYPE_TYPE = GLOBAL_TYPE | 3,
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,
Expand All @@ -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,
Expand Down
27 changes: 24 additions & 3 deletions test/sets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -89,7 +89,7 @@ describe.skip('Sets', () => {
});
});

describe('set subclass', () => {
describe.skip('set subclass', () => {
itSerializes('no entries', {
in() {
class S extends Set {}
Expand Down Expand Up @@ -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'"
Expand Down Expand Up @@ -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();
}
});
});

0 comments on commit c346e2c

Please sign in to comment.