mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Support Object.freeze, Object.seal, Object.preventExtensions and Object.defineProperty on class instances
Object.freeze, Object.seal and Object.preventExtensions accept a class instance whose static type names the class, and Object.isFrozen, Object.isSealed and Object.isExtensible answer for it as Node does. Object.defineProperty with a string-literal key changes the value and the writable, enumerable and configurable attributes of a declared field, with Node's validation and errors. Only hierarchies passed to these calls pay for them: they get a per-instance integrity word and an attribute word per redefined field, and each write to a declared field checks them first. A write to a read-only field throws Node's TypeError in strict mode code and leaves the field unchanged in sloppy mode code; a class field initializer on a non-extensible instance throws "Cannot define property x, object is not extensible". The instance's checked view carries the attributes, so Object.keys, JSON.stringify, util.inspect and getOwnPropertyDescriptor follow them. Readonly<C> over a class maps to the class. Through an untyped value, freezing, sealing or preventing extensions of a class instance throws SC2020 instead of doing nothing. The runtime's "Cannot redefine property" and "Cannot add property" errors name the property, as Node does.
This commit is contained in:
@@ -1 +1 @@
|
||||
- **Object freezing:** `Object.freeze` and `Object.isFrozen` support plain checked dictionaries and primitives. Freezing is shallow and preserves accessors. Aliased typed records, arrays, proxies, and other native objects cannot be frozen.
|
||||
- **Object freezing:** `Object.freeze` and `Object.isFrozen` support plain checked dictionaries and primitives. Freezing is shallow and preserves accessors. Aliased typed records, arrays, proxies, and other native objects cannot be frozen. Class instances support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible` when the static type of the argument names the class. A write to a read-only field throws Node's `TypeError` in strict mode code and leaves the field unchanged in sloppy mode code. `Object.defineProperty` with a string-literal key changes the value, `writable`, `enumerable`, and `configurable` attributes of a declared field; an accessor descriptor for a declared field throws `SC2020`. Freezing, sealing, or preventing extensions of a class instance held in an untyped value throws `SC2020`. A JavaScript class field that is first created after its instance became non-extensible is added instead of throwing.
|
||||
|
||||
@@ -5,10 +5,12 @@ import { streamTypedRefEligible, undefinedArmTag } from "../../ir/analysis.js";
|
||||
import {
|
||||
type IrRecordShape,
|
||||
type IrType,
|
||||
CLASS_ATTRIBUTES,
|
||||
DYN_CLASS_PROPERTIES,
|
||||
RUNTIME_STREAM_CLASSES,
|
||||
SYMBOL_T,
|
||||
classDynViewSupported,
|
||||
classFieldAttributesName,
|
||||
isClassOwnEnumerableFieldName,
|
||||
isDirentShape,
|
||||
isDynTypedRefType,
|
||||
@@ -766,6 +768,7 @@ export function streamTypedRefMaterializeAdapter(
|
||||
host.declare(`declare ptr @scr_stream_dyn_view(ptr)`);
|
||||
B.line(`${out} = call ptr @scr_stream_dyn_view(ptr %p)`);
|
||||
} else B.line(`${out} = call ptr @scr_dyn_new_obj()`);
|
||||
const withAttributes = meta.def.fields.some((field) => field.name === CLASS_ATTRIBUTES);
|
||||
if (meta.def.tracksOwnFields) {
|
||||
host.declare(`declare ptr @scr_dyn_copy_property_descriptors(ptr, ptr)`);
|
||||
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
||||
@@ -835,9 +838,27 @@ export function streamTypedRefMaterializeAdapter(
|
||||
const nullable = host.nullableFields.get(t.className, field.name);
|
||||
if (nullable) fieldValue = nullableFieldUnion(host, B, fieldValue, nullable);
|
||||
const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue);
|
||||
// Attributes copied from the bag do not stop the view taking the value.
|
||||
const setter = withAttributes ? "scr_dyn_obj_set_field" : "scr_dyn_obj_set";
|
||||
if (withAttributes)
|
||||
host.declare(`declare void @scr_dyn_obj_set_field(ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`,
|
||||
`call void @${setter}(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`,
|
||||
);
|
||||
// A field redefined through Object.defineProperty keeps its attributes.
|
||||
const attributesField = classFieldAttributesName(field.name);
|
||||
if (meta.def.fields.some((f) => f.name === attributesField)) {
|
||||
host.declare(`declare void @scr_dyn_class_field_attributes(ptr, ptr, double)`);
|
||||
const slot = B.tmp(),
|
||||
bits = B.tmp();
|
||||
B.line(
|
||||
`${slot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${classFieldIndex(meta, attributesField).index}`,
|
||||
);
|
||||
B.line(`${bits} = load double, ptr ${slot}`);
|
||||
B.line(
|
||||
`call void @scr_dyn_class_field_attributes(ptr ${out}, ptr ${host.internLiteral(field.name)}, double ${bits})`,
|
||||
);
|
||||
}
|
||||
if (nullable) {
|
||||
host.declare(`declare void @scr_union_release(ptr)`);
|
||||
B.line(`call void @scr_union_release(ptr ${fieldValue})`);
|
||||
@@ -920,6 +941,17 @@ export function streamTypedRefMaterializeAdapter(
|
||||
B.line(`${copied} = call ptr @scr_dyn_copy_property_descriptors(ptr ${out}, ptr ${bag})`);
|
||||
B.line(`call void @scr_dyn_release_v(ptr ${copied})`);
|
||||
}
|
||||
// A frozen, sealed or non-extensible instance's view says so.
|
||||
if (withAttributes) {
|
||||
host.declare(`declare void @scr_dyn_class_view_attributes(ptr, double)`);
|
||||
const slot = B.tmp(),
|
||||
bits = B.tmp();
|
||||
B.line(
|
||||
`${slot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${classFieldIndex(meta, CLASS_ATTRIBUTES).index}`,
|
||||
);
|
||||
B.line(`${bits} = load double, ptr ${slot}`);
|
||||
B.line(`call void @scr_dyn_class_view_attributes(ptr ${out}, double ${bits})`);
|
||||
}
|
||||
B.terminate(`ret ptr ${out}`);
|
||||
} else if (t.kind === "record") {
|
||||
const shape = host.recordsById.get(t.shapeId);
|
||||
|
||||
@@ -907,6 +907,12 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.objectTag": "scr_dyn_object_tag",
|
||||
"dyn.freeze": "scr_dyn_freeze",
|
||||
"dyn.isFrozen": "scr_dyn_is_frozen",
|
||||
"class.defineField": "scr_class_define_field",
|
||||
"class.definedBits": "scr_class_defined_bits",
|
||||
"class.definedValue": "scr_class_defined_value",
|
||||
"class.descriptorHasValue": "scr_class_descriptor_has_value",
|
||||
"class.throwReadOnly": "scr_class_throw_read_only",
|
||||
"class.throwNotExtensible": "scr_class_throw_not_extensible",
|
||||
"dyn.nativeSetNew": "scr_dyn_native_set_new",
|
||||
"dyn.nativeSetIs": "scr_dyn_native_set_is",
|
||||
"dyn.nativeMapIs": "scr_dyn_native_map_is",
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import {
|
||||
BOOL,
|
||||
CLASS_ATTRIBUTES,
|
||||
CLASS_FROZEN,
|
||||
CLASS_NOT_EXTENSIBLE,
|
||||
CLASS_SEALED,
|
||||
DYN,
|
||||
F64,
|
||||
FIELD_NOT_WRITABLE,
|
||||
STRING,
|
||||
VOID,
|
||||
canConvertToDyn,
|
||||
canDynCheckTo,
|
||||
classFieldAttributesName,
|
||||
isClassOwnEnumerableFieldName,
|
||||
isDynTypedRefType,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { numLit, strLit, varRef } from "../../ir/build.js";
|
||||
import { transformStmtList } from "../../ir/traverse.js";
|
||||
import { locOf } from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import { classPropertiesHelper } from "./class-dynamic-dispatch.js";
|
||||
import { runtimeRooted } from "./class-descriptors.js";
|
||||
|
||||
/* Object.freeze, Object.seal, Object.preventExtensions and
|
||||
* Object.defineProperty on class instances. Only the hierarchies these calls
|
||||
* name pay for them: such a hierarchy gets a CLASS_ATTRIBUTES integrity word
|
||||
* per instance, an attribute word per redefined field, and a check before
|
||||
* each write to a declared field (guardClassAttributeWrites). The instance's
|
||||
* checked view carries the attributes, so reflection answers from it. */
|
||||
|
||||
type ClassObjectType = Extract<IrType, { kind: "object" }>;
|
||||
|
||||
function attributeClass(lowerer: Lowerer, target: IrExpr): ClassInfo | null {
|
||||
if (!isDynTypedRefType(target.type)) return null;
|
||||
const info = lowerer.classes.get(target.type.className);
|
||||
if (!info || runtimeRooted(info) || info.decl == null) return null;
|
||||
return info;
|
||||
}
|
||||
|
||||
/** Adds a zero-initialized f64 to every class of the hierarchy, at the same
|
||||
* offset, like the property bag. */
|
||||
function addHiddenField(info: ClassInfo, name: string): void {
|
||||
let root = info;
|
||||
while (root.base && !root.base.def.runtime) root = root.base;
|
||||
if (root.fields.has(name)) return;
|
||||
const index = root.def.fields.length;
|
||||
const add = (current: ClassInfo): void => {
|
||||
if (!current.fields.has(name)) {
|
||||
current.fields.set(name, F64);
|
||||
current.def.fields.splice(index, 0, { name, type: F64 });
|
||||
}
|
||||
current.subclasses.forEach(add);
|
||||
};
|
||||
add(root);
|
||||
}
|
||||
|
||||
function hiddenFieldGet(receiver: IrExpr, className: string, field: string, loc: SrcLoc): IrExpr {
|
||||
return { kind: "fieldGet", obj: receiver, className, field, type: F64, loc };
|
||||
}
|
||||
|
||||
/** Object.freeze, Object.seal and Object.preventExtensions of a class
|
||||
* instance: sets the instance's integrity bits, then applies the operation to
|
||||
* the properties added to the instance. Answers the instance. */
|
||||
export function lowerClassIntegrity(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
member: "freeze" | "seal" | "preventExtensions",
|
||||
target: IrExpr,
|
||||
): IrExpr | null {
|
||||
const info = attributeClass(lowerer, target);
|
||||
if (!info) return null;
|
||||
addHiddenField(info, CLASS_ATTRIBUTES);
|
||||
const loc = locOf(call);
|
||||
const type = target.type as ClassObjectType;
|
||||
const bits =
|
||||
member === "freeze"
|
||||
? CLASS_NOT_EXTENSIBLE | CLASS_SEALED | CLASS_FROZEN
|
||||
: member === "seal"
|
||||
? CLASS_NOT_EXTENSIBLE | CLASS_SEALED
|
||||
: CLASS_NOT_EXTENSIBLE;
|
||||
const slot = lowerer.declareHiddenLocal("%integrityReceiver", type);
|
||||
const receiver = (): IrExpr => varRef(slot.id, type, loc);
|
||||
const bag: IrExpr = {
|
||||
kind: "call",
|
||||
callee: classPropertiesHelper(lowerer, loc).name,
|
||||
args: [lowerer.coerceToExpected(receiver(), DYN)],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
const operation = {
|
||||
freeze: "dyn.freeze",
|
||||
seal: "dyn.seal",
|
||||
preventExtensions: "dyn.preventExtensions",
|
||||
} as const;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: slot.id, init: target, loc },
|
||||
{
|
||||
kind: "fieldSet",
|
||||
obj: receiver(),
|
||||
className: type.className,
|
||||
field: CLASS_ATTRIBUTES,
|
||||
value: {
|
||||
kind: "bin",
|
||||
op: "|",
|
||||
left: hiddenFieldGet(receiver(), type.className, CLASS_ATTRIBUTES, loc),
|
||||
right: numLit(bits, loc),
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
attributesChecked: true,
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: operation[member], args: [bag], type: DYN, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: receiver(),
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Object.defineProperty with a string-literal key naming a declared field
|
||||
* of a class instance: a data descriptor changes the field's value and
|
||||
* attributes, validated as Node validates ordinary objects. Answers the
|
||||
* instance, or null when the call is not of that form. */
|
||||
export function lowerClassFieldDefine(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
target: IrExpr,
|
||||
): IrExpr | null {
|
||||
const info = attributeClass(lowerer, target);
|
||||
const key = call.arguments[1]!;
|
||||
if (!info || !ts.isStringLiteral(key)) return null;
|
||||
const field = key.text;
|
||||
const fieldType = info.fields.get(field);
|
||||
if (
|
||||
fieldType === undefined ||
|
||||
!isClassOwnEnumerableFieldName(field) ||
|
||||
[...(info.symbolFields?.values() ?? [])].includes(field)
|
||||
)
|
||||
return null;
|
||||
const getRecord = (id: string) => lowerer.shapes.get(id);
|
||||
const getUnion = (id: string) => lowerer.unions.get(id);
|
||||
if (
|
||||
!canConvertToDyn(fieldType, getRecord, getUnion) ||
|
||||
!canDynCheckTo(fieldType, getRecord, getUnion)
|
||||
)
|
||||
return null;
|
||||
const attributesField = classFieldAttributesName(field);
|
||||
addHiddenField(info, CLASS_ATTRIBUTES);
|
||||
addHiddenField(info, attributesField);
|
||||
const loc = locOf(call);
|
||||
const type = target.type as ClassObjectType;
|
||||
const receiverSlot = lowerer.declareHiddenLocal("%defineReceiver", type);
|
||||
const descriptorSlot = lowerer.declareHiddenLocal("%defineDescriptor", DYN);
|
||||
const holderSlot = lowerer.declareHiddenLocal("%defineHolder", DYN);
|
||||
const receiver = (): IrExpr => varRef(receiverSlot.id, type, loc);
|
||||
const descriptor = (): IrExpr => varRef(descriptorSlot.id, DYN, loc);
|
||||
const holder = (): IrExpr => varRef(holderSlot.id, DYN, loc);
|
||||
const name = (): IrExpr => strLit(field, loc);
|
||||
const current: IrExpr = {
|
||||
kind: "fieldGet",
|
||||
obj: receiver(),
|
||||
className: type.className,
|
||||
field,
|
||||
type: fieldType,
|
||||
loc,
|
||||
};
|
||||
const defined: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "class.definedValue",
|
||||
args: [holder(), name()],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: receiverSlot.id, init: target, loc },
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: descriptorSlot.id,
|
||||
init: lowerer.lowerExprExpecting(call.arguments[2]!, DYN),
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: holderSlot.id,
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "class.defineField",
|
||||
args: [
|
||||
lowerer.coerceToExpected(current, DYN),
|
||||
hiddenFieldGet(receiver(), type.className, attributesField, loc),
|
||||
hiddenFieldGet(receiver(), type.className, CLASS_ATTRIBUTES, loc),
|
||||
name(),
|
||||
descriptor(),
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "libCall",
|
||||
fn: "class.descriptorHasValue",
|
||||
args: [descriptor()],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "fieldSet",
|
||||
obj: receiver(),
|
||||
className: type.className,
|
||||
field,
|
||||
value: lowerer.coerceToExpected(defined, fieldType),
|
||||
attributesChecked: true,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "fieldSet",
|
||||
obj: receiver(),
|
||||
className: type.className,
|
||||
field: attributesField,
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "class.definedBits",
|
||||
args: [holder(), name()],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
attributesChecked: true,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: receiver(),
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
function classDisplayName(info: ClassInfo): string {
|
||||
return info.def.jsName ?? info.decl?.name?.text ?? info.def.name.replace(/^%/, "");
|
||||
}
|
||||
|
||||
/** The instance's own class name for an error message: a helper testing the
|
||||
* most derived classes of the hierarchy first. */
|
||||
function instanceClassName(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
receiver: IrExpr,
|
||||
helpers: Map<string, IrFunction>,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
if (info.subclasses.length === 0) return strLit(classDisplayName(info), loc);
|
||||
let helper = helpers.get(info.def.name);
|
||||
if (!helper) {
|
||||
const type: IrType = { kind: "object", className: info.def.name };
|
||||
const value = varRef("value", type, loc);
|
||||
const descendants = (current: ClassInfo): ClassInfo[] =>
|
||||
current.subclasses.flatMap((child) => [...descendants(child), child]);
|
||||
const body: IrStmt[] = descendants(info).map((child): IrStmt => ({
|
||||
kind: "if",
|
||||
cond: { kind: "instanceOf", value, className: child.def.name, type: BOOL, loc },
|
||||
then: [{ kind: "return", value: strLit(classDisplayName(child), loc), loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
}));
|
||||
body.push({ kind: "return", value: strLit(classDisplayName(info), loc), loc });
|
||||
helper = {
|
||||
name: `%class.instanceName.${helpers.size}`,
|
||||
params: [{ localId: "value", name: "value", type }],
|
||||
returnType: STRING,
|
||||
locals: [{ id: "value", name: "value", type, mutable: false }],
|
||||
body,
|
||||
loc,
|
||||
};
|
||||
helpers.set(info.def.name, helper);
|
||||
}
|
||||
return { kind: "call", callee: helper.name, args: [receiver], type: STRING, loc };
|
||||
}
|
||||
|
||||
/** Checks the integrity word, and the field's attribute word when it has one,
|
||||
* before every write to a declared field of a hierarchy that has them. A
|
||||
* read-only field throws Node's TypeError in strict mode code; a sloppy-mode
|
||||
* write leaves it unchanged. Other classes keep their plain stores. */
|
||||
export function guardClassAttributeWrites(lowerer: Lowerer, functions: IrFunction[]): void {
|
||||
const guarded = new Set(
|
||||
[...lowerer.classes.values()]
|
||||
.filter((info) => info.def.fields.some((field) => field.name === CLASS_ATTRIBUTES))
|
||||
.map((info) => info.def.name),
|
||||
);
|
||||
if (guarded.size === 0) return;
|
||||
const helpers = new Map<string, IrFunction>();
|
||||
for (const fn of functions) {
|
||||
const local = (type: IrType, loc: SrcLoc): Extract<IrExpr, { kind: "varRef" }> => {
|
||||
const id = `%attributeWrite.${fn.locals.length}`;
|
||||
fn.locals.push({ id, name: id, type, mutable: false });
|
||||
return { kind: "varRef", localId: id, type, loc };
|
||||
};
|
||||
fn.body = transformStmtList(fn.body, {
|
||||
stmt: (statement) => {
|
||||
if (
|
||||
statement.kind !== "fieldSet" ||
|
||||
statement.attributesChecked ||
|
||||
!guarded.has(statement.className) ||
|
||||
!isClassOwnEnumerableFieldName(statement.field)
|
||||
)
|
||||
return statement;
|
||||
const info = lowerer.classes.get(statement.className)!;
|
||||
const loc = statement.loc;
|
||||
const receiver = local(statement.obj.type, loc);
|
||||
const value = local(statement.value.type, loc);
|
||||
const masked = (field: string, bit: number): IrExpr => ({
|
||||
kind: "bin",
|
||||
op: "&",
|
||||
left: hiddenFieldGet(receiver, statement.className, field, loc),
|
||||
right: numLit(bit, loc),
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const attributesField = classFieldAttributesName(statement.field);
|
||||
let readOnly = masked(
|
||||
CLASS_ATTRIBUTES,
|
||||
statement.define ? CLASS_NOT_EXTENSIBLE : CLASS_FROZEN,
|
||||
);
|
||||
if (!statement.define && info.fields.has(attributesField)) {
|
||||
readOnly = {
|
||||
kind: "bin",
|
||||
op: "|",
|
||||
left: readOnly,
|
||||
right: masked(attributesField, FIELD_NOT_WRITABLE),
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const reject: IrStmt[] = statement.define
|
||||
? [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "class.throwNotExtensible",
|
||||
args: [strLit(statement.field, loc)],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
]
|
||||
: statement.sloppy
|
||||
? []
|
||||
: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "class.throwReadOnly",
|
||||
args: [
|
||||
strLit(statement.field, loc),
|
||||
instanceClassName(lowerer, info, receiver, helpers, loc),
|
||||
],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
];
|
||||
return {
|
||||
kind: "block",
|
||||
body: [
|
||||
{ kind: "varDecl", localId: receiver.localId, init: statement.obj, loc },
|
||||
{ kind: "varDecl", localId: value.localId, init: statement.value, loc },
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "!==",
|
||||
left: readOnly,
|
||||
right: numLit(0, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: reject,
|
||||
else_: [{ ...statement, obj: receiver, value, attributesChecked: true }],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
};
|
||||
},
|
||||
expr: (expression) => expression,
|
||||
});
|
||||
}
|
||||
for (const helper of helpers.values()) functions.push(helper);
|
||||
}
|
||||
@@ -23,7 +23,7 @@ function bagOnlyProperty(owner: ClassInfo, name: string): boolean {
|
||||
return !owner.fields.has(name) && owner.subclasses.every((child) => bagOnlyProperty(child, name));
|
||||
}
|
||||
|
||||
function runtimeRooted(info: ClassInfo): boolean {
|
||||
export function runtimeRooted(info: ClassInfo): boolean {
|
||||
for (let current: ClassInfo | null = info; current; current = current.base) {
|
||||
if (
|
||||
current.def.runtime ||
|
||||
|
||||
@@ -215,6 +215,7 @@ import {
|
||||
} from "./optional-arguments.js";
|
||||
import { fenceSymbolFieldCopy } from "./symbol-fields.js";
|
||||
import { lowerClassDataDescriptor, lowerClassDescriptorRead } from "./class-descriptors.js";
|
||||
import { lowerClassFieldDefine, lowerClassIntegrity } from "./class-attributes.js";
|
||||
import { classStaticDataFor } from "./class-static-data.js";
|
||||
import { lowerUrlFactory } from "./lower-url.js";
|
||||
import { nullishValueUnitOf } from "./binding-analysis.js";
|
||||
@@ -7415,6 +7416,8 @@ function lowerObjectStaticCall(
|
||||
if (target && declaredTarget && isDynTypedRefType(declaredTarget))
|
||||
target = lowerer.coerceToExpected(target, declaredTarget);
|
||||
if (target && member === "defineProperty") {
|
||||
const field = lowerClassFieldDefine(lowerer, call, target);
|
||||
if (field) return field;
|
||||
const native = lowerClassDataDescriptor(lowerer, call, member, target);
|
||||
if (native) return native;
|
||||
}
|
||||
@@ -7557,6 +7560,19 @@ function lowerObjectStaticCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (member === "preventExtensions" || member === "seal") {
|
||||
const integrity = lowerClassIntegrity(lowerer, call, member, value);
|
||||
if (integrity) return integrity;
|
||||
} else if (isDynTypedRefType(value.type)) {
|
||||
// The instance's checked view carries its integrity state.
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: member === "isSealed" ? "dyn.isSealed" : "dyn.isExtensible",
|
||||
args: [lowerer.coerceToExpected(value, DYN)],
|
||||
type: BOOL,
|
||||
loc: locOf(call),
|
||||
};
|
||||
}
|
||||
if (value.type.kind === "dyn") {
|
||||
const operation = {
|
||||
preventExtensions: "dyn.preventExtensions",
|
||||
@@ -7627,6 +7643,8 @@ function lowerObjectStaticCall(
|
||||
)
|
||||
)
|
||||
return value;
|
||||
const integrity = lowerClassIntegrity(lowerer, call, "freeze", value);
|
||||
if (integrity) return integrity;
|
||||
lowerer.noLowering(
|
||||
"Object.freeze of a possibly-aliased value",
|
||||
call,
|
||||
|
||||
@@ -7758,6 +7758,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL
|
||||
className: info.def.name,
|
||||
field: f.name,
|
||||
value: dynUndefinedExpr(loc),
|
||||
define: true,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
@@ -7773,6 +7774,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL
|
||||
className: info.def.name,
|
||||
field: f.name,
|
||||
value,
|
||||
define: true,
|
||||
loc: locOf(f.initializer),
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
maybePromiseInstanceOf,
|
||||
maybePromiseUnionOf,
|
||||
} from "./maybe-promise.js";
|
||||
import { isStrictCode } from "./strict-mode.js";
|
||||
import { checkedClassAssertion, checkedClassUnionAssertion } from "./class-assertions.js";
|
||||
import { checkedArmExtraction } from "./coercions/unions.js";
|
||||
import { narrowClassUnion, narrowStoredClassValue } from "./class-unions.js";
|
||||
@@ -16149,6 +16150,7 @@ export function fieldSetStmt(
|
||||
className: target.className,
|
||||
field: target.field,
|
||||
value,
|
||||
...(isStrictCode(blame) ? {} : { sloppy: true as const }),
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
type SrcLoc,
|
||||
canConvertToDyn,
|
||||
canDynCheckTo,
|
||||
CLASS_FIELD_ATTRIBUTES_PREFIX,
|
||||
classDynViewSupported,
|
||||
DYN_CLASS_PROPERTIES,
|
||||
isClassOwnEnumerableFieldName,
|
||||
@@ -499,12 +500,16 @@ function classInspectsThroughView(lowerer: Lowerer, info: ClassInfo | undefined)
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether a class holds properties added outside its declared layout, which
|
||||
* only its checked-dynamic view can list. Decided after lowering, when the
|
||||
/** Whether a class holds properties added outside its declared layout, or
|
||||
* fields Object.defineProperty can make non-enumerable, which only its
|
||||
* checked-dynamic view lists as Node does. Decided after lowering, when the
|
||||
* class has its final fields. */
|
||||
function classInspectsAddedProperties(lowerer: Lowerer, info: ClassInfo): boolean {
|
||||
return (
|
||||
info.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES) &&
|
||||
info.def.fields.some(
|
||||
(field) =>
|
||||
field.name === DYN_CLASS_PROPERTIES || field.name.startsWith(CLASS_FIELD_ATTRIBUTES_PREFIX),
|
||||
) &&
|
||||
classDynViewSupported(
|
||||
info.def.fields,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
|
||||
@@ -85,10 +85,10 @@ import {
|
||||
isCjsJsFile,
|
||||
isEsModuleStamp,
|
||||
isJsSourceFile,
|
||||
isNodeEsmFile,
|
||||
locOf,
|
||||
requireSpecOf,
|
||||
} from "../program.js";
|
||||
import { isStrictCode } from "./strict-mode.js";
|
||||
import {
|
||||
COMPOUND_ASSIGN_OPS,
|
||||
type CompoundOp,
|
||||
@@ -5879,35 +5879,6 @@ export function lowerTry(lowerer: Lowerer, stmt: ts.TryStatement): IrStmt {
|
||||
};
|
||||
}
|
||||
|
||||
function hasUseStrictDirective(statements: readonly ts.Statement[]): boolean {
|
||||
for (const statement of statements) {
|
||||
if (!ts.isExpressionStatement(statement) || !ts.isStringLiteral(statement.expression))
|
||||
return false;
|
||||
if (statement.expression.text === "use strict") return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isStrictDelete(node: ts.DeleteExpression): boolean {
|
||||
for (let parent: ts.Node | undefined = node.parent; parent; parent = parent.parent) {
|
||||
if (ts.isClassDeclaration(parent) || ts.isClassExpression(parent)) return true;
|
||||
if (
|
||||
ts.isFunctionLike(parent) &&
|
||||
parent.body &&
|
||||
ts.isBlock(parent.body) &&
|
||||
hasUseStrictDirective(parent.body.statements)
|
||||
)
|
||||
return true;
|
||||
if (ts.isSourceFile(parent)) {
|
||||
return (
|
||||
hasUseStrictDirective(parent.statements) ||
|
||||
(!isCjsJsFile(parent) && (isNodeEsmFile(parent) || ts.isExternalModule(parent)))
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Statement-position `delete`: process.env keys → process.envUnset
|
||||
* (unsetenv), pure `Record<string, T>` keys → recordKeyDelete (the
|
||||
* overflow Map delete), declared OPTIONAL fields → the field's ABSENT
|
||||
@@ -6001,7 +5972,7 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt
|
||||
obj = lowerer.coerceToExpected(obj, DYN);
|
||||
}
|
||||
if (obj.type.kind === "dyn") {
|
||||
const strict = { kind: "boolLit", value: isStrictDelete(expr), type: BOOL, loc } as const;
|
||||
const strict = { kind: "boolLit", value: isStrictCode(expr), type: BOOL, loc } as const;
|
||||
if (ts.isElementAccessExpression(target)) {
|
||||
const key = lowerer.lowerExprExpecting(target.argumentExpression, DYN);
|
||||
return {
|
||||
|
||||
@@ -112,6 +112,7 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "./optional-arguments.js";
|
||||
import { type ProtocolDispatch } from "./class-protocol-dispatch.js";
|
||||
import { ClassDynamicDispatch, trackClassFieldCreation } from "./class-dynamic-dispatch.js";
|
||||
import { guardClassAttributeWrites } from "./class-attributes.js";
|
||||
import { ClassProtocols } from "./class-protocols.js";
|
||||
import { earlyReadVarSymbols } from "./var-hoisting.js";
|
||||
import { recordTextCodecClass } from "../../ir/ir.js";
|
||||
@@ -6772,6 +6773,7 @@ export class Lowerer {
|
||||
if (this.diags.length === 0) fuseArrayPipelines(this, functions);
|
||||
finalizeClassMethodValues(this, functions);
|
||||
trackClassFieldCreation(this, functions);
|
||||
guardClassAttributeWrites(this, functions);
|
||||
|
||||
// Globals typed by a class that never REGISTERED (a JS class whose
|
||||
// collection fenced — Symbol-keyed fields, an unsupported base): the
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { isCjsJsFile, isNodeEsmFile } from "../program.js";
|
||||
|
||||
function hasUseStrictDirective(statements: readonly ts.Statement[]): boolean {
|
||||
for (const statement of statements) {
|
||||
if (!ts.isExpressionStatement(statement) || !ts.isStringLiteral(statement.expression))
|
||||
return false;
|
||||
if (statement.expression.text === "use strict") return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether `node` runs in strict mode code: inside a class body, a function
|
||||
* with a "use strict" directive, a module, or a script that starts with
|
||||
* "use strict". CommonJS JavaScript is sloppy otherwise. */
|
||||
export function isStrictCode(node: ts.Node): boolean {
|
||||
for (let parent: ts.Node | undefined = node.parent; parent; parent = parent.parent) {
|
||||
if (ts.isClassDeclaration(parent) || ts.isClassExpression(parent)) return true;
|
||||
if (
|
||||
ts.isFunctionLike(parent) &&
|
||||
parent.body &&
|
||||
ts.isBlock(parent.body) &&
|
||||
hasUseStrictDirective(parent.body.statements)
|
||||
)
|
||||
return true;
|
||||
if (ts.isSourceFile(parent)) {
|
||||
return (
|
||||
hasUseStrictDirective(parent.statements) ||
|
||||
(!isCjsJsFile(parent) && (isNodeEsmFile(parent) || ts.isExternalModule(parent)))
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import type { IrRecordShape, IrType, IrUnionDef, IrUnionDiscriminant } from "../
|
||||
import {
|
||||
BYTES_ELEMENT_NAME,
|
||||
arrayOf,
|
||||
isDynTypedRefType,
|
||||
BOOL,
|
||||
bytesOf,
|
||||
canConvertToDyn,
|
||||
@@ -1511,6 +1512,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
contextResolutions++;
|
||||
return DYN;
|
||||
}
|
||||
// Readonly<C> over a program class (Object.freeze's result type) is the
|
||||
// same instance.
|
||||
const readonlyClass = readonlyClassType(type, ctx);
|
||||
if (readonlyClass !== null) return readonlyClass;
|
||||
if (resolveTypeParam && type.flags & ts.TypeFlags.TypeParameter) {
|
||||
const bound = resolveTypeParam(type);
|
||||
if (bound) {
|
||||
@@ -5111,6 +5116,19 @@ function mentionsMappedParams(
|
||||
return false;
|
||||
}
|
||||
|
||||
function readonlyClassType(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
const alias = type.getAliasSymbol();
|
||||
if (alias?.name !== "Readonly") return null;
|
||||
const args = type.getAliasTypeArguments();
|
||||
if (args.length !== 1 || !args[0]) return null;
|
||||
const isLibAlias = ctx.checker
|
||||
.declarationsOf(alias)
|
||||
.some((d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()));
|
||||
if (!isLibAlias) return null;
|
||||
const mapped = mapType(args[0], ctx);
|
||||
return mapped !== null && isDynTypedRefType(mapped) ? mapped : null;
|
||||
}
|
||||
|
||||
function mapGenericUtilityAlias(widened: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
const { resolveTypeParam, shapes, unions } = ctx;
|
||||
if (!resolveTypeParam) return null;
|
||||
|
||||
@@ -577,6 +577,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"dyn.mapSeedEntry",
|
||||
"dyn.freeze",
|
||||
"dyn.isFrozen",
|
||||
"class.defineField",
|
||||
"class.throwReadOnly",
|
||||
"class.throwNotExtensible",
|
||||
"dyn.toString",
|
||||
"dyn.valueOf",
|
||||
"string.fromCodePoint",
|
||||
|
||||
@@ -202,6 +202,23 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"dyn.in": { argTypes: [DYN, STRING], result: BOOL },
|
||||
"dyn.freeze": { argTypes: [DYN], result: DYN },
|
||||
"dyn.isFrozen": { argTypes: [DYN], result: BOOL },
|
||||
/** Object.defineProperty on a declared class field (scr_json.c): the
|
||||
* field's current value (borrowed), its FIELD_* attribute word, the
|
||||
* instance's CLASS_* integrity word, the field name and the descriptor.
|
||||
* Validates as Node does (TypeError "Cannot redefine property: f") and
|
||||
* answers a one-property holder of the defined value and attributes (+1),
|
||||
* read back with class.definedBits and class.definedValue. */
|
||||
"class.defineField": { argTypes: [DYN, F64, F64, STRING, DYN], result: DYN },
|
||||
"class.definedBits": { argTypes: [DYN, STRING], result: F64 },
|
||||
"class.definedValue": { argTypes: [DYN, STRING], result: DYN },
|
||||
/** Whether a property descriptor has a `value` member. */
|
||||
"class.descriptorHasValue": { argTypes: [DYN], result: BOOL },
|
||||
/** Throws Node's TypeError for a write to a read-only class field: the
|
||||
* field name and the instance's class name. */
|
||||
"class.throwReadOnly": { argTypes: [STRING, STRING], result: VOID },
|
||||
/** Throws Node's TypeError for a class field initializer on a
|
||||
* non-extensible instance: the field name. */
|
||||
"class.throwNotExtensible": { argTypes: [STRING], result: VOID },
|
||||
"dyn.nativeSetNew": { argTypes: [DYN], result: DYN },
|
||||
"dyn.nativeSetIs": { argTypes: [DYN], result: BOOL },
|
||||
"dyn.nativeMapIs": { argTypes: [DYN], result: BOOL },
|
||||
|
||||
@@ -1896,8 +1896,23 @@ export type IrStmt =
|
||||
| { kind: "return"; value: IrExpr | null; awaited?: true; loc: SrcLoc }
|
||||
/** Field write `obj.f = v` — statement-only, like `assign`/`arraySet`.
|
||||
* Evaluation order: obj, then value. The old value is released; ownership
|
||||
* of a refcounted new value MOVES into the object. */
|
||||
| { kind: "fieldSet"; obj: IrExpr; className: string; field: string; value: IrExpr; loc: SrcLoc }
|
||||
* of a refcounted new value MOVES into the object. `sloppy` marks a write
|
||||
* in sloppy-mode code, which skips a read-only field instead of throwing.
|
||||
* `define` marks a class field initializer, which defines the property
|
||||
* (a non-extensible instance rejects it). `attributesChecked` marks a
|
||||
* write that already honors the instance's property attributes
|
||||
* (CLASS_ATTRIBUTES). */
|
||||
| {
|
||||
kind: "fieldSet";
|
||||
obj: IrExpr;
|
||||
className: string;
|
||||
field: string;
|
||||
value: IrExpr;
|
||||
sloppy?: true;
|
||||
define?: true;
|
||||
attributesChecked?: true;
|
||||
loc: SrcLoc;
|
||||
}
|
||||
/** Record field write `r.f = v` — mirrors `fieldSet` exactly (evaluation
|
||||
* order obj then value; old value released; refcounted new value moved
|
||||
* in), with a shape id in place of a class name. */
|
||||
@@ -4048,6 +4063,27 @@ export function isClassOwnEnumerableFieldName(name: string): boolean {
|
||||
/** Compiler-owned storage for properties added through untyped references. */
|
||||
export const DYN_CLASS_PROPERTIES = "%dynProperties";
|
||||
|
||||
/** Compiler-owned integrity state of a class instance whose hierarchy is
|
||||
* passed to Object.freeze, Object.seal, Object.preventExtensions or
|
||||
* Object.defineProperty: an f64 holding CLASS_NOT_EXTENSIBLE, CLASS_SEALED
|
||||
* and CLASS_FROZEN bits. Other hierarchies have no such field. */
|
||||
export const CLASS_ATTRIBUTES = "%attributes";
|
||||
export const CLASS_NOT_EXTENSIBLE = 1;
|
||||
export const CLASS_SEALED = 2;
|
||||
export const CLASS_FROZEN = 4;
|
||||
|
||||
/** Compiler-owned attributes of one declared field that Object.defineProperty
|
||||
* changes: an f64 holding FIELD_NOT_WRITABLE, FIELD_NOT_ENUMERABLE and
|
||||
* FIELD_NOT_CONFIGURABLE bits (zero is an ordinary data property). */
|
||||
export const CLASS_FIELD_ATTRIBUTES_PREFIX = "%attr:";
|
||||
export const FIELD_NOT_WRITABLE = 1;
|
||||
export const FIELD_NOT_ENUMERABLE = 2;
|
||||
export const FIELD_NOT_CONFIGURABLE = 4;
|
||||
|
||||
export function classFieldAttributesName(field: string): string {
|
||||
return CLASS_FIELD_ATTRIBUTES_PREFIX + field;
|
||||
}
|
||||
|
||||
/** A class capsule can always preserve its exact native identity. Its
|
||||
* optional property view additionally needs converters in both directions;
|
||||
* fields such as Maps may remain opaque without preventing the round trip. */
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../src/index.js";
|
||||
import type { IrModule } from "../src/ir/ir.js";
|
||||
|
||||
async function lowerModule(source: string, file = "main.ts"): Promise<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-class-attributes-"));
|
||||
try {
|
||||
const entry = join(dir, file);
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
return deserializeModule(await readFile(outPath, "utf8"));
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function fieldsOf(mod: IrModule, className: string): string[] {
|
||||
const cls = (mod.classes ?? []).find(
|
||||
(c) => c.name === className || c.name.endsWith(`.${className}`),
|
||||
);
|
||||
if (!cls) throw new Error(`no class ${className}`);
|
||||
return cls.fields.map((f) => f.name);
|
||||
}
|
||||
|
||||
function functionsCalling(mod: IrModule, fn: string): string[] {
|
||||
return mod.functions
|
||||
.filter((f) => JSON.stringify(f.body).includes(`"fn":"${fn}"`))
|
||||
.map((f) => f.name);
|
||||
}
|
||||
|
||||
test("only hierarchies passed to an integrity call get attribute state and write checks", async () => {
|
||||
const mod = await lowerModule(`
|
||||
export {};
|
||||
class Point { x = 1; }
|
||||
class Other { x = 1; }
|
||||
function movePoint(p: Point): void { p.x = 2; }
|
||||
function moveOther(o: Other): void { o.x = 2; }
|
||||
const p = new Point();
|
||||
Object.freeze(p);
|
||||
movePoint(p);
|
||||
moveOther(new Other());
|
||||
`);
|
||||
expect(fieldsOf(mod, "Point")).toContain("%attributes");
|
||||
expect(fieldsOf(mod, "Other")).not.toContain("%attributes");
|
||||
const checked = functionsCalling(mod, "class.throwReadOnly");
|
||||
expect(checked).toContain("movePoint");
|
||||
expect(checked).not.toContain("moveOther");
|
||||
});
|
||||
|
||||
test("a redefined field gets its own attribute word", async () => {
|
||||
const mod = await lowerModule(`
|
||||
export {};
|
||||
class Config { mode = "dev"; level = 1; }
|
||||
const c = new Config();
|
||||
Object.defineProperty(c, "mode", { writable: false });
|
||||
`);
|
||||
const fields = fieldsOf(mod, "Config");
|
||||
expect(fields).toContain("%attributes");
|
||||
expect(fields).toContain("%attr:mode");
|
||||
expect(fields).not.toContain("%attr:level");
|
||||
});
|
||||
|
||||
test("sloppy-mode writes skip a read-only field without throwing", async () => {
|
||||
const mod = await lowerModule(
|
||||
`
|
||||
class Box { constructor() { this.x = 1; } }
|
||||
const b = new Box();
|
||||
Object.freeze(b);
|
||||
b.x = 2;
|
||||
`,
|
||||
"main.cjs",
|
||||
);
|
||||
expect(fieldsOf(mod, "Box")).toContain("%attributes");
|
||||
// Class bodies are strict mode code; the top-level write is not.
|
||||
const throwing = functionsCalling(mod, "class.throwReadOnly");
|
||||
expect(throwing.length).toBeGreaterThan(0);
|
||||
expect(throwing.every((name) => name.includes("constructor"))).toBe(true);
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6004-class-instance-integrity.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6005-class-instance-integrity-sloppy.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -882,7 +882,17 @@ double scr_dyn_arr_len(const ScrDyn *d);
|
||||
ScrDyn *scr_dyn_arr_at(const ScrDyn *d, double i);
|
||||
ScrDyn *scr_dyn_arr_named_get(const ScrDyn *d, const ScrStr *key);
|
||||
void scr_dyn_obj_set(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value);
|
||||
void scr_dyn_obj_set_field(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value);
|
||||
ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor);
|
||||
void scr_dyn_class_field_attributes(ScrDyn *view, ScrStr *name, double bits);
|
||||
void scr_dyn_class_view_attributes(ScrDyn *view, double bits);
|
||||
ScrDyn *scr_class_define_field(ScrDyn *current, double field_bits, double object_bits, ScrStr *name,
|
||||
ScrDyn *descriptor);
|
||||
double scr_class_defined_bits(ScrDyn *holder, ScrStr *name);
|
||||
ScrDyn *scr_class_defined_value(ScrDyn *holder, ScrStr *name);
|
||||
bool scr_class_descriptor_has_value(ScrDyn *descriptor);
|
||||
void scr_class_throw_read_only(ScrStr *field, ScrStr *owner);
|
||||
void scr_class_throw_not_extensible(ScrStr *field);
|
||||
ScrDyn *scr_dyn_get_own_property_descriptor(ScrDyn *target, ScrDyn *key);
|
||||
ScrDyn *scr_dyn_array_proto_call(ScrDyn *target, ScrStr *method, ScrDyn *args);
|
||||
ScrDyn *scr_dyn_array_prototype(void); /* +1, installs native method adapters */
|
||||
|
||||
@@ -1585,8 +1585,12 @@ static void scr_dyn_obj_put(ScrDyn *obj, const char *key, size_t key_len, ScrDyn
|
||||
}
|
||||
if (obj->non_extensible) {
|
||||
scr_dyn_release(value);
|
||||
static const char message[] = "Cannot add property, object is not extensible";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1);
|
||||
ScrJsonBuf buffer;
|
||||
scr_jb_init(&buffer);
|
||||
scr_jb_puts(&buffer, "Cannot add property ");
|
||||
scr_jb_write(&buffer, key, key_len);
|
||||
scr_jb_puts(&buffer, ", object is not extensible");
|
||||
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer));
|
||||
return;
|
||||
}
|
||||
if (obj->v.obj.len == obj->v.obj.cap) {
|
||||
@@ -1922,9 +1926,40 @@ ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) {
|
||||
return scr_dyn_retain(object);
|
||||
}
|
||||
|
||||
/* A class instance's integrity state lives in its native layout, which only
|
||||
* statically typed calls can reach (the compiler adds the state to the
|
||||
* hierarchies they name). Through an untyped value the operation throws. */
|
||||
static bool scr_dyn_class_integrity_fence(const ScrDyn *object, const char *operation) {
|
||||
if (object->kind != SCR_DYN_TYPED_REF) return false;
|
||||
ScrJsonBuf buffer;
|
||||
scr_jb_init(&buffer);
|
||||
scr_jb_puts(&buffer, operation);
|
||||
scr_jb_puts(&buffer, " of a class instance held in an untyped value is not supported yet");
|
||||
ScrStr *message = scr_jb_finish(&buffer);
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020");
|
||||
scr_str_release(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Integrity queries on a class instance answer from its checked view, which
|
||||
* carries the instance's attributes (scr_dyn_class_view_attributes). */
|
||||
static bool scr_dyn_typed_ref_view_answer(ScrDyn *object, bool (*query)(const ScrDyn *),
|
||||
bool *answer) {
|
||||
if (object->kind != SCR_DYN_TYPED_REF) return false;
|
||||
ScrDyn *view = scr_dyn_typed_ref_materialize(object);
|
||||
*answer = scr_exc_pending() ? false : query(view);
|
||||
scr_dyn_release(view);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool scr_dyn_is_extensible_view(const ScrDyn *view) {
|
||||
return (view->kind == SCR_DYN_OBJ || view->kind == SCR_DYN_ARR) && !view->non_extensible;
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) {
|
||||
if (scr_dyn_class_reflection_fence(object)) return NULL;
|
||||
if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.preventExtensions"); return NULL; }
|
||||
if (scr_dyn_class_integrity_fence(object, "Object.preventExtensions")) return NULL;
|
||||
if (object->kind == SCR_DYN_OBJ || object->kind == SCR_DYN_ARR) {
|
||||
object->non_extensible = true;
|
||||
if (object->kind == SCR_DYN_ARR && object->v.arr.properties)
|
||||
@@ -1936,12 +1971,15 @@ ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) {
|
||||
bool scr_dyn_is_extensible(ScrDyn *object) {
|
||||
if (scr_dyn_class_reflection_fence(object)) return false;
|
||||
if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.isExtensible"); return false; }
|
||||
return (object->kind == SCR_DYN_OBJ || object->kind == SCR_DYN_ARR) && !object->non_extensible;
|
||||
bool answer;
|
||||
if (scr_dyn_typed_ref_view_answer(object, scr_dyn_is_extensible_view, &answer)) return answer;
|
||||
return scr_dyn_is_extensible_view(object);
|
||||
}
|
||||
|
||||
ScrDyn *scr_dyn_seal(ScrDyn *object) {
|
||||
if (scr_dyn_class_reflection_fence(object)) return NULL;
|
||||
if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.seal"); return NULL; }
|
||||
if (scr_dyn_class_integrity_fence(object, "Object.seal")) return NULL;
|
||||
if (object->symbol_properties) scr_dyn_release(scr_dyn_seal(object->symbol_properties));
|
||||
if (object->kind == SCR_DYN_OBJ) {
|
||||
object->non_extensible = true;
|
||||
@@ -1958,6 +1996,8 @@ ScrDyn *scr_dyn_seal(ScrDyn *object) {
|
||||
bool scr_dyn_is_sealed(const ScrDyn *object) {
|
||||
if (scr_dyn_class_reflection_fence(object)) return false;
|
||||
if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.isSealed"); return false; }
|
||||
bool answer;
|
||||
if (scr_dyn_typed_ref_view_answer((ScrDyn *)object, scr_dyn_is_sealed, &answer)) return answer;
|
||||
if (object->symbol_properties && !scr_dyn_is_sealed(object->symbol_properties)) return false;
|
||||
if (object->kind == SCR_DYN_ARR)
|
||||
return object->non_extensible && (object->v.arr.len == 0 || object->v.arr.sealed) &&
|
||||
@@ -4243,6 +4283,25 @@ void scr_dyn_obj_set(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value
|
||||
scr_dyn_obj_put(obj, key, key_len, value);
|
||||
}
|
||||
|
||||
/* A class instance's view takes its native field values whatever the
|
||||
* attributes copied into it (a frozen instance's view is built the same way
|
||||
* as any other). Consumes value. */
|
||||
void scr_dyn_obj_set_field(ScrDyn *obj, const char *key, size_t key_len, ScrDyn *value) {
|
||||
ScrDynEntry *existing = obj->v.obj.index ? scr_dyn_find_entry(obj, key, key_len)
|
||||
: scr_dyn_find_linear_entry(obj, key, key_len);
|
||||
if (existing && !existing->accessor) {
|
||||
scr_dyn_region_store(obj, value);
|
||||
ScrDyn *old = existing->value;
|
||||
existing->value = value;
|
||||
scr_dyn_release(old);
|
||||
return;
|
||||
}
|
||||
bool non_extensible = obj->non_extensible;
|
||||
obj->non_extensible = false;
|
||||
scr_dyn_obj_put(obj, key, key_len, value);
|
||||
obj->non_extensible = non_extensible;
|
||||
}
|
||||
|
||||
static ScrStr *scr_dyn_property_key(const ScrDyn *key) {
|
||||
return scr_dyn_string_coerce_js(key);
|
||||
}
|
||||
@@ -6036,9 +6095,12 @@ ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor)
|
||||
(!accessor && ((!prior->writable && writable) ||
|
||||
(!prior->writable && value && !scr_dyn_property_same_value(prior->value, value)))))) {
|
||||
scr_dyn_descriptor_fields_drop(fields);
|
||||
ScrJsonBuf buffer;
|
||||
scr_jb_init(&buffer);
|
||||
scr_jb_puts(&buffer, "Cannot redefine property: ");
|
||||
scr_jb_write(&buffer, name->data, name->len);
|
||||
scr_str_release(name);
|
||||
static const char msg[] = "Cannot redefine property";
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
|
||||
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer));
|
||||
return NULL;
|
||||
}
|
||||
if (prior) {
|
||||
@@ -13864,6 +13926,7 @@ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrSt
|
||||
|
||||
ScrDyn *scr_dyn_freeze(ScrDyn *value) {
|
||||
if (scr_dyn_class_reflection_fence(value)) return NULL;
|
||||
if (scr_dyn_class_integrity_fence(value, "Object.freeze")) return NULL;
|
||||
if (value->symbol_properties) scr_dyn_release(scr_dyn_freeze(value->symbol_properties));
|
||||
if (value->kind == SCR_DYN_ARR) {
|
||||
value->non_extensible = true;
|
||||
@@ -13891,6 +13954,8 @@ ScrDyn *scr_dyn_freeze(ScrDyn *value) {
|
||||
|
||||
bool scr_dyn_is_frozen(const ScrDyn *value) {
|
||||
if (scr_dyn_class_reflection_fence(value)) return false;
|
||||
bool answer;
|
||||
if (scr_dyn_typed_ref_view_answer((ScrDyn *)value, scr_dyn_is_frozen, &answer)) return answer;
|
||||
if (value->symbol_properties && !scr_dyn_is_frozen(value->symbol_properties)) return false;
|
||||
if (value->kind == SCR_DYN_ARR)
|
||||
return scr_dyn_is_sealed(value) && (value->v.arr.len == 0 || value->v.arr.frozen) &&
|
||||
@@ -13944,3 +14009,127 @@ ScrDyn *scr_caught_to_dyn(const ScrCaught *c) {
|
||||
return scr_dyn_new_obj();
|
||||
}
|
||||
}
|
||||
|
||||
/* ── class instance attributes ───────────────────────────────────────────
|
||||
* A hierarchy passed to Object.freeze, Object.seal, Object.preventExtensions
|
||||
* or Object.defineProperty keeps an integrity state per instance and an
|
||||
* attribute word per redefined field (CLASS_* and FIELD_* bits in the
|
||||
* compiler's ir.ts). The checked view of an instance carries them, so
|
||||
* reflection over the view answers as Node does. */
|
||||
enum {
|
||||
SCR_CLASS_NOT_EXTENSIBLE = 1,
|
||||
SCR_CLASS_SEALED = 2,
|
||||
SCR_CLASS_FROZEN = 4,
|
||||
SCR_FIELD_NOT_WRITABLE = 1,
|
||||
SCR_FIELD_NOT_ENUMERABLE = 2,
|
||||
SCR_FIELD_NOT_CONFIGURABLE = 4,
|
||||
};
|
||||
|
||||
void scr_dyn_class_field_attributes(ScrDyn *view, ScrStr *name, double bits) {
|
||||
unsigned b = (unsigned)bits;
|
||||
if (b == 0 || view->kind != SCR_DYN_OBJ) return;
|
||||
ScrDynEntry *entry = scr_dyn_entry(view, name);
|
||||
if (!entry) return;
|
||||
if (b & SCR_FIELD_NOT_WRITABLE) entry->writable = false;
|
||||
if (b & SCR_FIELD_NOT_ENUMERABLE) entry->enumerable = false;
|
||||
if (b & SCR_FIELD_NOT_CONFIGURABLE) entry->configurable = false;
|
||||
}
|
||||
|
||||
void scr_dyn_class_view_attributes(ScrDyn *view, double bits) {
|
||||
unsigned b = (unsigned)bits;
|
||||
if (b == 0 || view->kind != SCR_DYN_OBJ) return;
|
||||
view->non_extensible = true;
|
||||
for (size_t i = 0; i < view->v.obj.len; i++) {
|
||||
ScrDynEntry *entry = &view->v.obj.entries[i];
|
||||
if (b & (SCR_CLASS_SEALED | SCR_CLASS_FROZEN)) entry->configurable = false;
|
||||
if ((b & SCR_CLASS_FROZEN) && !entry->accessor) entry->writable = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Object.defineProperty on a declared class field. The descriptor applies to
|
||||
* a one-property object holding the field's current value and attributes,
|
||||
* so validation and errors match ordinary objects. Answers that object (+1),
|
||||
* or NULL with a pending exception. */
|
||||
ScrDyn *scr_class_define_field(ScrDyn *current, double field_bits, double object_bits, ScrStr *name,
|
||||
ScrDyn *descriptor) {
|
||||
unsigned f = (unsigned)field_bits, o = (unsigned)object_bits;
|
||||
ScrDyn *holder = scr_dyn_new_obj();
|
||||
scr_dyn_obj_set(holder, name->data, name->len, scr_dyn_retain(current));
|
||||
ScrDynEntry *entry = scr_dyn_entry(holder, name);
|
||||
entry->writable = !(f & SCR_FIELD_NOT_WRITABLE) && !(o & SCR_CLASS_FROZEN);
|
||||
entry->enumerable = !(f & SCR_FIELD_NOT_ENUMERABLE);
|
||||
entry->configurable =
|
||||
!(f & SCR_FIELD_NOT_CONFIGURABLE) && !(o & (SCR_CLASS_SEALED | SCR_CLASS_FROZEN));
|
||||
ScrDyn *key = scr_dyn_new_str(name);
|
||||
ScrDyn *defined = scr_dyn_define_property(holder, key, descriptor);
|
||||
scr_dyn_release(key);
|
||||
if (!defined) {
|
||||
scr_dyn_release(holder);
|
||||
return NULL;
|
||||
}
|
||||
scr_dyn_release(defined);
|
||||
entry = scr_dyn_entry(holder, name);
|
||||
if (entry->accessor) {
|
||||
scr_dyn_release(holder);
|
||||
ScrJsonBuf buffer;
|
||||
scr_jb_init(&buffer);
|
||||
scr_jb_puts(&buffer, "Object.defineProperty with an accessor descriptor for the declared field '");
|
||||
scr_jb_write(&buffer, name->data, name->len);
|
||||
scr_jb_puts(&buffer, "' is not supported yet");
|
||||
ScrStr *message = scr_jb_finish(&buffer);
|
||||
scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020");
|
||||
scr_str_release(message);
|
||||
return NULL;
|
||||
}
|
||||
return holder;
|
||||
}
|
||||
|
||||
/* The field attribute word of a scr_class_define_field result. */
|
||||
double scr_class_defined_bits(ScrDyn *holder, ScrStr *name) {
|
||||
const ScrDynEntry *entry = scr_dyn_entry(holder, name);
|
||||
unsigned bits = 0;
|
||||
if (!entry->writable) bits |= SCR_FIELD_NOT_WRITABLE;
|
||||
if (!entry->enumerable) bits |= SCR_FIELD_NOT_ENUMERABLE;
|
||||
if (!entry->configurable) bits |= SCR_FIELD_NOT_CONFIGURABLE;
|
||||
return (double)bits;
|
||||
}
|
||||
|
||||
/* The value a scr_class_define_field result holds (+1). */
|
||||
ScrDyn *scr_class_defined_value(ScrDyn *holder, ScrStr *name) {
|
||||
return scr_dyn_retain(scr_dyn_entry(holder, name)->value);
|
||||
}
|
||||
|
||||
/* Whether a property descriptor names a value (Node's ToPropertyDescriptor
|
||||
* reads `value` with [[HasProperty]]). */
|
||||
bool scr_class_descriptor_has_value(ScrDyn *descriptor) {
|
||||
if (descriptor->kind == SCR_DYN_TYPED_REF) {
|
||||
ScrDyn *view = scr_dyn_typed_ref_materialize(descriptor);
|
||||
bool has = !scr_exc_pending() && scr_class_descriptor_has_value(view);
|
||||
scr_dyn_release(view);
|
||||
return has;
|
||||
}
|
||||
return descriptor->kind == SCR_DYN_OBJ && scr_dyn_obj_get(descriptor, "value", 5) != NULL;
|
||||
}
|
||||
|
||||
/* Node's TypeError for a write to a read-only field of a class instance. */
|
||||
void scr_class_throw_read_only(ScrStr *field, ScrStr *owner) {
|
||||
ScrJsonBuf buffer;
|
||||
scr_jb_init(&buffer);
|
||||
scr_jb_puts(&buffer, "Cannot assign to read only property '");
|
||||
scr_jb_write(&buffer, field->data, field->len);
|
||||
scr_jb_puts(&buffer, "' of object '#<");
|
||||
scr_jb_write(&buffer, owner->data, owner->len);
|
||||
scr_jb_puts(&buffer, ">'");
|
||||
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer));
|
||||
}
|
||||
|
||||
/* Node's TypeError for a class field initializer on a non-extensible
|
||||
* instance (a base constructor froze or sealed `this`). */
|
||||
void scr_class_throw_not_extensible(ScrStr *field) {
|
||||
ScrJsonBuf buffer;
|
||||
scr_jb_init(&buffer);
|
||||
scr_jb_puts(&buffer, "Cannot define property ");
|
||||
scr_jb_write(&buffer, field->data, field->len);
|
||||
scr_jb_puts(&buffer, ", object is not extensible");
|
||||
scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&buffer));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
export {};
|
||||
import { inspect } from "node:util";
|
||||
|
||||
const message = (run: () => void): string => {
|
||||
try {
|
||||
run();
|
||||
return "no error";
|
||||
} catch (e) {
|
||||
return `${e instanceof TypeError ? "TypeError" : "Error"}: ${(e as Error).message}`;
|
||||
}
|
||||
};
|
||||
|
||||
class Point {
|
||||
x = 1;
|
||||
y = 2;
|
||||
label = "p";
|
||||
move(dx: number): void {
|
||||
this.x += dx;
|
||||
}
|
||||
}
|
||||
|
||||
const p = new Point();
|
||||
console.log(Object.isFrozen(p), Object.isSealed(p), Object.isExtensible(p));
|
||||
const same = Object.freeze(p);
|
||||
console.log(same === p, Object.isFrozen(p), Object.isSealed(p), Object.isExtensible(p));
|
||||
console.log(message(() => {
|
||||
p.x = 5;
|
||||
}));
|
||||
console.log(message(() => p.move(1)));
|
||||
console.log(p.x, Object.keys(p));
|
||||
const q = new Point();
|
||||
q.x = 9;
|
||||
console.log(q.x, Object.isFrozen(q));
|
||||
|
||||
class Point3 extends Point {
|
||||
z = 3;
|
||||
}
|
||||
const p3 = new Point3();
|
||||
Object.freeze(p3);
|
||||
console.log(message(() => {
|
||||
p3.z = 1;
|
||||
}));
|
||||
const asBase: Point = new Point3();
|
||||
Object.freeze(asBase);
|
||||
console.log(message(() => {
|
||||
asBase.y = 0;
|
||||
}));
|
||||
console.log(asBase.y);
|
||||
|
||||
class Counter {
|
||||
n = 0;
|
||||
}
|
||||
const c = Object.seal(new Counter());
|
||||
c.n = 5;
|
||||
console.log(c.n, Object.isSealed(c), Object.isFrozen(c), Object.isExtensible(c));
|
||||
const d = new Counter();
|
||||
Object.preventExtensions(d);
|
||||
d.n = 2;
|
||||
console.log(d.n, Object.isExtensible(d), Object.isSealed(d), Object.isFrozen(d));
|
||||
const loose: any = d;
|
||||
console.log(message(() => {
|
||||
loose.extra = 1;
|
||||
}));
|
||||
|
||||
class Config {
|
||||
mode = "dev";
|
||||
level = 1;
|
||||
}
|
||||
const cfg = new Config();
|
||||
Object.defineProperty(cfg, "mode", { value: "prod", writable: false });
|
||||
console.log(cfg.mode, Object.keys(cfg), Object.values(cfg));
|
||||
console.log(message(() => {
|
||||
cfg.mode = "x";
|
||||
}));
|
||||
Object.defineProperty(cfg, "level", { enumerable: false });
|
||||
console.log(Object.keys(cfg), Object.entries(cfg), cfg.level);
|
||||
Object.defineProperty(cfg, "level", { configurable: false });
|
||||
console.log(message(() => Object.defineProperty(cfg, "level", { enumerable: true })));
|
||||
console.log(JSON.stringify(Object.getOwnPropertyDescriptor(cfg, "mode")));
|
||||
console.log(JSON.stringify(Object.getOwnPropertyDescriptor(cfg, "level")));
|
||||
cfg.level = 7;
|
||||
console.log(cfg.level);
|
||||
|
||||
const fz = Object.freeze(new Config());
|
||||
console.log(message(() => Object.defineProperty(fz, "level", { value: 2 })));
|
||||
console.log(message(() => Object.defineProperty(fz, "level", { value: 1 })));
|
||||
console.log(fz.level, JSON.stringify(Object.getOwnPropertyDescriptor(fz, "level")));
|
||||
|
||||
class Frozen {
|
||||
a = 1;
|
||||
constructor() {
|
||||
Object.freeze(this);
|
||||
}
|
||||
}
|
||||
const frozenInCtor = new Frozen();
|
||||
console.log(frozenInCtor.a, Object.isFrozen(frozenInCtor));
|
||||
console.log(message(() => {
|
||||
frozenInCtor.a = 2;
|
||||
}));
|
||||
|
||||
class Extended extends Frozen {
|
||||
b = 2;
|
||||
}
|
||||
console.log(message(() => new Extended()));
|
||||
|
||||
class Empty {}
|
||||
const empty = new Empty();
|
||||
console.log(Object.isExtensible(empty), Object.isSealed(empty), Object.isFrozen(empty));
|
||||
Object.preventExtensions(empty);
|
||||
console.log(Object.isExtensible(empty), Object.isSealed(empty), Object.isFrozen(empty));
|
||||
|
||||
class Totals {
|
||||
count = 1;
|
||||
items: string[] = [];
|
||||
}
|
||||
const t = new Totals();
|
||||
Object.freeze(t);
|
||||
console.log(message(() => {
|
||||
t.count += 1;
|
||||
}));
|
||||
console.log(message(() => {
|
||||
t.count++;
|
||||
}));
|
||||
console.log(message(() => {
|
||||
({ value: t.count } = { value: 5 });
|
||||
}));
|
||||
t.items.push("still mutable");
|
||||
console.log(t.count, t.items, Object.isFrozen(t.items));
|
||||
console.log(JSON.stringify(Object.getOwnPropertyDescriptor(t, "count")));
|
||||
|
||||
class Holder<T> {
|
||||
value: T;
|
||||
constructor(value: T) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
const h = Object.freeze(new Holder("text"));
|
||||
console.log(h.value, Object.isFrozen(h));
|
||||
const h2 = new Holder(4);
|
||||
Object.seal(h2);
|
||||
h2.value = 5;
|
||||
console.log(h2.value, Object.isSealed(h2));
|
||||
|
||||
class Settings {
|
||||
mode = "dev";
|
||||
level = 1;
|
||||
}
|
||||
const settings = new Settings();
|
||||
console.log(settings);
|
||||
Object.defineProperty(settings, "level", { enumerable: false });
|
||||
console.log(settings, inspect(settings));
|
||||
console.log([settings], { settings });
|
||||
|
||||
class Pair {
|
||||
x = 1;
|
||||
y = 2;
|
||||
}
|
||||
const pair = new Pair();
|
||||
Object.freeze(pair);
|
||||
console.log(pair, Object.isFrozen(pair));
|
||||
const sealed = new Pair();
|
||||
(sealed as any).extra = "added";
|
||||
Object.seal(sealed);
|
||||
console.log(sealed, Object.isSealed(sealed));
|
||||
|
||||
// JSON.stringify reads the same view: non-enumerable fields are skipped.
|
||||
console.log(JSON.stringify(cfg), JSON.stringify(settings));
|
||||
console.log(JSON.stringify({ settings, pair, sealed }));
|
||||
console.log(JSON.stringify([fz, t]));
|
||||
@@ -0,0 +1,40 @@
|
||||
class Box {
|
||||
constructor() {
|
||||
this.x = 1;
|
||||
}
|
||||
bump() {
|
||||
this.x = 2;
|
||||
}
|
||||
}
|
||||
|
||||
const b = new Box();
|
||||
Object.freeze(b);
|
||||
b.x = 5;
|
||||
console.log(b.x, Object.isFrozen(b));
|
||||
|
||||
function strictWrite() {
|
||||
"use strict";
|
||||
try {
|
||||
b.x = 6;
|
||||
} catch (e) {
|
||||
console.log(e instanceof TypeError, e.message);
|
||||
}
|
||||
}
|
||||
strictWrite();
|
||||
|
||||
try {
|
||||
b.bump();
|
||||
} catch (e) {
|
||||
console.log("class body:", e.message);
|
||||
}
|
||||
console.log(b.x);
|
||||
|
||||
const s = new Box();
|
||||
Object.seal(s);
|
||||
s.x = 3;
|
||||
console.log(s.x, Object.isSealed(s), Object.isFrozen(s));
|
||||
|
||||
const plain = new Box();
|
||||
Object.defineProperty(plain, "x", { writable: false });
|
||||
plain.x = 9;
|
||||
console.log(plain.x, JSON.stringify(Object.getOwnPropertyDescriptor(plain, "x")));
|
||||
@@ -2226,6 +2226,8 @@
|
||||
"6001-json-stringify-js-class-instances.js": 4668,
|
||||
"6002-inspect-class-added-properties.ts": 6044,
|
||||
"6003-inspect-js-class-properties.js": 2752,
|
||||
"6004-class-instance-integrity.ts": 10548,
|
||||
"6005-class-instance-integrity-sloppy.cjs": 7466,
|
||||
"601-closures-loops.ts": 1788,
|
||||
"6010-match-metadata.ts": 3990,
|
||||
"6011-match-metadata-js.js": 3660,
|
||||
@@ -5311,6 +5313,8 @@
|
||||
"6001-json-stringify-js-class-instances.js": 7469,
|
||||
"6002-inspect-class-added-properties.ts": 9670,
|
||||
"6003-inspect-js-class-properties.js": 4403,
|
||||
"6004-class-instance-integrity.ts": 16877,
|
||||
"6005-class-instance-integrity-sloppy.cjs": 11946,
|
||||
"601-closures-loops.ts": 2297,
|
||||
"6010-match-metadata.ts": 5450,
|
||||
"6011-match-metadata-js.js": 4890,
|
||||
|
||||
Reference in New Issue
Block a user