Skip to content

Commit d75e18c

Browse files
committed
json-parse-with-source: JSON.parse forward modification
1 parent ff5aa19 commit d75e18c

File tree

3 files changed

+231
-176
lines changed

3 files changed

+231
-176
lines changed

test/built-ins/JSON/parse/reviver-call-args-after-forward-modification.js

Lines changed: 0 additions & 47 deletions
This file was deleted.
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
// Copyright (C) 2025 Richard Gibson. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
4+
/*---
5+
esid: sec-json.parse
6+
description: >
7+
JSON.parse reviver is called without "source" for modified properties unless
8+
they have been restored to original values.
9+
info: |
10+
JSON.parse ( _text_ [ , _reviver_ ] )
11+
1. Let _jsonString_ be ? ToString(_text_).
12+
2. Let _parseResult_ be ? ParseJSON(_jsonString_).
13+
3. Let _unfiltered_ be _parseResult_.[[Value]].
14+
4. If IsCallable(_reviver_) is *true*, then
15+
a. Let _root_ be OrdinaryObjectCreate(%Object.prototype%).
16+
b. Let _rootName_ be the empty String.
17+
c. Perform ! CreateDataPropertyOrThrow(_root_, _rootName_, _unfiltered_).
18+
d. Let _snapshot_ be <emu-meta suppress-effects="user-code">CreateJSONParseRecord(_parseResult_.[[ParseNode]], _rootName_, _unfiltered_)</emu-meta>.
19+
e. Return ? InternalizeJSONProperty(_root_, _rootName_, _reviver_, _snapshot_).
20+
21+
InternalizeJSONProperty ( _holder_, _name_, _reviver_, _parseRecord_ )
22+
1. Let _val_ be ? Get(_holder_, _name_).
23+
2. Let _context_ be OrdinaryObjectCreate(%Object.prototype%).
24+
3. If _parseRecord_ is a JSON Parse Record and SameValue(_parseRecord_.[[Value]], _val_) is *true*, then
25+
a. If _val_ is not an Object, then
26+
i. Let _parseNode_ be _parseRecord_.[[ParseNode]].
27+
ii. Assert: _parseNode_ is not an |ArrayLiteral| Parse Node and not an |ObjectLiteral| Parse Node.
28+
iii. Let _sourceText_ be the source text matched by _parseNode_.
29+
iv. Perform ! CreateDataPropertyOrThrow(_context_, *"source"*, CodePointsToString(_sourceText_)).
30+
b. Let _elementRecords_ be _parseRecord_.[[Elements]].
31+
c. Let _entryRecords_ be _parseRecord_.[[Entries]].
32+
4. Else,
33+
a. Let _elementRecords_ be a new empty List.
34+
b. Let _entryRecords_ be a new empty List.
35+
5. If _val_ is an Object, then
36+
a. Let _isArray_ be ? IsArray(_val_).
37+
b. If _isArray_ is *true*, then
38+
...
39+
iv. Repeat, while _I_ &lt; _len_,
40+
1. Let _prop_ be ! ToString(𝔽(_I_)).
41+
2. If _I_ &lt; _elementRecordsLen_, let _elementRecord_ be _elementRecords_[_I_]. Otherwise, let _elementRecord_ be ~empty~.
42+
3. Let _newElement_ be ? InternalizeJSONProperty(_val_, _prop_, _reviver_, _elementRecord_).
43+
...
44+
c. Else,
45+
i. Let _keys_ be ? EnumerableOwnProperties(_val_, ~key~).
46+
ii. For each String _P_ of _keys_, do
47+
1. Let _entryRecord_ be the element of _entryRecords_ whose [[Key]] field is _P_. If there is no such element, set _entryRecord_ to ~empty~.
48+
2. Let _newElement_ be ? InternalizeJSONProperty(_val_, _P_, _reviver_, _entryRecord_).
49+
...
50+
6. Return ? Call(_reviver_, _holder_, « _name_, _val_, _context_ »).
51+
52+
includes: [compareArray.js]
53+
features: [json-parse-with-source]
54+
---*/
55+
56+
var log = [];
57+
function overwritingReviver(key, value, context) {
58+
log.push('[' + key + ']: ' + JSON.stringify(value) + ' from |' + context.source + '|');
59+
if (value === 'start') {
60+
// Add new properties.
61+
this[1].added = true;
62+
this[2].push('added');
63+
} else if (value === 'obj') {
64+
// Replace properties of this object.
65+
this.toObj = {};
66+
this.toPrim = 2;
67+
this.toClone = this.toClone.slice();
68+
this.toOtherPrim = 4;
69+
} else if (value === 'arr') {
70+
// Replace elements of this array.
71+
this[1] = {};
72+
this[2] = 2;
73+
this[3] = this[3].slice();
74+
this[4] = 4;
75+
}
76+
return value;
77+
}
78+
var overwriteResult = JSON.parse(
79+
'[ ' +
80+
'"start", ' +
81+
'{ "0": "obj", "toObj": 1, "toPrim": {}, "toClone": ["clone me"], "toOtherPrim": "four" }, ' +
82+
'["arr", 1, {}, ["clone me"], "four"] ' +
83+
']',
84+
overwritingReviver
85+
);
86+
var expectObjJson = '{"0":"obj","toObj":{},"toPrim":2,"toClone":["clone me"],"toOtherPrim":4,"added":true}';
87+
var expectArrJson = '["arr",{},2,["clone me"],4,"added"]';
88+
assert.compareArray(log, [
89+
'[0]: "start" from |"start"|',
90+
91+
'[0]: "obj" from |"obj"|',
92+
'[toObj]: {} from |undefined|',
93+
'[toPrim]: 2 from |undefined|',
94+
'[0]: "clone me" from |undefined|',
95+
'[toClone]: ["clone me"] from |undefined|',
96+
'[toOtherPrim]: 4 from |undefined|',
97+
'[added]: true from |undefined|',
98+
'[1]: ' + expectObjJson + ' from |undefined|',
99+
100+
'[0]: "arr" from |"arr"|',
101+
'[1]: {} from |undefined|',
102+
'[2]: 2 from |undefined|',
103+
'[0]: "clone me" from |undefined|',
104+
'[3]: ["clone me"] from |undefined|',
105+
'[4]: 4 from |undefined|',
106+
'[5]: "added" from |undefined|',
107+
'[2]: ' + expectArrJson + ' from |undefined|',
108+
109+
'[]: ["start",' + expectObjJson + ',' + expectArrJson + '] from |undefined|'
110+
], 'overwrite result');
111+
assert.sameValue(
112+
JSON.stringify(overwriteResult),
113+
'["start",' + expectObjJson + ',' + expectArrJson + ']',
114+
'overwrite result'
115+
);
116+
117+
log = [];
118+
var cache = {};
119+
function replacingReviver(key, value, context) {
120+
log.push('[' + key + ']: ' + JSON.stringify(value) + ' from |' + context.source + '|');
121+
if (value === 'start') {
122+
// Remove properties from the upcoming object, caching and truncating its array.
123+
cache.objNonPrim = this[2].nonPrim;
124+
this[2].nonPrim.length = 0;
125+
delete this[2].prim;
126+
delete this[2].nonPrim;
127+
128+
// Remove elements from the upcoming array, caching and truncating its array.
129+
cache.arrNonPrim = this[3][2];
130+
this[3][2].length = 0;
131+
this[3].length = 1;
132+
133+
assert.sameValue(JSON.stringify(this), '["start","continue",{"0":"obj"},["arr"]]',
134+
'reviver forward removal');
135+
} else if (value === 'continue') {
136+
// Restore properties of the upcoming object, but in reverse order.
137+
// Then freeze the object to mutate attributes of those properties.
138+
this[2].nonPrim = cache.objNonPrim;
139+
this[2].prim = 1;
140+
Object.freeze(this[2]);
141+
142+
// Restore elements of the upcoming array, then freeze it.
143+
this[3].push(1, cache.arrNonPrim);
144+
Object.freeze(this[3]);
145+
146+
assert.sameValue(
147+
JSON.stringify(this),
148+
'["start","continue",{"0":"obj","nonPrim":[],"prim":1},["arr",1,[]]]',
149+
'reviver forward restoration');
150+
} else if (value === 'obj') {
151+
// Restore the element of the upcoming array and freeze it.
152+
this.nonPrim.push('string');
153+
Object.freeze(this.nonPrim);
154+
} else if (value === 'arr') {
155+
// Restore the element of the upcoming array and freeze it.
156+
this[2].push('string');
157+
Object.freeze(this[2]);
158+
}
159+
return value;
160+
}
161+
var replacedResult = JSON.parse(
162+
'[ ' +
163+
'"start", ' +
164+
'"continue", ' +
165+
'{ "0": "obj", "prim": 1e0, "nonPrim": ["string"] }, ' +
166+
'["arr", 1e0, ["string"]] ' +
167+
']',
168+
replacingReviver
169+
);
170+
expectObjJson = '{"0":"obj","nonPrim":["string"],"prim":1}';
171+
expectArrJson = '["arr",1,["string"]]';
172+
assert.compareArray(log, [
173+
'[0]: "start" from |"start"|',
174+
'[1]: "continue" from |"continue"|',
175+
176+
'[0]: "obj" from |"obj"|',
177+
'[0]: "string" from |"string"|',
178+
'[nonPrim]: ["string"] from |undefined|',
179+
'[prim]: 1 from |1e0|',
180+
'[2]: ' + expectObjJson + ' from |undefined|',
181+
182+
'[0]: "arr" from |"arr"|',
183+
'[1]: 1 from |1e0|',
184+
'[0]: "string" from |"string"|',
185+
'[2]: ["string"] from |undefined|',
186+
'[3]: ' + expectArrJson + ' from |undefined|',
187+
188+
'[]: ["start","continue",' + expectObjJson + ',' + expectArrJson + '] from |undefined|'
189+
], 'replaced result');
190+
assert.sameValue(
191+
JSON.stringify(replacedResult),
192+
'["start","continue",' + expectObjJson + ',' + expectArrJson + ']',
193+
'replaced result'
194+
);
195+
196+
log = [];
197+
function splicingReviver(key, value, context) {
198+
log.push('[' + key + ']: ' + JSON.stringify(value) + ' from |' + context.source + '|');
199+
if (value === 'start') {
200+
// Remove the "x" at index 1, throwing off the index for values 1+.
201+
this.splice(1, 1);
202+
} else if (value === 1) {
203+
// Insert a following element, restoring the index for values 2+.
204+
this.splice(+key + 1, 0, 1.5);
205+
} else if (value === 2) {
206+
// Insert a following element, throwing off the index for values 3+.
207+
this.splice(+key + 1, 0, 'pre 3');
208+
} else if (value === 'pre 3') {
209+
// Remove the following element (the original 3e0), restoring the index for values 4+.
210+
this.splice(+key, 1);
211+
} else if (value === 4) {
212+
// Add a new final element.
213+
this.push("end");
214+
}
215+
return value;
216+
}
217+
var replacedResult = JSON.parse('["start", "x", 1e0, 2e0, 3e0, 4e0]', splicingReviver);
218+
assert.compareArray(log, [
219+
'[0]: "start" from |"start"|',
220+
'[1]: 1 from |undefined|',
221+
'[2]: 1.5 from |undefined|',
222+
'[3]: 2 from |2e0|',
223+
'[4]: "pre 3" from |undefined|',
224+
'[5]: 4 from |4e0|',
225+
'[]: ["start",1,1.5,2,"pre 3",4,"end"] from |undefined|'
226+
], 'spliced result');
227+
assert.sameValue(
228+
JSON.stringify(replacedResult),
229+
'["start",1,1.5,2,"pre 3",4,"end"]',
230+
'spliced result'
231+
);

0 commit comments

Comments
 (0)