[nonextensible-applies-to-private] Adding private field to non-extensible object throws (#4577)

* [nonextensible-applies-to-private] Adding private field to non-extensible object throws

* add feature nonextensible-applies-to-private

* extending tests in response to reviewer suggestions

* use const instead of let
This commit is contained in:
Mark S. Miller 2025-09-22 12:58:55 -07:00 committed by GitHub
parent e0d8f66a2b
commit 822589b1ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 231 additions and 35 deletions

View File

@ -88,6 +88,10 @@ upsert
# https://github.com/tc39/proposal-immutable-arraybuffer
immutable-arraybuffer
# Non-extensible Applies to Private
# https://github.com/tc39/proposal-nonextensible-applies-to-private
nonextensible-applies-to-private
## Standard language features
#
# Language features that have been included in a published version of the

View File

@ -1,31 +0,0 @@
// Copyright (C) 2019 Caio Lima. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
description: It is possible to add private fields on frozen objects
esid: sec-define-field
info: |
DefineField(receiver, fieldRecord)
...
8. If fieldName is a Private Name,
a. Perform ? PrivateFieldAdd(fieldName, receiver, initValue).
9. Else,
a. Assert: IsPropertyKey(fieldName) is true.
b. Perform ? CreateDataPropertyOrThrow(receiver, fieldName, initValue).
10. Return.
features: [class, class-fields-private, class-fields-public]
flags: [onlyStrict]
---*/
class Test {
f = this;
#g = (Object.freeze(this), "Test262");
get g() {
return this.#g;
}
}
let t = new Test();
assert.sameValue(t.f, t);
assert.sameValue(t.g, "Test262");

View File

@ -0,0 +1,117 @@
// Copyright (C) 2019 Caio Lima. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
description: It is not possible to add private fields on non-extensible objects
esid: sec-define-field
info: |
1.1 PrivateFieldAdd ( O, P, value )
1. If O.[[Extensible]] is false, throw a TypeError exception.
...
features:
- class
- class-fields-private
- class-fields-public
- nonextensible-applies-to-private
flags: [onlyStrict]
---*/
// Analogous to
// test/language/statements/class/subclass/private-class-field-on-nonextensible-return-override.js
class NonExtensibleBase {
constructor(seal) {
if (seal) Object.preventExtensions(this);
}
}
// extend superclass with private instance data field
class ClassWithPrivateField extends NonExtensibleBase {
#val;
constructor(seal) {
super(seal);
this.#val = 42;
}
val() {
return this.#val;
}
}
const t = new ClassWithPrivateField(false);
// extensible objects can be extended
assert.sameValue(t.val(), 42);
// where superclass prevented extensions & subclass extended
assert.throws(TypeError, function () {
new ClassWithPrivateField(true);
});
// extend superclass with private instance method
class ClassWithPrivateMethod extends NonExtensibleBase {
constructor(seal) {
super(seal);
}
// private methods are on the instance, so will fail
#privateMethod() {
return 42;
};
// public methods are on the prototype, so are ok.
publicMethod() {
return this.#privateMethod();
}
}
const m = new ClassWithPrivateMethod(false);
// extensible objects can be extended
assert.sameValue(m.publicMethod(), 42);
// where superclass prevented extensions & subclass extended
assert.throws(TypeError, function () {
new ClassWithPrivateMethod(true);
});
// extend superclass with private instance accessor
class ClassWithPrivateAccessor extends NonExtensibleBase {
constructor(seal) {
super(seal);
}
// private accessors are on the instance, so will fail
get #privateAccessor() {
return 42;
};
// public accessors are on the prototype, so are ok.
get publicAccessor() {
return this.#privateAccessor;
}
}
const a = new ClassWithPrivateAccessor(false);
// extensible objects can be extended
assert.sameValue(m.publicAccessor, 42);
// where superclass prevented extensions & subclass extended
assert.throws(TypeError, function () {
new ClassWithPrivateAccessor(true);
});
// base class private instance data field
class TestNonExtensibleData {
#g = (Object.preventExtensions(this), "Test262");
}
assert.throws(TypeError, function () {
new TestNonExtensibleData();
});
// base class with private static data field
assert.throws(TypeError, function () {
class TestNonExtensibleStaticData {
static #g = (Object.preventExtensions(TestNonExtensibleStaticData), "Test262");
}
});

View File

@ -0,0 +1,100 @@
// Copyright (C) 2019 Caio Lima. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
description: It is not possible to add private fields on non-extensible objects via return override
esid: sec-define-field
info: |
1.1 PrivateFieldAdd ( O, P, value )
1. If O.[[Extensible]] is false, throw a TypeError exception.
...
features:
- class
- class-fields-private
- class-fields-public
- nonextensible-applies-to-private
flags: [onlyStrict]
---*/
// Analogous to
// test/language/statements/class/elements/private-class-field-on-nonextensible-objects.js
class TrojanBase {
constructor(obj) {
return obj;
}
}
// extend superclass with private instance data field
class ClassWithPrivateField extends TrojanBase {
#val;
constructor(obj) {
super(obj);
this.#val = 42;
}
val() {
return this.#val;
}
}
const t = new ClassWithPrivateField({});
// extensible objects can be extended
assert.sameValue(t.val(), 42);
// where superclass prevented extensions & subclass extended
assert.throws(TypeError, function () {
new ClassWithPrivateField(Object.preventExtensions({}));
});
// extend superclass with private instance method
class ClassWithPrivateMethod extends TrojanBase {
constructor(obj) {
super(obj);
}
// private methods are on the instance, so will fail
#privateMethod() {
return 42;
};
// public methods are on the prototype, so are ok.
publicMethod() {
return this.#privateMethod();
}
}
const m = new ClassWithPrivateMethod({});
// extensible objects can be extended
assert.sameValue(m.publicMethod(), 42);
// where superclass prevented extensions & subclass extended
assert.throws(TypeError, function () {
new ClassWithPrivateMethod(Object.preventExtensions({}));
});
// extend superclass with private instance accessor
class ClassWithPrivateAccessor extends TrojanBase {
constructor(obj) {
super(obj);
}
// private accessors are on the instance, so will fail
get #privateAccessor() {
return 42;
};
// public accessors are on the prototype, so are ok.
get publicAccessor() {
return this.#privateAccessor;
}
}
const a = new ClassWithPrivateAccessor({});
// extensible objects can be extended
assert.sameValue(m.publicAccessor, 42);
// where superclass prevented extensions & subclass extended
assert.throws(TypeError, function () {
new ClassWithPrivateAccessor(Object.preventExtensions({}));
});

View File

@ -2,6 +2,11 @@
// This code is governed by the BSD license found in the LICENSE file.
/*---
features:
- class
- class-fields-private
- class-fields-public
- nonextensible-applies-to-private
flags:
- noStrict
description: |
@ -32,8 +37,8 @@ class A extends OverrideBase {
}
var obj = {};
Object.seal(obj);
new A(obj); // Add #a to obj, but not g.
Object.seal(obj);
assert.sameValue('g' in obj, false);
assert.sameValue(A.gs(obj), 1);
A.inca(obj);
@ -67,8 +72,10 @@ assert.sameValue(A.gs(proxy), 2)
var target = { a: 10 };
Object.freeze(target);
new A(target);
assert.sameValue(Object.isFrozen(target), true)
assert.throws(TypeError, function () {
new A(target);
});
assert.sameValue(Object.isFrozen(target), true);
var getOwnKeys = [];
var proxy = new Proxy(target, {
@ -80,4 +87,3 @@ var proxy = new Proxy(target, {
Object.isFrozen(proxy);
assert.sameValue(getOwnKeys.length, 1);