/* eslint-disable id-length */ /* eslint-disable prefer-regex-literals */ /* eslint-disable require-unicode-regexp */ /* eslint-disable no-sparse-arrays */ /* eslint-disable unicorn/new-for-builtins */ /* eslint-disable no-new-wrappers */ import { assertNoPoisoning, restoreGlobals } from '@fast-check/poisoning' import { test } from '@fast-check/vitest' import { afterEach, describe, expect } from 'vitest' import type { UnevalOptions } from './index.ts' import { anythingArb } from './testing/arbs.ts' // This has to happen before `uneval` is imported because well-known symbols are // collected its module runs. const evilSymbol = Symbol(`evil`) // @ts-expect-error For testing. Symbol[`xss`] = evilSymbol const { default: uneval } = await import(`./testing/package.ts`) // @ts-expect-error For testing. delete Symbol[`xss`] const ignoredRootRegex = /^(?:console|__vitest_.*|Person|__SEROVAL_REFS__)$/u const poisoningAfterEach = () => { try { assertNoPoisoning({ ignoredRootRegex }) } catch (error: unknown) { restoreGlobals({ ignoredRootRegex }) throw error } } afterEach(poisoningAfterEach) const isComparison = !!process.env.UNEVAL_COMPARISON type Case = { todo?: boolean name: string value: unknown options?: UnevalOptions expected: | { source: string roundtrips?: boolean } | { error: true | string } compare?: boolean } const customBoolean: UnevalOptions[`custom`] = value => typeof value === `boolean` ? String(value) : undefined const customNumber: UnevalOptions[`custom`] = value => typeof value === `number` && Number.isInteger(value) ? `${value}.0` : undefined const customString: UnevalOptions[`custom`] = value => typeof value === `string` ? JSON.stringify(value).replaceAll(`"`, `'`) : undefined const customBigInt: UnevalOptions[`custom`] = value => typeof value === `bigint` ? `BigInt("${value}")` : undefined const customSymbol: UnevalOptions[`custom`] = (value, uneval) => typeof value === `symbol` ? `Symbol(${uneval(value.description)})` : undefined const cases: Record = { undefined: [ { name: `undefined`, value: undefined, expected: { source: `void 0` }, }, { name: `custom undefined`, value: undefined, options: { custom: value => (value === undefined ? `undefined` : undefined), }, expected: { source: `undefined` }, }, { name: `omit undefined from array`, value: [1, undefined, 3], options: { custom: value => (value === undefined ? null : undefined) }, expected: { source: `[1,,3]`, roundtrips: false, }, }, ], null: [ { name: `null`, value: null, expected: { source: `null` }, }, { name: `custom null`, value: null, options: { custom: value => (value === null ? `JSON.parse("null")` : undefined), }, expected: { source: `JSON.parse("null")` }, }, { name: `omit null from array`, value: [1, null, 3], options: { custom: value => (value === null ? null : undefined) }, expected: { source: `[1,,3]`, roundtrips: false, }, }, ], boolean: [ { name: `false`, value: false, expected: { source: `!1` } }, { name: `boxed false`, value: new Boolean(false), expected: { source: `Object(!1)` }, }, { name: `true`, value: true, expected: { source: `!0` } }, { name: `boxed true`, value: new Boolean(true), expected: { source: `Object(!0)` }, }, { name: `polluted boxed boolean`, value: (() => { const value = new Boolean() value.valueOf = () => // @ts-expect-error Purposefully using the wrong type. ``, expected: { source: `"<\\u002fscript>"` }, }, { name: `string with multiple closing script tags`, value: ` sdf sdfsfd sdf `, expected: { source: `" <\\u002fscript> sdf <\\u002fscript> sdfsfd <\\u002fscript> sdf <\\u002fscript>"`, }, }, { name: `boxed string with closing script tag`, value: new String(``), expected: { source: `Object("<\\u002fscript>")` }, }, { name: `string with capitalized closing script tag`, value: ``, expected: { source: `"<\\u002fSCRIPT>"` }, }, { name: `boxed string with capitalized closing script tag`, value: new String(``), expected: { source: `Object("<\\u002fSCRIPT>")` }, }, { name: `string with mixed capitalization closing script tag`, value: ``, expected: { source: `"<\\u002fsCrIpT>"` }, }, { name: `boxed string with mixed capitalization capitalized closing script tag`, value: new String(``), expected: { source: `Object("<\\u002fsCrIpT>")` }, }, { name: `string with closing script tag with whitespace`, value: ``, expected: { source: `"<\\u002fscript >"` }, }, { name: `boxed string with closing script tag with whitespace`, value: new String(``), expected: { source: `Object("<\\u002fscript >")` }, }, { name: `string with unpaired low surrogate`, value: `\uDC00`, expected: { source: `"\\udc00"` }, }, { name: `boxed string with unpaired low surrogate`, value: new String(`\uDC00`), expected: { source: `Object("\\udc00")` }, }, { name: `string with unpaired high surrogate`, value: `\uD800`, expected: { source: `"\\ud800"` }, }, { name: `boxed string with unpaired high surrogate`, value: new String(`\uD800`), expected: { source: `Object("\\ud800")` }, }, { name: `string with unpaired low surrogate in middle`, value: `a\uDC00b`, expected: { source: `"a\\udc00b"` }, }, { name: `boxed string with unpaired low surrogate in middle`, value: new String(`a\uDC00b`), expected: { source: `Object("a\\udc00b")` }, }, { name: `string with unpaired high surrogate in middle`, value: `a\uD800b`, expected: { source: `"a\\ud800b"` }, }, { name: `boxed string with unpaired high surrogate in middle`, value: new String(`a\uD800b`), expected: { source: `Object("a\\ud800b")` }, }, { name: `string with multiple unpaired surrogates`, value: `\uD800\uDBFF`, expected: { source: `"\\ud800\\udbff"` }, }, { name: `boxed string with multiple unpaired surrogates`, value: new String(`\uD800\uDBFF`), expected: { source: `Object("\\ud800\\udbff")` }, }, { name: `string with surrogate pair`, value: `\uD83D\uDE00`, expected: { source: `"😀"` }, }, { name: `boxed string with surrogate pair`, value: new String(`\uD83D\uDE00`), expected: { source: `Object("😀")` }, }, { name: `polluted boxed string`, value: (() => { const value = new String(42) value.valueOf = () => ``), expected: { source: `Symbol.for("<\\u002fscript>")` }, }, { name: `unique symbol`, // eslint-disable-next-line symbol-description value: Symbol(), expected: { error: `Unsupported: Symbol`, }, }, { name: `unique symbol with description`, value: Symbol(`howdy`), expected: { error: `Unsupported: Symbol`, }, }, { name: `polluted symbol`, value: evilSymbol, expected: { error: `Unsupported: Symbol`, }, compare: false, }, { name: `custom symbol`, value: Symbol(`hi`), options: { custom: customSymbol }, expected: { source: `Symbol("hi")`, roundtrips: false, }, }, { name: `custom string does not affect symbol`, value: Symbol.for(`hi`), options: { custom: customString }, expected: { source: `Symbol.for("hi")` }, }, { name: `custom string does not affect symbol when string is sibling`, value: [Symbol.for(`hi`), `hi`], options: { custom: customString }, expected: { source: `[Symbol.for("hi"),'hi']` }, }, { name: `omit string does not affect symbol`, value: Symbol.for(`hi`), options: { custom: value => (value === `hi` ? null : undefined) }, expected: { source: `Symbol.for("hi")` }, }, { name: `omit string does not affect symbol when string is sibling`, value: [Symbol.for(`hi`), `hi`], options: { custom: value => (value === `hi` ? null : undefined) }, expected: { source: `[Symbol.for("hi"),,]`, roundtrips: false, }, }, ], Array: [ { name: `empty array`, value: [], expected: { source: `[]` }, }, { name: `non-empty array`, value: [1, 2, 3], expected: { source: `[1,2,3]` }, }, { name: `sparse array with all empty slots`, value: [, , ,], expected: { source: `[,,,]` }, }, { name: `sparse array with leading empty slots`, value: [, 1, 2, 3], expected: { source: `[,1,2,3]` }, }, { name: `sparse array with trailing empty slots`, value: [1, 2, 3, ,], expected: { source: `[1,2,3,,]` }, }, { name: `sparse array with leading and trailing empty slots`, value: [, 1, 2, 3, ,], expected: { source: `[,1,2,3,,]` }, }, { name: `sparse array with middle empty slots`, value: [1, , , , , 2], expected: { source: `[1,,,,,2]` }, }, { name: `small empty sparse array`, value: Array(27), expected: { source: `Array(27)` }, }, { name: `small sparse array with trailing empty slots`, value: (() => { const array: unknown[] = Array(27) array[4] = 42 return array })(), expected: { source: `[,,,,42,,,,,,,,,,,,,,,,,,,,,,,]` }, }, { name: `small sparse array with no trailing empty slots`, value: (() => { const array: unknown[] = Array(27) array[26] = 42 return array })(), expected: { source: `Object.assign([],{26:42})` }, }, { name: `medium sparse array with trailing empty slots`, value: (() => { const array: unknown[] = Array(50) array[4] = 42 return array })(), expected: { source: `Object.assign(Array(50),{4:42})` }, }, { name: `medium sparse array with no trailing empty slots`, value: (() => { const array: unknown[] = Array(50) array[49] = 42 return array })(), expected: { source: `Object.assign([],{49:42})` }, }, { name: `large empty sparse array`, value: Array(1000), expected: { source: `Array(1000)` }, }, { name: `large sparse array with non-trailing value`, value: (() => { const array: unknown[] = Array(100) array[4] = 42 return array })(), expected: { source: `Object.assign(Array(100),{4:42})` }, }, { name: `large sparse array with trailing value`, value: (() => { const array: unknown[] = Array(100) array[99] = 42 return array })(), expected: { source: `Object.assign([],{99:42})` }, }, { name: `extremely sparse array with non-trailing value`, value: (() => { const value: unknown[] = Array(100) value[50] = 1 return value })(), expected: { source: `Object.assign(Array(100),{50:1})` }, }, { name: `extremely sparse array with trailing value`, value: (() => { const value: unknown[] = [] value[50] = 1 return value })(), expected: { source: `Object.assign([],{50:1})` }, }, { name: `extremely sparse array with multiple values`, value: (() => { const value: unknown[] = [] value[10] = `a` value[50] = `b` value[1000] = `c` return value })(), expected: { source: `Object.assign([],{10:"a",50:"b",1000:"c"})` }, }, { name: `polluted array`, value: (() => { const value = Object.create(Array.prototype) as Record< PropertyKey, unknown > value[Symbol.toStringTag] = `Array` Object.defineProperty(value, `length`, { value: Number.MAX_SAFE_INTEGER, }) return value })(), expected: { source: `Array(9007199254740991)`, roundtrips: false, }, }, { name: `custom array`, value: [1, 2, 3], options: { custom: (value, uneval) => Array.isArray(value) ? `[${value.map(uneval).join(`, `)}]` : undefined, }, expected: { source: `[1, 2, 3]` }, }, { name: `custom element affects array`, value: [1, 2, 3], options: { custom: customNumber }, expected: { source: `[1.0,2.0,3.0]` }, }, { name: `omit array element`, value: [1, 2, 3], options: { custom: value => (value === 2 ? null : undefined) }, expected: { source: `[1,,3]`, roundtrips: false, }, }, { name: `omit trailing array element`, value: [1, 2, 3], options: { custom: value => (value === 3 ? null : undefined) }, expected: { source: `[1,2,,]`, roundtrips: false, }, }, { name: `omit element from sparse array using Object.assign representation`, value: (() => { const array: unknown[] = Array(100) array[50] = 42 return array })(), options: { custom: value => (value === 42 ? null : undefined) }, expected: { source: `Array(100)`, roundtrips: false, }, }, ], Arguments: [ { name: `empty arguments`, value: (function () { // eslint-disable-next-line prefer-rest-params return arguments })(), expected: { source: `(function(){return arguments})()` }, }, { name: `arguments with primitives`, value: (function () { // eslint-disable-next-line prefer-rest-params return arguments // @ts-expect-error For testing })(1, `hello`, true), expected: { source: `(function(){return arguments})(1,"hello",!0)` }, }, { name: `nested arguments`, value: (function () { // eslint-disable-next-line prefer-rest-params return arguments })( // @ts-expect-error For testing (function () { // eslint-disable-next-line prefer-rest-params return arguments // @ts-expect-error For testing })(10, 20), `hello`, ), expected: { source: `(function(){return arguments})((function(){return arguments})(10,20),"hello")`, }, }, { name: `custom arguments`, value: (function () { // eslint-disable-next-line prefer-rest-params return arguments // @ts-expect-error For testing })(1, 2, 3), options: { custom: value => Object.prototype.toString.call(value) === `[object Arguments]` ? `customArgs` : undefined, }, expected: { source: `customArgs`, roundtrips: false }, }, { name: `custom value affects arguments`, value: (function () { // eslint-disable-next-line prefer-rest-params return arguments // @ts-expect-error For testing })(1, 2, 3), options: { custom: customNumber }, expected: { source: `(function(){return arguments})(1.0,2.0,3.0)`, }, }, { name: `omit argument value`, value: (function () { // eslint-disable-next-line prefer-rest-params return arguments // @ts-expect-error For testing })(1, 2, 3), options: { custom: value => (value === 2 ? null : undefined) }, expected: { source: `(a=>(delete a[1],a))((function(){return arguments})(1,0,3))`, roundtrips: false, }, }, ], Object: [ { name: `empty object`, value: {}, expected: { source: `{}` }, }, { name: `object with single character property`, value: { a: 2 }, expected: { source: `{a:2}` }, }, { name: `object with string property`, value: { ab: 2 }, expected: { source: `{ab:2}` }, }, { name: `object with string with spaces property`, value: { 'a b c': 2 }, expected: { source: `{"a b c":2}` }, }, { name: `object with string with underscores property`, value: { __a__: 2 }, expected: { source: `{__a__:2}` }, }, { name: `object with string with dollar signs property`, value: { $a$: 2 }, expected: { source: `{$a$:2}` }, }, { name: `object with closing script tag property`, value: { [``]: 2 }, expected: { source: `{"<\\u002fscript>":2}` }, }, { name: `object with zero property`, value: { 0: 2 }, expected: { source: `{0:2}` }, }, { name: `object with multiple zeros property`, value: { '00': 2 }, expected: { source: `{"00":2}` }, }, { name: `object with integer property`, value: { 1: 2 }, expected: { source: `{1:2}` }, }, { name: `object with integer property with matching value`, value: { 1: 1 }, expected: { source: `{1:1}` }, }, { name: `object with string positive integer property`, value: { '1': 2 }, expected: { source: `{1:2}` }, }, { name: `object with string negative integer property`, value: { '-1': 2 }, expected: { source: `{"-1":2}` }, }, { name: `object with string positive decimal property`, value: { '1.2': 2 }, expected: { source: `{"1.2":2}` }, }, { name: `object with string negative decimal property`, value: { '-1.2': 2 }, expected: { source: `{"-1.2":2}` }, }, { name: `object with large safe integer property`, value: { 1_000_000_000_000_000: 2 }, expected: { source: `{1000000000000000:2}` }, }, { name: `object with non-safe integer property`, value: { '10000000000000000000000000000000000000000': 2 }, expected: { source: `{"10000000000000000000000000000000000000000":2}` }, }, { name: `object with symbol property`, value: { [Symbol.toStringTag]: `hi` }, expected: { source: `{[Symbol.toStringTag]:"hi"}` }, }, { name: `null prototype empty object`, value: Object.create(null), expected: { source: `Object.setPrototypeOf({},null)` }, }, { name: `null prototype non-empty object`, value: Object.setPrototypeOf({ a: 2 }, null), expected: { source: `Object.setPrototypeOf({a:2},null)` }, }, { name: `object with null __proto__ property`, value: { __proto__: null }, expected: { source: `Object.setPrototypeOf({},null)` }, }, { name: `object with null own __proto__ property`, value: Object.defineProperty({}, `__proto__`, { value: null, configurable: true, enumerable: true, writable: true, }), expected: { source: `{["__proto__"]:null}` }, }, { name: `object with own constructor property`, value: { constructor: { name: `RegExp` } }, expected: { source: `{constructor:{name:"RegExp"}}` }, }, { name: `object with copy of default prototype`, value: Object.create( Object.fromEntries( Object.getOwnPropertyNames(Object.prototype).map(key => [ key, (Object.prototype as Record)[key], ]), ), ), expected: { source: `{}`, roundtrips: false, }, }, { name: `object with near copy of default prototype`, value: Object.create( Object.fromEntries( Object.getOwnPropertyNames(Object.prototype) .slice(1) .map(key => { const value = (Object.prototype as Record)[key] return [key, typeof value === `function` ? `function` : value] }), ), ), expected: { source: `Object.setPrototypeOf({},{__defineGetter__:"function",__defineSetter__:"function",hasOwnProperty:"function",__lookupGetter__:"function",__lookupSetter__:"function",isPrototypeOf:"function",propertyIsEnumerable:"function",toString:"function",valueOf:"function",["__proto__"]:null,toLocaleString:"function"})`, roundtrips: false, }, }, { name: `non-enumerable non-configurable non-writable property`, value: Object.defineProperty({}, `a`, { value: 1 }), expected: { source: `Object.defineProperties({},{a:{value:1}})` }, }, { name: `enumerable non-configurable non-writable property`, value: Object.defineProperty({}, `a`, { value: 1, enumerable: true }), expected: { source: `Object.defineProperties({},{a:{value:1,enumerable:!0}})`, }, }, { name: `non-enumerable configurable non-writable property`, value: Object.defineProperty({}, `a`, { value: 1, configurable: true }), expected: { source: `Object.defineProperties({},{a:{value:1,configurable:!0}})`, }, }, { name: `non-enumerable non-configurable writable property`, value: Object.defineProperty({}, `a`, { value: 1, writable: true }), expected: { source: `Object.defineProperties({},{a:{value:1,writable:!0}})`, }, }, { name: `enumerable configurable non-writable property`, value: Object.defineProperty({}, `a`, { value: 1, enumerable: true, configurable: true, }), expected: { source: `Object.defineProperties({},{a:{value:1,configurable:!0,enumerable:!0}})`, }, }, { name: `enumerable non-configurable writable property`, value: Object.defineProperty({}, `a`, { value: 1, enumerable: true, writable: true, }), expected: { source: `Object.defineProperties({},{a:{value:1,enumerable:!0,writable:!0}})`, }, }, { name: `enumerable configurable non-writable property`, value: Object.defineProperty({}, `a`, { value: 1, enumerable: true, configurable: true, }), expected: { source: `Object.defineProperties({},{a:{value:1,configurable:!0,enumerable:!0}})`, }, }, { name: `enumerable configurable writable property`, value: Object.defineProperty({}, `a`, { value: 1, enumerable: true, configurable: true, writable: true, }), expected: { source: `{a:1}` }, }, { name: `regular then non-regular property`, value: Object.defineProperty({ a: 1 }, `b`, { value: 2 }), expected: { source: `Object.defineProperties({a:1},{b:{value:2}})` }, }, { name: `non-regular then regular property`, value: Object.defineProperties( {}, { a: { value: 1 }, b: { value: 2, enumerable: true, configurable: true, writable: true }, }, ), expected: { source: `Object.defineProperties({},{a:{value:1},b:{value:2,configurable:!0,enumerable:!0,writable:!0}})`, }, }, { name: `non-enumerable symbol property`, value: Object.defineProperty({}, Symbol.toStringTag, { value: `hi` }), expected: { source: `Object.defineProperties({},{[Symbol.toStringTag]:{value:"hi"}})`, }, }, { name: `non-enumerable __proto__ property`, value: Object.defineProperty({}, `__proto__`, { value: null }), expected: { source: `Object.defineProperties({},{["__proto__"]:{value:null}})`, }, }, { name: `omit value from non-regular property descriptor`, value: Object.defineProperty({}, `a`, { value: 42 }), options: { custom: value => (value === 42 ? null : undefined) }, expected: { source: `Object.defineProperties({},{a:{}})`, roundtrips: false, }, }, { name: `accessor property with undefined getter`, value: Object.defineProperty({}, `a`, { get: undefined }), expected: { source: `Object.defineProperties({},{a:{get:void 0}})` }, }, { name: `accessor property with undefined setter`, value: Object.defineProperty({}, `a`, { set: undefined }), expected: { source: `Object.defineProperties({},{a:{get:void 0}})` }, }, { name: `accessor property with undefined getter and setter`, value: Object.defineProperty({}, `a`, { get: undefined, set: undefined }), expected: { source: `Object.defineProperties({},{a:{get:void 0}})` }, }, { name: `only non-regular properties`, value: Object.defineProperties( {}, { a: { value: 1, writable: true }, b: { value: 2, enumerable: true } }, ), expected: { source: `Object.defineProperties({},{a:{value:1,writable:!0},b:{value:2,enumerable:!0}})`, }, }, { name: `non-regular property with null prototype`, value: Object.setPrototypeOf( Object.defineProperty({}, `a`, { value: 1 }), null, ), expected: { source: `Object.setPrototypeOf(Object.defineProperties({},{a:{value:1}}),null)`, }, }, { name: `custom object`, value: { a: 1, b: 2, c: 3 }, options: { custom: (value, uneval) => value !== null && typeof value === `object` ? `{ ${Object.entries(value) .map(([key, value]) => `[${uneval(key)}]: ${uneval(value)}`) .join(`, `)} }` : undefined, }, expected: { source: `{ ["a"]: 1, ["b"]: 2, ["c"]: 3 }` }, }, { name: `custom string does not affect object keys`, value: { a: 1, b: 2, c: 3, 'x y z': 4 }, options: { custom: customString }, expected: { source: `{a:1,b:2,c:3,"x y z":4}` }, }, { name: `custom string does not affect object keys when string is sibling`, value: [{ a: 1, b: 2, c: 3, 'x y z': 4 }, `x y z`], options: { custom: customString }, expected: { source: `[{a:1,b:2,c:3,"x y z":4},'x y z']` }, }, { name: `custom __proto__ string does not affect object keys`, value: { a: 1, b: 2, c: 3, [`__proto__`]: 4 }, options: { custom: customString }, expected: { source: `{a:1,b:2,c:3,["__proto__"]:4}` }, }, { name: `custom __proto__ string does not affect object keys when string is sibling`, value: [{ a: 1, b: 2, c: 3, [`__proto__`]: 4 }, `__proto__`], options: { custom: customString }, expected: { source: `[{a:1,b:2,c:3,["__proto__"]:4},'__proto__']` }, }, { name: `omit string does not affect object keys`, value: { a: 1, b: 2, c: 3, 'x y z': 4 }, options: { custom: value => (value === `x y z` ? null : undefined) }, expected: { source: `{a:1,b:2,c:3,"x y z":4}` }, }, { name: `omit string does not affect object keys when string is sibling`, value: [{ a: 1, b: 2, c: 3, 'x y z': 4 }, `x y z`], options: { custom: value => (value === `x y z` ? null : undefined) }, expected: { source: `[{a:1,b:2,c:3,"x y z":4},,]`, roundtrips: false, }, }, { name: `omit __proto__ string does not affect object keys`, value: { a: 1, b: 2, c: 3, [`__proto__`]: 4 }, options: { custom: value => (value === `__proto__` ? null : undefined) }, expected: { source: `{a:1,b:2,c:3,["__proto__"]:4}` }, }, { name: `omit __proto__ string does not affect object keys when string is sibling`, value: [{ a: 1, b: 2, c: 3, [`__proto__`]: 4 }, `__proto__`], options: { custom: value => (value === `__proto__` ? null : undefined) }, expected: { source: `[{a:1,b:2,c:3,["__proto__"]:4},,]`, roundtrips: false, }, }, { name: `custom number does not affect object keys`, value: { 42: 4, a: 1, b: 2, c: 3 }, options: { custom: customNumber }, expected: { source: `{42:4.0,a:1.0,b:2.0,c:3.0}` }, }, { name: `custom number does not affect object keys when number is sibling`, value: [{ 42: 4, a: 1, b: 2, c: 3 }, 42], options: { custom: customNumber }, expected: { source: `[{42:4.0,a:1.0,b:2.0,c:3.0},42.0]` }, }, { name: `omit number does not affect object keys`, value: { 42: 4, a: 1, b: 2, c: 3 }, options: { custom: value => (value === 42 ? null : undefined) }, expected: { source: `{42:4,a:1,b:2,c:3}` }, }, { name: `omit number does not affect object keys when number is sibling`, value: [{ 42: 4, a: 1, b: 2, c: 3 }, 42], options: { custom: value => (value === 42 ? null : undefined) }, expected: { source: `[{42:4,a:1,b:2,c:3},,]`, roundtrips: false, }, }, { name: `custom symbol affects object keys`, value: { a: 1, [Symbol.for(`hi`)]: 2 }, options: { custom: customSymbol }, expected: { source: `{a:1,[Symbol("hi")]:2}`, roundtrips: false, }, }, { name: `custom value affects object values`, value: { a: 1, b: 2, c: 3 }, options: { custom: customNumber }, expected: { source: `{a:1.0,b:2.0,c:3.0}` }, }, { name: `omit object property value`, value: { a: 1, b: 2, c: 3 }, options: { custom: value => (value === 2 ? null : undefined) }, expected: { source: `{a:1,c:3}`, roundtrips: false, }, }, { name: `omit object from array`, value: [1, { a: 1 }, 3], options: { custom: value => typeof value === `object` && value !== null && `a` in value ? null : undefined, }, expected: { source: `[1,,3]`, roundtrips: false, }, }, { name: `omit symbol key from object`, value: (() => { const sym = Symbol.for(`omitMe`) return { a: 1, [sym]: 2, b: 3 } })(), options: { custom: value => (value === Symbol.for(`omitMe`) ? null : undefined), }, expected: { source: `{a:1,b:3}`, roundtrips: false, }, }, ], Set: [ { name: `empty Set`, value: new Set(), expected: { source: `new Set` }, }, { name: `empty Set from empty array`, value: new Set([]), expected: { source: `new Set` }, }, { name: `non-empty Set`, value: new Set([1, 2, 3]), expected: { source: `new Set([1,2,3])` }, }, { name: `polluted Set`, value: (() => { const value = Object.create(Set.prototype) as Set Object.defineProperty(value, Symbol.iterator, { *value() { yield ``), expected: { source: `/<\\/script>/` }, }, { name: `RegExp literal with closing script tag with whitespace`, // eslint-disable-next-line no-regex-spaces value: /<\/script >/, expected: { source: `/<\\/script >/` }, }, { name: `RegExp constructor with closing script tag with whitespace`, value: new RegExp(``), expected: { source: `/<\\/script >/` }, }, { name: `RegExp literal with unpaired low surrogate`, value: /\uDC00/, expected: { source: `/\\uDC00/` }, }, { name: `RegExp constructor with unpaired low surrogate`, value: new RegExp(`\uDC00`), expected: { source: `new RegExp("\\udc00")` }, }, { name: `RegExp literal with unpaired high surrogate`, value: /\uD800/, expected: { source: `/\\uD800/` }, }, { name: `RegExp constructor with unpaired high surrogate`, value: new RegExp(`\uD800`), expected: { source: `new RegExp("\\ud800")` }, }, { name: `RegExp literal with unpaired low surrogate in middle`, value: /a\uDC00b/, expected: { source: `/a\\uDC00b/` }, }, { name: `RegExp constructor with unpaired low surrogate in middle`, value: new RegExp(`a\uDC00b`), expected: { source: `new RegExp("a\\udc00b")` }, }, { name: `RegExp literal with unpaired high surrogate in middle`, value: /a\uD800b/, expected: { source: `/a\\uD800b/` }, }, { name: `RegExp constructor with unpaired high surrogate in middle`, value: new RegExp(`a\uD800b`), expected: { source: `new RegExp("a\\ud800b")` }, }, { name: `RegExp literal with multiple unpaired surrogates`, value: /\uD800\uDBFF/, expected: { source: `/\\uD800\\uDBFF/` }, }, { name: `RegExp constructor with multiple unpaired surrogates`, value: new RegExp(`\uD800\uDBFF`), expected: { source: `new RegExp("\\ud800\\udbff")` }, }, { name: `RegExp literal with surrogate pair`, value: /\uD83D\uDE00/, expected: { source: `/\\uD83D\\uDE00/` }, }, { name: `RegExp constructor with surrogate pair`, value: new RegExp(`\uD83D\uDE00`), expected: { source: `/😀/` }, }, { name: `RegExp literal with emoji`, value: /😀/, expected: { source: `/😀/` }, }, { name: `RegExp constructor with emoji`, value: new RegExp(`😀`), expected: { source: `/😀/` }, }, { name: `script polluted RegExp source`, value: Object.defineProperty(/a/, `source`, { value: `` XSS. expect(source).not.toMatch(/<\/script>/gi) }) }) const expectUnevalRoundtrips = ( value: unknown, options?: UnevalOptions, ): string => { const source = uneval(value, options) let roundtrippedValue: unknown try { // eslint-disable-next-line no-eval roundtrippedValue = (0, eval)(`(${source})`) as unknown expect(roundtrippedValue, source).toStrictEqual(value) } catch (error: unknown) { console.log(value) console.log(source) throw error } return source }