json-callbacks-test262.test.ts 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. /*
  2. * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75:
  3. * - test/built-ins/JSON/parse/reviver-call-order.js
  4. * - test/built-ins/JSON/parse/reviver-call-err.js
  5. * - test/built-ins/JSON/stringify/replacer-array-duplicates.js
  6. * - test/built-ins/JSON/stringify/replacer-array-empty.js
  7. * - test/built-ins/JSON/stringify/replacer-array-number.js
  8. * - test/built-ins/JSON/stringify/replacer-array-order.js
  9. * - test/built-ins/JSON/stringify/replacer-array-undefined.js
  10. * - test/built-ins/JSON/stringify/replacer-array-wrong-type.js
  11. * - test/built-ins/JSON/stringify/replacer-function-abrupt.js
  12. * - test/built-ins/JSON/stringify/replacer-function-arguments.js
  13. * - test/built-ins/JSON/stringify/replacer-function-array-circular.js
  14. * - test/built-ins/JSON/stringify/replacer-function-object-deleted-property.js
  15. * - test/built-ins/JSON/stringify/replacer-function-object-circular.js
  16. * - test/built-ins/JSON/stringify/replacer-function-result-undefined.js
  17. * - test/built-ins/JSON/stringify/replacer-function-result.js
  18. *
  19. * Function-context assertions are omitted because CodeMode functions intentionally
  20. * have no `this`. Number/String wrapper cases are omitted because CodeMode does not
  21. * support primitive wrapper objects. Proxy and Symbol cases are outside CodeMode.
  22. *
  23. * Copyright (C) 2012 Ecma International. All rights reserved.
  24. * Copyright (C) 2016 the V8 project authors. All rights reserved.
  25. * Copyright (C) 2019 Aleksey Shvayka. All rights reserved.
  26. * Copyright 2019 Kevin Gibbons. All rights reserved.
  27. * Test262 portions are governed by the BSD license in LICENSE.test262.
  28. */
  29. import { describe, expect, test } from "bun:test"
  30. import { Effect } from "effect"
  31. import { CodeMode } from "../src/index.js"
  32. import { invokeJsonMethod } from "../src/stdlib/json.js"
  33. const value = async (code: string) => {
  34. const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} }))
  35. if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`)
  36. return result.value
  37. }
  38. describe("Test262 JSON.parse reviver adaptations", () => {
  39. test("test/built-ins/JSON/parse/reviver-call-order.js", async () => {
  40. expect(
  41. await value(`
  42. const calls = []
  43. JSON.parse('{"p1":0,"p2":0,"p1":0,"2":0,"1":0}', (key, item) => {
  44. calls.push(key)
  45. return item
  46. })
  47. return calls
  48. `),
  49. ).toEqual(["1", "2", "p1", "p2", ""])
  50. })
  51. test("deletes properties and array elements when the reviver returns undefined", async () => {
  52. expect(
  53. await value(`
  54. const result = JSON.parse('{"keep":1,"drop":2,"items":[1,2,3]}', (key, item) => {
  55. if (key === "drop" || key === "1") return undefined
  56. return item
  57. })
  58. return [result, Object.hasOwn(result, "drop"), Object.hasOwn(result.items, 1), result.items.length]
  59. `),
  60. ).toEqual([{ keep: 1, items: [1, null, 3] }, false, false, 3])
  61. })
  62. test("replaces the root and may delete it", async () => {
  63. expect(await value(`return JSON.parse('{"a":1}', (key, item) => key === "" ? item.a : item)`)).toBe(1)
  64. expect(
  65. await value(`return JSON.parse('{"a":1}', (key, item) => key === "" ? undefined : item) === undefined`),
  66. ).toBe(true)
  67. })
  68. test("test/built-ins/JSON/parse/reviver-call-err.js", async () => {
  69. expect(
  70. await value(`try { JSON.parse("0", () => { throw "stop" }) } catch (error) { return error } return "missed"`),
  71. ).toBe("stop")
  72. })
  73. })
  74. describe("Test262 JSON.stringify replacer adaptations", () => {
  75. test("array replacers filter keys in their own order and apply recursively", async () => {
  76. expect(await value(`return JSON.stringify({ b: 1, a: 2, c: 3 }, ["c", "b", "a"])`)).toBe('{"c":3,"b":1,"a":2}')
  77. expect(await value(`return JSON.stringify({ a: { b: 2, c: 3 } }, ["c", "b", "a"])`)).toBe('{"a":{"c":3,"b":2}}')
  78. expect(await value(`return JSON.stringify({ a: 1 }, [])`)).toBe("{}")
  79. })
  80. test("array replacers deduplicate and coerce number primitives", async () => {
  81. expect(await value(`return JSON.stringify({ a: 1, b: 2 }, ["a", "a", "b"])`)).toBe('{"a":1,"b":2}')
  82. expect(
  83. await value(
  84. `return JSON.stringify({ "0": 0, "1": 1, "-4": 2, "0.3": 3, "-Infinity": 4, "NaN": 5 }, [-0, 1, -4, 0.3, -Infinity, NaN])`,
  85. ),
  86. ).toBe('{"0":0,"1":1,"-4":2,"0.3":3,"-Infinity":4,"NaN":5}')
  87. })
  88. test("array replacers ignore unsupported entry types", async () => {
  89. expect(
  90. await value(`return JSON.stringify({ true: 1, false: 2, null: 3, kept: 4 }, [true, false, null, {}, "kept"])`),
  91. ).toBe('{"kept":4}')
  92. expect(await value(`return JSON.stringify({ a: 1 }, undefined)`)).toBe('{"a":1}')
  93. })
  94. test("function replacers receive keys and values in preorder including the root", async () => {
  95. expect(
  96. await value(`
  97. const calls = []
  98. const output = JSON.stringify({ a: [1, 2], b: true }, (key, item) => {
  99. calls.push([key, Array.isArray(item) ? "array" : typeof item])
  100. return item
  101. })
  102. return [output, calls]
  103. `),
  104. ).toEqual([
  105. '{"a":[1,2],"b":true}',
  106. [
  107. ["", "object"],
  108. ["a", "array"],
  109. ["0", "number"],
  110. ["1", "number"],
  111. ["b", "boolean"],
  112. ],
  113. ])
  114. })
  115. test("test/built-ins/JSON/stringify/replacer-function-object-deleted-property.js", async () => {
  116. expect(
  117. await value(`
  118. const input = { a: 1, b: 2 }
  119. return JSON.stringify(input, (key, item) => {
  120. if (key === "a") delete input.b
  121. if (key === "b" && item === undefined) return "<replaced>"
  122. return item
  123. })
  124. `),
  125. ).toBe('{"a":1,"b":"<replaced>"}')
  126. })
  127. test("test/built-ins/JSON/stringify/replacer-function-object-circular.js", async () => {
  128. expect(
  129. await value(`
  130. const direct = { prop: {} }
  131. const indirect = { p1: { p2: {} } }
  132. const errors = []
  133. try { JSON.stringify(direct, () => direct) } catch (error) { errors.push(error.name) }
  134. try {
  135. JSON.stringify(indirect, (key, item) => key === "p2" ? indirect : item)
  136. } catch (error) { errors.push(error.name) }
  137. return errors
  138. `),
  139. ).toEqual(["TypeError", "TypeError"])
  140. })
  141. test("test/built-ins/JSON/stringify/replacer-function-array-circular.js", async () => {
  142. expect(
  143. await value(`
  144. const circular = [{}]
  145. try { JSON.stringify(circular, () => circular) } catch (error) { return error.name }
  146. return "missed"
  147. `),
  148. ).toBe("TypeError")
  149. })
  150. test("repeated objects outside the active ancestor chain are not circular", async () => {
  151. expect(
  152. await value(`
  153. const shared = { value: 1 }
  154. return JSON.stringify({ left: shared, right: shared }, (key, item) => item)
  155. `),
  156. ).toBe('{"left":{"value":1},"right":{"value":1}}')
  157. })
  158. test("function replacers replace values and map undefined to omission or null", async () => {
  159. expect(
  160. await value(`return JSON.stringify({ a: 1, nested: { b: [1] } }, (key, item) => item === 1 ? undefined : item)`),
  161. ).toBe('{"nested":{"b":[null]}}')
  162. expect(await value(`return JSON.stringify(null, (key) => key === "" ? { value: 1 } : 2)`)).toBe('{"value":2}')
  163. expect(await value(`return JSON.stringify(1, () => undefined) === undefined`)).toBe(true)
  164. })
  165. test("callable replacer results follow JSON omission semantics", async () => {
  166. expect(
  167. await value(`
  168. const fn = () => 1
  169. return [
  170. JSON.stringify({ user: 1, builtin: 2 }, (key, item) => key === "user" ? fn : key === "builtin" ? Math.abs : item),
  171. JSON.stringify([1, 2], (key, item) => key === "0" ? fn : key === "1" ? Number : item),
  172. JSON.stringify(null, () => "abc".includes) === undefined,
  173. ]
  174. `),
  175. ).toEqual(["{}", "[null,null]", true])
  176. })
  177. test("built-in toJSON conversion runs before the function replacer", async () => {
  178. expect(
  179. await value(`
  180. const seen = []
  181. const output = JSON.stringify({ date: new Date(0), url: new URL("https://example.test/a") }, (key, item) => {
  182. if (key === "date" || key === "url") seen.push(typeof item)
  183. return item
  184. })
  185. return [output, seen]
  186. `),
  187. ).toEqual(['{"date":"1970-01-01T00:00:00.000Z","url":"https://example.test/a"}', ["string", "string"]])
  188. })
  189. test("test/built-ins/JSON/stringify/replacer-function-abrupt.js", async () => {
  190. expect(
  191. await value(`try { JSON.stringify({}, () => { throw "stop" }) } catch (error) { return error } return "missed"`),
  192. ).toBe("stop")
  193. })
  194. })
  195. describe("CodeMode JSON callback boundaries", () => {
  196. test("this remains unsupported rather than exposing callback holders", async () => {
  197. const result = await Effect.runPromise(
  198. CodeMode.execute({ code: `return JSON.parse("1", function (key, item) { return this })`, tools: {} }),
  199. )
  200. expect(result).toMatchObject({ ok: false, error: { kind: "UnsupportedSyntax" } })
  201. })
  202. test("blocked parse keys are rejected before reviver traversal", async () => {
  203. expect(
  204. await value(
  205. `try { JSON.parse('{"__proto__":1}', (key, item) => item) } catch (error) { return true } return false`,
  206. ),
  207. ).toBe(true)
  208. })
  209. test("JSON.stringify directly rejects blocked input keys", () => {
  210. expect(() =>
  211. invokeJsonMethod(
  212. {
  213. invokeFunction: () => Effect.die("unused"),
  214. invokeCallable: () => Effect.die("unused"),
  215. settlePromise: () => Effect.die("unused"),
  216. },
  217. "stringify",
  218. [Object.fromEntries([["constructor", 1]])],
  219. { type: "CallExpression" },
  220. ),
  221. ).toThrow("blocked property 'constructor'")
  222. })
  223. })