mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-15 06:39:17 +00:00
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:
1 parent
c002f29226
commit
c1369a71ff
3 files changed
+26
-1
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user