Skip to content

Commit 8274783

Browse files
committed
feat(mangler): mangle private class members
1 parent caaa44b commit 8274783

File tree

7 files changed

+227
-14
lines changed

7 files changed

+227
-14
lines changed

crates/oxc/src/compiler.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use oxc_ast::ast::Program;
55
use oxc_codegen::{Codegen, CodegenOptions, CodegenReturn};
66
use oxc_diagnostics::OxcDiagnostic;
77
use oxc_isolated_declarations::{IsolatedDeclarations, IsolatedDeclarationsOptions};
8-
use oxc_mangler::{MangleOptions, Mangler};
8+
use oxc_mangler::{MangleOptions, Mangler, ManglerReturn};
99
use oxc_minifier::{CompressOptions, Compressor};
1010
use oxc_parser::{ParseOptions, Parser, ParserReturn};
1111
use oxc_semantic::{Scoping, SemanticBuilder, SemanticBuilderReturn};
@@ -282,21 +282,28 @@ pub trait CompilerInterface {
282282
Compressor::new(allocator).build(program, options);
283283
}
284284

285-
fn mangle(&self, program: &mut Program<'_>, options: MangleOptions) -> Scoping {
285+
fn mangle(&self, program: &mut Program<'_>, options: MangleOptions) -> ManglerReturn {
286286
Mangler::new().with_options(options).build(program)
287287
}
288288

289289
fn codegen(
290290
&self,
291291
program: &Program<'_>,
292292
source_path: &Path,
293-
scoping: Option<Scoping>,
293+
mangler_return: Option<ManglerReturn>,
294294
options: CodegenOptions,
295295
) -> CodegenReturn {
296296
let mut options = options;
297297
if self.enable_sourcemap() {
298298
options.source_map_path = Some(source_path.to_path_buf());
299299
}
300-
Codegen::new().with_options(options).with_scoping(scoping).build(program)
300+
let (scoping, class_private_mappings) = mangler_return
301+
.map(|m| (Some(m.scoping), Some(m.class_private_mappings)))
302+
.unwrap_or_default();
303+
Codegen::new()
304+
.with_options(options)
305+
.with_scoping(scoping)
306+
.with_private_member_mappings(class_private_mappings)
307+
.build(program)
301308
}
302309
}

crates/oxc_codegen/src/gen.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2264,6 +2264,7 @@ impl Gen for Class<'_> {
22642264
let n = p.code_len();
22652265
let wrap = self.is_expression() && (p.start_of_stmt == n || p.start_of_default_export == n);
22662266
p.wrap(wrap, |p| {
2267+
p.enter_class();
22672268
p.print_decorators(&self.decorators, ctx);
22682269
p.print_space_before_identifier();
22692270
p.add_source_mapping(self.span);
@@ -2295,6 +2296,7 @@ impl Gen for Class<'_> {
22952296
p.print_soft_space();
22962297
self.body.print(p, ctx);
22972298
p.needs_semicolon = false;
2299+
p.exit_class();
22982300
});
22992301
}
23002302
}
@@ -2747,9 +2749,20 @@ impl Gen for AccessorProperty<'_> {
27472749

27482750
impl Gen for PrivateIdentifier<'_> {
27492751
fn r#gen(&self, p: &mut Codegen, _ctx: Context) {
2752+
let name = if let Some(class_index) = p.current_class_index()
2753+
&& let Some(mangled) = &p
2754+
.private_member_mappings
2755+
.as_ref()
2756+
.and_then(|m| m[class_index].get(self.name.as_str()))
2757+
{
2758+
(*mangled).clone()
2759+
} else {
2760+
self.name.into_compact_str()
2761+
};
2762+
27502763
p.print_ascii_byte(b'#');
27512764
p.add_source_mapping_for_name(self.span, &self.name);
2752-
p.print_str(self.name.as_str());
2765+
p.print_str(name.as_str());
27532766
}
27542767
}
27552768

crates/oxc_codegen/src/lib.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ use cow_utils::CowUtils;
1212
use oxc_ast::ast::*;
1313
use oxc_data_structures::{code_buffer::CodeBuffer, stack::Stack};
1414
use oxc_semantic::Scoping;
15-
use oxc_span::{GetSpan, Span};
15+
use oxc_span::{CompactStr, GetSpan, Span};
1616
use oxc_syntax::{
1717
identifier::{is_identifier_part, is_identifier_part_ascii},
1818
operator::{BinaryOperator, UnaryOperator, UpdateOperator},
1919
precedence::Precedence,
2020
};
21+
use rustc_hash::FxHashMap;
2122

2223
mod binary_expr_visitor;
2324
mod comment;
@@ -82,6 +83,9 @@ pub struct Codegen<'a> {
8283

8384
scoping: Option<Scoping>,
8485

86+
/// Private member name mappings for mangling
87+
private_member_mappings: Option<Vec<FxHashMap<String, CompactStr>>>,
88+
8589
/// Output Code
8690
code: CodeBuffer,
8791

@@ -91,6 +95,7 @@ pub struct Codegen<'a> {
9195
need_space_before_dot: usize,
9296
print_next_indent_as_space: bool,
9397
binary_expr_stack: Stack<BinaryExpressionVisitor<'a>>,
98+
class_stack_pos: usize,
9499
/// Indicates the output is JSX type, it is set in [`Program::gen`] and the result
95100
/// is obtained by [`oxc_span::SourceType::is_jsx`]
96101
is_jsx: bool,
@@ -146,11 +151,13 @@ impl<'a> Codegen<'a> {
146151
options,
147152
source_text: None,
148153
scoping: None,
154+
private_member_mappings: None,
149155
code: CodeBuffer::default(),
150156
needs_semicolon: false,
151157
need_space_before_dot: 0,
152158
print_next_indent_as_space: false,
153159
binary_expr_stack: Stack::with_capacity(12),
160+
class_stack_pos: 0,
154161
prev_op_end: 0,
155162
prev_reg_exp_end: 0,
156163
prev_op: None,
@@ -190,6 +197,19 @@ impl<'a> Codegen<'a> {
190197
self
191198
}
192199

200+
/// Set private member name mappings for mangling.
201+
///
202+
/// This allows renaming of private class members like `#field` -> `#a`.
203+
/// The Vec contains per-class mappings, indexed by class declaration order.
204+
#[must_use]
205+
pub fn with_private_member_mappings(
206+
mut self,
207+
mappings: Option<Vec<FxHashMap<String, CompactStr>>>,
208+
) -> Self {
209+
self.private_member_mappings = mappings;
210+
self
211+
}
212+
193213
/// Print a [`Program`] into a string of source code.
194214
///
195215
/// A source map will be generated if [`CodegenOptions::source_map_path`] is set.
@@ -445,6 +465,21 @@ impl<'a> Codegen<'a> {
445465
}
446466
}
447467

468+
#[inline]
469+
fn enter_class(&mut self) {
470+
self.class_stack_pos += 1;
471+
}
472+
473+
#[inline]
474+
fn exit_class(&mut self) {
475+
self.class_stack_pos -= 1;
476+
}
477+
478+
#[inline]
479+
fn current_class_index(&self) -> Option<usize> {
480+
if self.class_stack_pos > 0 { Some(self.class_stack_pos - 1) } else { None }
481+
}
482+
448483
#[inline]
449484
fn wrap<F: FnMut(&mut Self)>(&mut self, wrap: bool, mut f: F) {
450485
if wrap {

crates/oxc_mangler/src/lib.rs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ use std::iter::{self, repeat_with};
22

33
use itertools::Itertools;
44
use keep_names::collect_name_symbols;
5-
use rustc_hash::FxHashSet;
5+
use rustc_hash::{FxHashMap, FxHashSet};
66

77
use base54::base54;
88
use oxc_allocator::{Allocator, BitSet, Vec};
99
use oxc_ast::ast::{Declaration, Program, Statement};
1010
use oxc_data_structures::inline_string::InlineString;
1111
use oxc_index::Idx;
1212
use oxc_semantic::{AstNodes, Scoping, Semantic, SemanticBuilder, SymbolId};
13-
use oxc_span::Atom;
13+
use oxc_span::{Atom, CompactStr};
1414

1515
pub(crate) mod base54;
1616
mod keep_names;
@@ -56,6 +56,13 @@ impl TempAllocator<'_> {
5656
}
5757
}
5858

59+
pub struct ManglerReturn {
60+
pub scoping: Scoping,
61+
/// A vector where each element corresponds to a class in declaration order.
62+
/// Each element is a mapping from original private member names to their mangled names.
63+
pub class_private_mappings: std::vec::Vec<FxHashMap<String, CompactStr>>,
64+
}
65+
5966
/// # Name Mangler / Symbol Minification
6067
///
6168
/// ## Example
@@ -257,11 +264,12 @@ impl<'t> Mangler<'t> {
257264
/// Mangles the program. The resulting SymbolTable contains the mangled symbols - `program` is not modified.
258265
/// Pass the symbol table to oxc_codegen to generate the mangled code.
259266
#[must_use]
260-
pub fn build(self, program: &Program<'_>) -> Scoping {
267+
pub fn build(self, program: &Program<'_>) -> ManglerReturn {
261268
let mut semantic =
262269
SemanticBuilder::new().with_scope_tree_child_ids(true).build(program).semantic;
270+
let class_private_mappings = Self::collect_private_members_from_semantic(&semantic);
263271
self.build_with_semantic(&mut semantic, program);
264-
semantic.into_scoping()
272+
ManglerReturn { scoping: semantic.into_scoping(), class_private_mappings }
265273
}
266274

267275
/// # Panics
@@ -548,6 +556,36 @@ impl<'t> Mangler<'t> {
548556
let ids = collect_name_symbols(keep_names, scoping, nodes);
549557
(ids.iter().map(|id| scoping.symbol_name(*id)).collect(), ids)
550558
}
559+
560+
/// Collects and generates mangled names for private members using semantic information
561+
/// Returns a Vec where each element corresponds to a class in declaration order
562+
fn collect_private_members_from_semantic(
563+
semantic: &Semantic<'_>,
564+
) -> std::vec::Vec<FxHashMap<String, CompactStr>> {
565+
let classes = semantic.classes();
566+
classes
567+
.elements
568+
.iter()
569+
.map(|class_elements| {
570+
assert!(u32::try_from(class_elements.len()).is_ok(), "too many class elements");
571+
class_elements
572+
.iter()
573+
.filter_map(|element| {
574+
if element.is_private { Some(element.name.to_string()) } else { None }
575+
})
576+
.enumerate()
577+
.map(|(i, name)| {
578+
#[expect(
579+
clippy::cast_possible_truncation,
580+
reason = "checked above with assert"
581+
)]
582+
let mangled = CompactStr::new(base54(i as u32).as_str());
583+
(name, mangled)
584+
})
585+
.collect::<FxHashMap<_, _>>()
586+
})
587+
.collect()
588+
}
551589
}
552590

553591
fn is_special_name(name: &str) -> bool {

crates/oxc_minifier/examples/mangler.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ fn main() -> std::io::Result<()> {
6262
fn mangler(source_text: &str, source_type: SourceType, options: MangleOptions) -> String {
6363
let allocator = Allocator::default();
6464
let ret = Parser::new(&allocator, source_text, source_type).parse();
65-
let symbol_table = Mangler::new().with_options(options).build(&ret.program);
66-
Codegen::new().with_scoping(Some(symbol_table)).build(&ret.program).code
65+
let mangler_return = Mangler::new().with_options(options).build(&ret.program);
66+
Codegen::new()
67+
.with_scoping(Some(mangler_return.scoping))
68+
.with_private_member_mappings(Some(mangler_return.class_private_mappings))
69+
.build(&ret.program)
70+
.code
6771
}

crates/oxc_minifier/tests/mangler/mod.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ fn mangle(source_text: &str, options: MangleOptions) -> String {
1111
let source_type = SourceType::mjs();
1212
let ret = Parser::new(&allocator, source_text, source_type).parse();
1313
let program = ret.program;
14-
let symbol_table = Mangler::new().with_options(options).build(&program);
15-
Codegen::new().with_scoping(Some(symbol_table)).build(&program).code
14+
let mangler_return = Mangler::new().with_options(options).build(&program);
15+
Codegen::new()
16+
.with_scoping(Some(mangler_return.scoping))
17+
.with_private_member_mappings(Some(mangler_return.class_private_mappings))
18+
.build(&program)
19+
.code
1620
}
1721

1822
#[test]
@@ -100,3 +104,31 @@ fn mangler() {
100104
insta::assert_snapshot!("mangler", snapshot);
101105
});
102106
}
107+
108+
#[test]
109+
fn private_member_mangling() {
110+
let cases = [
111+
"class Foo { #privateField = 1; method() { return this.#privateField; } }",
112+
"class Foo { #a = 1; #b = 2; method() { return this.#a + this.#b; } }",
113+
"class Foo { #method() { return 1; } publicMethod() { return this.#method(); } }",
114+
"class Foo { #field; #method() { return this.#field; } get() { return this.#method(); } }",
115+
"class Foo { #x; check() { return #x in this; } }",
116+
// Nested classes
117+
"class Outer { #outerField = 1; inner() { return class Inner { #innerField = 2; get() { return this.#innerField; } }; } }",
118+
// Mixed public and private
119+
"class Foo { publicField = 1; #privateField = 2; getSum() { return this.publicField + this.#privateField; } }",
120+
// Test same names across different classes should reuse mangled names
121+
"class A { #field = 1; #method() { return this.#field; } } class B { #field = 2; #method() { return this.#field; } }",
122+
];
123+
124+
let mut snapshot = String::new();
125+
cases.into_iter().fold(&mut snapshot, |w, case| {
126+
let options = MangleOptions::default();
127+
write!(w, "{case}\n{}\n", mangle(case, options)).unwrap();
128+
w
129+
});
130+
131+
insta::with_settings!({ prepend_module_to_snapshot => false, omit_expression => true }, {
132+
insta::assert_snapshot!("private_member_mangling", snapshot);
133+
});
134+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
---
2+
source: crates/oxc_minifier/tests/mangler/mod.rs
3+
---
4+
class Foo { #privateField = 1; method() { return this.#privateField; } }
5+
class Foo {
6+
#e = 1;
7+
method() {
8+
return this.#e;
9+
}
10+
}
11+
12+
class Foo { #a = 1; #b = 2; method() { return this.#a + this.#b; } }
13+
class Foo {
14+
#e = 1;
15+
#t = 2;
16+
method() {
17+
return this.#e + this.#t;
18+
}
19+
}
20+
21+
class Foo { #method() { return 1; } publicMethod() { return this.#method(); } }
22+
class Foo {
23+
#e() {
24+
return 1;
25+
}
26+
publicMethod() {
27+
return this.#e();
28+
}
29+
}
30+
31+
class Foo { #field; #method() { return this.#field; } get() { return this.#method(); } }
32+
class Foo {
33+
#e;
34+
#t() {
35+
return this.#e;
36+
}
37+
get() {
38+
return this.#t();
39+
}
40+
}
41+
42+
class Foo { #x; check() { return #x in this; } }
43+
class Foo {
44+
#e;
45+
check() {
46+
return #e in this;
47+
}
48+
}
49+
50+
class Outer { #outerField = 1; inner() { return class Inner { #innerField = 2; get() { return this.#innerField; } }; } }
51+
class Outer {
52+
#e = 1;
53+
inner() {
54+
return class e {
55+
#e = 2;
56+
get() {
57+
return this.#e;
58+
}
59+
};
60+
}
61+
}
62+
63+
class Foo { publicField = 1; #privateField = 2; getSum() { return this.publicField + this.#privateField; } }
64+
class Foo {
65+
publicField = 1;
66+
#e = 2;
67+
getSum() {
68+
return this.publicField + this.#e;
69+
}
70+
}
71+
72+
class A { #field = 1; #method() { return this.#field; } } class B { #field = 2; #method() { return this.#field; } }
73+
class A {
74+
#e = 1;
75+
#t() {
76+
return this.#e;
77+
}
78+
}
79+
class B {
80+
#e = 2;
81+
#t() {
82+
return this.#e;
83+
}
84+
}

0 commit comments

Comments
 (0)