|
| 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_ < _len_, |
| 40 | + 1. Let _prop_ be ! ToString(𝔽(_I_)). |
| 41 | + 2. If _I_ < _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