Skip to content

Commit 3e49890

Browse files
authored
Fixed a couple of obscure replacer/reviver bugs. (#5)
* Fixed a couple of obscure replacer/reviver bugs. * Fixed a reviver bug with non-primitive arguments of function types.
1 parent a84c558 commit 3e49890

10 files changed

Lines changed: 109 additions & 32 deletions

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1+
### 4.2.3
2+
3+
* Fixed a reviver bug with non-primitive arguments of function types.
4+
5+
### 4.2.2
6+
7+
* Fixed a couple of bugs related to performing deletions of array/object elements via replacer or reviver functions.
8+
19
### 4.2.1
210

3-
* Documentation-only update.
11+
* Documentation-only update, clarifying replacer and reviver functions.
412

513
### 4.2.0
614

README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ The following features, which are not supported in standard JSON, have been adde
9090

9191
### Replacer functions (JSON-Z specific differences)
9292

93-
- When a replacer function returns `undefined` for the value of an item, for consistency with JSON replacer functions, that with cause the item to be deleted from the result. (`JSONZ.DELETE` can also be used for this purpose.)
93+
- When a replacer function returns `undefined` for the value of an item, for consistency with JSON replacer functions, that with cause the item to be deleted from the result. `JSONZ.DELETE` can also be used for this purpose, and *is the only way to delete an object or array item with a value that is originally `undefined`.*
9494
- Since JSON-Z can handle explicit `undefined` values, a replacer function can return the special value `JSONZ.UNDEFINED` to replace an item’s value with `undefined`.
9595
- Replacer functions can return the special value `JSONZ.DELETE` to indicate that a slot in an array be left empty, creating a sparse array.
9696
- A global replacer function can be specified.
@@ -215,9 +215,11 @@ The third argument passed to a JSON-Z reviver *might* be different from what a `
215215
> - `key`: The object key (or array index) of the value being parsed. The `key` is an empty string if the `value` is the root value.
216216
> - `value`: A value as originally parsed, which should be returned by the reviver as-is if the reviver is not modifying the original value.
217217
> - `extra`: This is either a `context` object containing a `source` string, as described at the link above, or the holder of the value, i.e., the object or array, if any, which contains the given key/value pair.
218-
> - `noContext`: if `true`, `extra` is the object or array which contains the current key/value pair. Otherwise, `value` is a primitive value, and `extra` functions like the (nearly) standard `context` object containing a `source` string, but also containing a `holder` value as well.
218+
> - `noContext`: if `true`, `extra` is the `holder` object or array which contains the current key/value pair. Otherwise, `value` is a primitive value, and `extra` functions like the (nearly) standard `context` object, containing a `source` string, but also containing a `holder` value as well.
219219
>
220-
> Returns: Either the original `value`, or a modified value.
220+
> Returns: Either the original `value`, `JSONZ.DELETE`, or a modified value (using `JSONZ.UNDEFINED` to change a value to `undefined`).
221+
222+
Please note that if you want to shrink the size of a containing array when using a reviver to delete an array element, you must both perform `holder.splice(parseInt(key), 1)` within the replacer and then return `JSONZ.DELETE` from the replacer function. Returning `JSONZ.DELETE` alone will simply result in a sparse array with an empty slot.
221223

222224
#### Return value
223225

@@ -239,8 +241,11 @@ This works very much like [`JSON.stringify`](https://developer.mozilla.org/en-US
239241
- `value`: The value to convert to a JSON-Z string.
240242
- `replacer`: A function which alters the behavior of the stringification process, or an array of String and Number objects that serve as an allowlist for selecting/filtering the properties of the value object to be included in the JSON-Z string. If this value is null or not provided, all properties of the object are included in the resulting JSON-Z string.
241243

242-
When using the standard `JSON.stringify()`, a replacer function is called with two arguments: `key` and `value`. JSON-Z adds a third argument, `holder`. This value is already available to standard replacer `function`s as `this`, but `this` won't be bound the holder when using an anonymous (arrow) function as a replacer. The JSON-Z third argument (which can be ignored if not needed) provides alternative access to the holder value.
243-
> `replacer(key, value[, holder])`
244+
When using the standard `JSON.stringify()`, a replacer function is called with two arguments: `key` and `value`. JSON-Z adds a third argument, `holder`. This value is already available to standard replacer `function`s as `this`, but `this` won't be bound the holder when using an anonymous (arrow) function as a replacer. The JSON-Z third argument (which can be ignored if not needed) provides alternative access to the holder value.<br><br>
245+
> `replacer(key, value[, holder])`
246+
247+
<br>Please note that if you want to shrink the size of a containing array when using a replacer to delete an array element, you must both perform `‑‑holder.length` within the replacer and then return `JSONZ.DELETE` from the replacer function. Returning `JSONZ.DELETE` alone will simply result in a sparse array with an empty slot.
248+
244249
- `space`: A string or number used to insert whitespace into the output JSON-Z string for readability purposes. If this is a number, it indicates the number of space characters to use as whitespace; this number is capped at 10. Values less than 1 indicate that no space should be used. If `space` is a string, that string (or the first 10 characters of the string if it's longer) is used as white space. A single space adds white space without adding indentation. If this parameter is not provided (or is null), no whitespace is added. If indenting white space is used, trailing commas can optionally appear in objects and arrays.
245250
- `options`: This can either be an `OptionSet` value (see [below](#jsonzsetoptionsoptions-additionaloptions)), or an object with the following properties:
246251
- `extendedPrimitives`: If `true` (the default is `false`) this enables direct stringification of `Infinity`, `-Infinity`, `NaN`, and `undefined`. Otherwise, these values become `null`.

lib/options-manager.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const big = require('./bignumber-util');
22
const util = require('./util');
33
const platform = require('./platform-specifics');
4+
const { ValueSourceWrapper } = require('./util');
45

56
let globalOptions = {};
67
let globalParseOptions = {};
@@ -254,6 +255,11 @@ module.exports = {
254255
let typeName;
255256
let container = false;
256257

258+
/* istanbul ignore next */ // This isn't currently being hit, but I feel better leaving it in for safety.
259+
if (value instanceof ValueSourceWrapper) {
260+
value = value.value;
261+
}
262+
257263
if (util.isTypeContainer(typeNameOrContainer)) {
258264
container = true;
259265
typeName = typeNameOrContainer._$_;

lib/parse.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ module.exports = function parse(text, reviver, newOptions) {
8282
return newPrimitiveToken('number', NaN);
8383
}
8484

85-
// noinspection JSUnusedGlobalSymbols (many functions here are accessed by string content, not direct in-code references)
8685
const lexStates = {
8786
default() {
8887
switch (c) {
@@ -1203,13 +1202,15 @@ module.exports = function parse(text, reviver, newOptions) {
12031202

12041203
if (value && typeof value === 'object' && !isBigNumber(value) && !(value instanceof ValueSourceWrapper)) {
12051204
// noinspection JSUnresolvedReference
1206-
const keys = Object.keys(value);
1205+
const keys = Object.keys(value).reverse();
12071206

12081207
for (const key of keys) {
12091208
const replacement = internalize(value, key, reviver);
12101209

12111210
if (replacement === undefined || replacement === util.DELETE) {
1212-
delete value[key];
1211+
if (!Array.isArray(value) || key < value.length - 1) {
1212+
delete value[key];
1213+
}
12131214
}
12141215
else {
12151216
value[key] = replacement === util.UNDEFINED ? undefined : replacement;
@@ -1556,7 +1557,9 @@ module.exports = function parse(text, reviver, newOptions) {
15561557
let revived;
15571558

15581559
try {
1559-
revived = optionsMgr.reviveTypeValue(current.name, current.arg);
1560+
const arg = typeof reviver === 'function' ? internalize({ '': current.arg }, '', reviver) : current.arg;
1561+
1562+
revived = optionsMgr.reviveTypeValue(current.name, arg);
15601563
}
15611564
catch (err) {
15621565
throw syntaxError(err.message, true);

lib/stringify.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ module.exports = function stringify(value, replacer, space) {
153153
value = value.toJSON(key);
154154
}
155155

156+
const originalValue = value;
157+
156158
if (replacerFunc) {
157159
value = replacerFunc.call(holder, key, value, holder);
158160
}
@@ -169,9 +171,9 @@ module.exports = function stringify(value, replacer, space) {
169171
case null: return 'null';
170172
case true: return 'true';
171173
case false: return 'false';
172-
case undefined: return extendedPrimitives ? 'undefined' : 'null';
174+
case undefined: return originalValue !== undefined ? util.DELETE : extendedPrimitives ? ('undefined') : 'null';
173175
case util.DELETE: return util.DELETE;
174-
case util.UNDEFINED: return util.UNDEFINED;
176+
case util.UNDEFINED: return 'undefined';
175177
}
176178

177179
if (typeof value === 'string') {
@@ -311,15 +313,14 @@ module.exports = function stringify(value, replacer, space) {
311313

312314
const partial = [];
313315

314-
for (let i = 0; i < value.length; i++) {
316+
for (let i = value.length - 1; i >= 0; --i) {
315317
if (!sparseArrays || i in value) {
316318
const propertyString = serializeProperty(String(i), value, true);
317319

318320
if (propertyString === util.DELETE) {
319-
partial.push('');
320-
}
321-
else if (propertyString === util.UNDEFINED) {
322-
partial.push('undefined');
321+
if (i < value.length) {
322+
partial.push('');
323+
}
323324
}
324325
else {
325326
partial.push(propertyString);
@@ -330,6 +331,8 @@ module.exports = function stringify(value, replacer, space) {
330331
}
331332
}
332333

334+
partial.reverse();
335+
333336
if (revealHiddenArrayProperties) {
334337
const keys = Object.keys(value);
335338

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-z",
3-
"version": "4.2.1",
3+
"version": "4.2.3",
44
"description": "JSON for everyone.",
55
"main": "lib/index.min.js",
66
"types": "lib/index.d.ts",

test/parse.spec.mjs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -571,18 +571,18 @@ it('parse(text, reviver)', () => {
571571
expect(
572572
JSONZ.parse('{a:1,b:2}', (k, v) => (k === 'a') ? JSONZ.DELETE : v)).to.deep.equal(
573573
{ b: 2 },
574-
'deletes property values'
574+
'JSONZ.DELETE deletes property values'
575575
);
576576

577577
expect(
578578
JSONZ.parse('{a:1,b:2}', (k, v) => (k === 'a') ? undefined : v)).to.deep.equal(
579579
{ b: 2 },
580-
'also deletes property values'
580+
'`undefined` deletes property values'
581581
);
582582

583583
expect(
584-
JSONZ.parse('{a:1,b:2}', (k, v) => (k === 'a') ? JSONZ.UNDEFINED : v)).to.deep.equal(
585-
{ a: undefined, b: 2 },
584+
JSONZ.parse('{a:1,b:2,c:_BigInt(3),d:_Map([[4,5]])}', (k, v) => (k === 'a') ? JSONZ.UNDEFINED : v)).to.deep.equal(
585+
{ a: undefined, b: 2, c: 3n, d: new Map([[4, 5]]) },
586586
'replaces property values with `undefined`'
587587
);
588588

@@ -611,6 +611,20 @@ it('parse(text, reviver)', () => {
611611
'deletes array values'
612612
);
613613

614+
expect(
615+
JSONZ.parse('[0,1,2]', (k, v, context) => {
616+
if (k === '1') {
617+
context.holder.splice(parseInt(k), 1);
618+
return JSONZ.DELETE;
619+
}
620+
else {
621+
return v;
622+
}
623+
})).to.deep.equal(
624+
[0, 2],
625+
'deletes array values and shrinks array'
626+
);
627+
614628
expect(
615629
JSONZ.parse('33', () => JSONZ.DELETE)).to.equal(
616630
undefined,

test/stringify.spec.mjs

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -491,24 +491,34 @@ describe('stringify', () => {
491491
);
492492
});
493493

494-
it('deletes object values when a replacer returns DELETE', () => {
494+
it('deletes object values when a replacer returns DELETE or `undefined`', () => {
495495
assert.strictEqual(
496496
JSONZ.stringify({ a: 1, b: 2 }, (key, value) => (key === 'b') ? JSONZ.DELETE : value),
497497
'{a:1}'
498498
);
499+
500+
assert.strictEqual(
501+
JSONZ.stringify({ a: 1, b: 2, c: undefined }, (key, value) => (key && key !== 'a') ? undefined : value),
502+
'{a:1,c:undefined}'
503+
);
504+
505+
assert.strictEqual(
506+
JSONZ.stringify({ a: 1, b: 2, c: undefined }, (key, value) => (key && key !== 'a') ? JSONZ.DELETE : value),
507+
'{a:1}'
508+
);
499509
});
500510

501511
it('can transform object values into undefined values with replacer', () => {
502512
assert.strictEqual(
503-
JSONZ.stringify({ a: 1, b: 2 }, (key, value) => (key === 'b') ? undefined : value),
513+
JSONZ.stringify({ a: 1, b: 2 }, (key, value) => (key === 'b') ? JSONZ.UNDEFINED : value),
504514
'{a:1,b:undefined}'
505515
);
506516
});
507517

508518
it('creates empty array slots when a replacer returns DELETE', () => {
509519
assert.strictEqual(
510-
JSONZ.stringify([1, 77, 3], (key, value) => (value === 77) ? JSONZ.DELETE : value),
511-
'[1,,3]'
520+
JSONZ.stringify([1, 77, 3, undefined], (key, value) => (value === 77) ? JSONZ.DELETE : value),
521+
'[1,,3,undefined]'
512522
);
513523
});
514524

@@ -519,10 +529,30 @@ describe('stringify', () => {
519529
);
520530
});
521531

522-
it('can transform array values into undefined values with replacer', () => {
532+
it('creates empty array slots when a replacer returns `undefined`, unless original value is `undefined`', () => {
523533
assert.strictEqual(
524-
JSONZ.stringify([1, 77, 3], (key, value) => (value === 77) ? undefined : value),
525-
'[1,undefined,3]'
534+
JSONZ.stringify([1, 77, 3, undefined], (key, value) => (key > 1) ? undefined : value),
535+
'[1,77,,undefined]'
536+
);
537+
538+
assert.strictEqual(
539+
JSONZ.stringify([1, 77, 3, undefined], (key, value) => (key > 1) ? JSONZ.DELETE : value),
540+
'[1,77,,]'
541+
);
542+
});
543+
544+
it('can shrink array by manipulating holder', () => {
545+
assert.strictEqual(
546+
JSONZ.stringify([1, 77, 3, undefined], (key, value, holder) => {
547+
if (key > 1) {
548+
--holder.length;
549+
return JSONZ.DELETE;
550+
}
551+
else {
552+
return value;
553+
}
554+
}),
555+
'[1,77]'
526556
);
527557
});
528558

tsconfig.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@
55
"emitDeclarationOnly": true,
66
"outDir": "types",
77
"noEmit": false,
8-
"skipLibCheck": true
8+
"skipLibCheck": true,
9+
"target": "es2020",
10+
"types": [
11+
"node"
12+
],
13+
"lib": [
14+
"es2020",
15+
"dom"
16+
]
917
},
1018
"include": ["lib/index.d.ts"],
1119
"exclude": ["node_modules", "dist"]

0 commit comments

Comments
 (0)