Refuse JSON and dynamic copies of object literals with accessors (#1490)

This commit is contained in:
Chris Tate
2026-10-20 00:37:25 -05:00
committed by GitHub
4 changed files with 67 additions and 0 deletions
+6
View File
@@ -3891,6 +3891,9 @@ function isJsonSafeAt(
const shape = getRecord(t.shapeId);
if (!shape) return false;
if (recordTextCodecClass(shape) !== null) return false;
// Accessor slots are not function-valued properties: Node's
// JSON.stringify invokes the getter (see accessorSlotProp).
if (shapeHasAccessorSlots(shape)) return false;
// The recursive knot: answer true and let the rest of the graph
// decide (any unsafe constituent ends the traversal immediately).
if (visiting.has(t.shapeId)) return true;
@@ -4228,6 +4231,9 @@ function canBoxDynCompositeAt(
const shape = getRecord(t.shapeId);
if (!shape) return false;
if (recordTextCodecClass(shape) !== null) return false;
// Accessor slots hold the getter and setter closures, not property
// values (see accessorSlotProp).
if (shapeHasAccessorSlots(shape)) return false;
// Recursive shapes answer coinductively, like isJsonSafeType.
if (visiting.has(t.shapeId)) return true;
visiting.add(t.shapeId);
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/object-literal-accessor-values.ts"
],
"diags": []
}
@@ -0,0 +1,31 @@
// Object literals with get/set accessors keep their closures in reserved
// slots, so their typed values have no JSON or checked-dynamic form: Node's
// JSON.stringify and dynamic reads invoke the getter, which neither a typed
// serializer nor a converted copy could do.
function hold(value: unknown): unknown {
return value;
}
const counter = {
count: 1,
get doubled(): number {
return 2;
},
};
// JSON.stringify of the typed value would omit the getter's property.
console.log(JSON.stringify(counter));
// Converting the typed value to `unknown` or `any` would lose the getter.
hold(counter);
const viaAny: any = counter;
console.log(viaAny);
// A literal written into an `any` slot directly keeps its accessor.
const direct: any = {
get value(): number {
return 3;
},
};
console.log(direct.value);
@@ -0,0 +1,24 @@
object-literal-accessor-values.ts:18:28 - error SC1090: JSON.stringify of '{ get doubled(): number; count: number }' values (only number, string, boolean, records, arrays, unions of those, and 'unknown' stringify) are not supported yet
17 | // JSON.stringify of the typed value would omit the getter's property.
18 | console.log(JSON.stringify(counter));
| ^~~~~~~
19 |
object-literal-accessor-values.ts:21:6 - error SC1101: converting typed values to 'unknown' is not supported yet
20 | // Converting the typed value to `unknown` or `any` would lose the getter.
21 | hold(counter);
| ^~~~~~~
22 | const viaAny: any = counter;
hint: numbers, strings, booleans, JSON-safe records/arrays/unions (a deep copy), functions, program class instances, and supported native handles convert into 'unknown' slots; functions, exact-class round trips, and handles preserve identity, while this value's type has no dynamic representation yet
object-literal-accessor-values.ts:22:21 - error SC1101: converting typed values to 'unknown' is not supported yet
21 | hold(counter);
22 | const viaAny: any = counter;
| ^~~~~~~
23 | console.log(viaAny);
hint: numbers, strings, booleans, JSON-safe records/arrays/unions (a deep copy), functions, program class instances, and supported native handles convert into 'unknown' slots; functions, exact-class round trips, and handles preserve identity, while this value's type has no dynamic representation yet