Files
scriptc/tests/corpus/2521-private-statics-class-expression.ts
Chris Tate 5ba6cffcd4 Private statics resolve through provably-exact class values: const-bound class expressions, class aliases, and rebindable decorated names
- staticCallOn and lowerClassValueProperty fenced every #private static access with a through-a-VALUE receiver, so C.#m()/C.#f on a const-bound class expression, A.#f through a const alias of the class, and Example.#m() under a replacing decorator all died with SC1090 while the direct class-name spelling compiled.
- The static-WRITE path already knew the sound rule (exactClassOfReceiver plus the leaf-class arm); classValueIsExactlyOwn now shares it with the call and read paths: a receiver that provably IS the class, or a class with no strict descendants, always carries the declaring class's brand — the fence stays for slots a subclass can inhabit, where Node throws TypeError.
- Rebindable decorated names ride the leaf arm for free: replacement decorators fence any class with subclasses before minting the rebindable global, so the decoration result is always the declaring class object.
- The read path additionally gains the subclass-spelling fence (reading an inherited private static through a subclass-named binding) that the call and direct-read paths already had.
- Fixtures 2520-2523 pin the four call forms (class-name calls including static field initializers and subclassed declarers, const-bound class expressions with reads/writes/calls, decorated classes under both replacing and void decorators, aliased class values local/module/param) byte-identical vs Node on both backends; the private-members diagnostics fixture repins the residual fence on a descendant-capable slot.
2026-07-23 16:37:51 -05:00

40 lines
1.1 KiB
TypeScript

// #private statics on CLASS EXPRESSIONS bound to a const: the binding IS
// the class (a const initialized by the class expression can never hold a
// descendant), so `C.#m()` calls, `C.#f` reads, and `C.#f = v` writes all
// resolve exactly — including from constructors and instance methods.
// Static field initializers stay self-contained (referencing the outer
// const during them is Node's TDZ), so the #private calls run afterwards.
const C = class {
static #scale = 3;
static #mul(n: number): number {
return n * C.#scale;
}
static apply(n: number): number {
return C.#mul(n);
}
static rescale(s: number): number {
C.#scale = s;
return C.#scale;
}
#v: number;
constructor(v: number) {
this.#v = C.#mul(v);
}
read(): number {
return this.#v;
}
};
console.log(C.apply(5));
console.log(new C(2).read());
console.log(C.rescale(7));
console.log(C.apply(5));
// The parenthesized (non-whole-initializer) spelling binds the same way.
const P = (class {
static #hidden = "quiet";
static tag(): string {
return P.#hidden.toUpperCase();
}
});
console.log(P.tag());