fix(codegen): resolve private names in class heritage (#25588)

### Summary

Fix private-member mangling for class heritage expressions.

The code generator previously activated a class's private-member mapping before emitting that class's decorators and `extends` expression. A private identifier in those expressions is evaluated in the enclosing lexical class scope, not the class currently being declared.

This change:

- reserves each class's mapping ID in AST traversal order;
- activates that mapping only immediately before emitting its class body;
- adds a mangler/codegen regression test covering a nested class with identically named private members.

### Reproduction

```js
class HasOuterBrand {}
class MissingOuterBrand {}

class Outer {
  #x;

  create(o) {
    return class Inner extends (
      #x in o ? HasOuterBrand : MissingOuterBrand
    ) {
      #x;
    };
  }
}
```

`#x` in `Inner`'s heritage expression refers to `Outer.#x`. `Inner.#x` is not in scope until the `Inner` class body.

### Scope model

```text
Outer class body: active private mapping
  Outer.#x -> #e

  create(o) {
    class Inner extends (#x in o ? ... : ...)
                        │
                        └─ resolves in Outer scope: #e

    Inner class body: active private mapping
      Inner.#x -> #t
  }
```

### Before

Codegen entered `Inner` before emitting the entire class header:

```rust
p.enter_class();
p.print_decorators(&self.decorators, ctx);
...
heritage.expression.print_expr(...);
...
self.body.print(p, ctx);
```

That made private-identifier lookup prefer `Inner`'s mapping while printing `extends`, producing the wrong brand:

```js
return class t extends (#t in e ? HasOuterBrand : MissingOuterBrand) {
  #t;
};
```

The `#t` in the heritage expression is invalid because `Inner.#t` is not yet in scope.

### After

The class ID is reserved first, preserving mapping IDs assigned in traversal order, but the class mapping is pushed only for the body:

```rust
let class_id = p.reserve_class();

p.print_decorators(&self.decorators, ctx);
...
heritage.expression.print_expr(...);

p.enter_class(class_id);
self.body.print(p, ctx);
p.exit_class();
```

```text
Class-ID allocation              Active private-name scope
────────────────────             ─────────────────────────────
reserve Outer ID                 enclosing class / none
  reserve nested header IDs      enclosing class / none
enter Outer ID                   Outer body
  enter Inner ID                 Inner body
```

Separating reservation from activation matters: moving allocation until after heritage could shift IDs for nested classes encountered in a heritage expression and break the mangler's precomputed mapping association.

### Regression coverage

The existing minifier mangler integration harness now snapshots this case with private-member mappings enabled. It confirms that the generated heritage expression uses Outer’s mapping (`#e`) while Inner’s field uses its distinct mapping (`#t`):

```js
class Outer {
  #e;
  create(e) {
    return class t extends (#e in e ? HasOuterBrand : MissingOuterBrand) {
      #t;
    };
  }
}
```
This commit is contained in:
camc314 committed 2026-08-13 13:17:31 +00:00
1 parent c002f29226
commit c1369a71ff
3 files changed
+26 -1

No files matched your search

+1 -1
View File
@@ -2540,7 +2540,6 @@ impl Gen for Class<'_> {
let wrap = self.is_expression() && (p.start_of_stmt == n || p.start_of_default_export == n);
let ctx = ctx.and_forbid_call(false);
p.wrap(wrap, |p| {
p.enter_class();
p.print_decorators(&self.decorators, ctx);
p.print_space_before_identifier();
p.add_source_mapping(self.span);
@@ -2574,6 +2573,7 @@ impl Gen for Class<'_> {
p.print_list(&self.implements, ctx);
}
p.print_soft_space();
p.enter_class();
self.body.print(p, ctx);
p.needs_semicolon = false;
p.exit_class();
+5
View File
@@ -199,6 +199,11 @@ fn private_member_mangling() {
"class Outer { #outerField = 1; inner() { return class Inner { #innerField = 2; get() { return this.#innerField; } }; } }",
"class Outer { #shared = 1; getInner() { let self = this; return class { method() { return self.#shared; } }; } }",
"class Outer { #shared = 1; getInner() { return class { #shared = 2; method() { return this.#shared; } }; } }",
// Private names in a class heritage expression resolve in the enclosing class scope.
"class HasOuterBrand {} class MissingOuterBrand {} class Outer { #x; create(o) { return class Inner extends (#x in o ? HasOuterBrand : MissingOuterBrand) { #x; }; } }",
// Classes in a heritage expression receive their private-member mappings before the
// outer class, matching semantic traversal order.
"class Outer extends (class Inner { #a; #e; }) { #a; }",
// Mixed public and private
"class Foo { publicField = 1; #privateField = 2; getSum() { return this.publicField + this.#privateField; } }",
// Test same names across different classes should reuse mangled names
@@ -86,6 +86,26 @@ class Outer {
}
}
class HasOuterBrand {} class MissingOuterBrand {} class Outer { #x; create(o) { return class Inner extends (#x in o ? HasOuterBrand : MissingOuterBrand) { #x; }; } }
class HasOuterBrand {}
class MissingOuterBrand {}
class Outer {
#e;
create(e) {
return class t extends (#e in e ? HasOuterBrand : MissingOuterBrand) {
#t;
};
}
}
class Outer extends (class Inner { #a; #e; }) { #a; }
class Outer extends class e {
#e;
#t;
} {
#e;
}
class Foo { publicField = 1; #privateField = 2; getSum() { return this.publicField + this.#privateField; } }
class Foo {
publicField = 1;