perf(minifier): use Ident for property mangler (#26334)

Use `Ident` in the property mangler. This reduces the `Set::contains` operation faster as the hashing doesn't happen.

The property names are not always an "ident" so it may not make sense to use the `Ident` type. But currently the `Ident` type allows characters that are not allowed in identifiers so the code works.

I used AI to generate the code and then reviewed the code and iterated a bit.
This commit is contained in:
sapphi-red committed 2026-09-07 12:58:02 +00:00
1 parent 116c8b8303
commit 34a242e134
4 files changed
+204 -157

No files matched your search

+2 -2
View File
@@ -149,10 +149,10 @@ impl<'a> Minifier {
// Run property mangling first because compression can change which strings are property
// keys.
let property_mangle_cache = mangle_properties.map(|options| {
let mut mangler = PropertyMangler::new(options);
let mut mangler = PropertyMangler::new_in(options, allocator);
mangler.collect(program);
mangler.assign();
mangler.rewrite(program, allocator);
mangler.rewrite(program);
mangler.into_cache()
});
+186 -139
View File
@@ -14,7 +14,7 @@ use oxc_ast_visit::{Visit, VisitMut, walk, walk_mut};
use oxc_ecmascript::StringToNumber;
use oxc_mangler::base54;
use oxc_span::Span;
use oxc_str::{CompactStr, Ident, Str};
use oxc_str::{ArenaIdentHashMap, ArenaIdentHashSet, CompactStr, Ident, Str, format_ident};
use oxc_syntax::{identifier::is_identifier_name, number::ToJsString};
use rustc_hash::{FxHashMap, FxHashSet};
@@ -95,11 +95,12 @@ impl std::fmt::Display for InvalidManglePropertyCacheTarget {
impl std::error::Error for InvalidManglePropertyCacheTarget {}
/// A single-application mapping from original property names to their final names.
/// An allocator-backed, single-application mapping from original property names to their final
/// names.
///
/// Applying a mapping more than once is not generally idempotent: a cache target may also be
/// another cache key or source candidate. Each program must therefore be rewritten exactly once.
pub type PropertyMapping = FxHashMap<CompactStr, CompactStr>;
pub type PropertyMapping<'a> = ArenaIdentHashMap<'a, Ident<'a>>;
/// Options for opt-in property-name mangling.
#[derive(Debug, Clone)]
@@ -171,12 +172,21 @@ fn key_annotated_spans(program: &Program<'_>) -> FxHashSet<u32> {
}
/// Data gathered by the read-only collection phase.
#[derive(Debug, Default)]
struct PropertyCollectState {
#[derive(Debug)]
struct PropertyCollectState<'a> {
/// Number of candidate occurrences before eligibility filtering.
frequencies: FxHashMap<CompactStr, u32>,
frequencies: ArenaIdentHashMap<'a, u32>,
/// Spellings that remain in the output and therefore cannot be automatic output names.
occupied: FxHashSet<CompactStr>,
occupied: ArenaIdentHashSet<'a>,
}
impl<'a> PropertyCollectState<'a> {
fn new_in(allocator: &'a Allocator) -> Self {
Self {
frequencies: ArenaIdentHashMap::new_in(allocator),
occupied: ArenaIdentHashSet::new_in(allocator),
}
}
}
/// Property candidates and occupied names collected from one program.
@@ -185,20 +195,28 @@ struct PropertyCollectState {
/// the results into one [`PropertyMangler`] without cloning caches or reserved-name sets per
/// program.
#[derive(Debug)]
pub struct PropertyMangleCollection(PropertyCollectState);
pub struct PropertyMangleCollection<'a>(&'a PropertyCollectState<'a>);
impl PropertyMangleCollection {
impl<'a> PropertyMangleCollection<'a> {
/// Collect property candidates from one program without mutating its AST.
pub fn from_program(options: &ManglePropertiesOptions, program: &Program<'_>) -> Self {
///
/// The allocator must remain alive until the collection and any mangler it is merged into are
/// dropped.
pub fn from_program(
options: &ManglePropertiesOptions,
program: &Program<'a>,
allocator: &'a Allocator,
) -> Self {
let key_annotated = key_annotated_spans(program);
Self(collect(options, program, &key_annotated))
Self(allocator.alloc(collect(options, program, &key_annotated, allocator)))
}
}
struct PropertyCollector<'o> {
struct PropertyCollector<'o, 'a> {
options: &'o ManglePropertiesOptions,
key_annotated: &'o FxHashSet<u32>,
state: PropertyCollectState,
allocator: &'a Allocator,
state: PropertyCollectState<'a>,
}
fn is_eligible(options: &ManglePropertiesOptions, name: &str) -> bool {
@@ -216,22 +234,25 @@ fn is_eligible(options: &ManglePropertiesOptions, name: &str) -> bool {
true
}
impl<'o> PropertyCollector<'o> {
fn new(options: &'o ManglePropertiesOptions, key_annotated: &'o FxHashSet<u32>) -> Self {
Self { options, key_annotated, state: PropertyCollectState::default() }
impl<'o, 'a> PropertyCollector<'o, 'a> {
fn new(
options: &'o ManglePropertiesOptions,
key_annotated: &'o FxHashSet<u32>,
allocator: &'a Allocator,
) -> Self {
Self { options, key_annotated, allocator, state: PropertyCollectState::new_in(allocator) }
}
fn candidate(&mut self, name: &str) {
// Avoid constructing an owned key for repeated occurrences.
if let Some(count) = self.state.frequencies.get_mut(name) {
fn candidate(&mut self, name: Ident<'a>) {
if let Some(count) = self.state.frequencies.get_mut(&name) {
*count = count.saturating_add(1);
return;
}
let count = self.state.frequencies.entry(CompactStr::from(name)).or_default();
let count = self.state.frequencies.entry(name).or_default();
*count = count.saturating_add(1);
}
fn quoted(&mut self, name: &str) {
fn quoted(&mut self, name: Ident<'a>) {
if self.options.mangle_quoted {
self.candidate(name);
} else {
@@ -239,15 +260,15 @@ impl<'o> PropertyCollector<'o> {
}
}
fn occupy(&mut self, name: &str) {
self.state.occupied.insert(CompactStr::from(name));
fn occupy(&mut self, name: Ident<'a>) {
self.state.occupied.insert(name);
}
fn special_literal(&self, span: Span) -> bool {
self.key_annotated.contains(&span.start)
}
fn observe_literal(&mut self, span: Span, name: &str) {
fn observe_literal(&mut self, span: Span, name: Ident<'a>) {
if self.special_literal(span) {
self.candidate(name);
} else {
@@ -258,17 +279,17 @@ impl<'o> PropertyCollector<'o> {
}
}
fn classify_key_expression(&mut self, expression: &Expression<'_>) {
fn classify_key_expression(&mut self, expression: &Expression<'a>) {
match expression.get_inner_expression() {
Expression::StringLiteral(literal) if self.special_literal(literal.span) => {}
Expression::StringLiteral(literal) => self.quoted(literal.value.as_str()),
Expression::StringLiteral(literal) => self.quoted(Ident::from(literal.value)),
Expression::TemplateLiteral(template)
if template.expressions.is_empty() && self.special_literal(template.span) => {}
Expression::TemplateLiteral(template) if template.expressions.is_empty() => {
if let [quasi] = template.quasis.as_slice()
&& let Some(cooked) = quasi.value.cooked
{
self.quoted(cooked.as_str());
self.quoted(Ident::from(cooked));
}
}
Expression::ConditionalExpression(expression) => {
@@ -280,17 +301,21 @@ impl<'o> PropertyCollector<'o> {
self.classify_key_expression(last);
}
}
Expression::NumericLiteral(literal) => self.occupy(&numeric_key_string(literal.value)),
Expression::NumericLiteral(literal) => {
let name = numeric_key_string(literal.value);
self.occupy(Ident::from_str_in(&name, &self.allocator));
}
_ => {}
}
}
fn classify_property_key(&mut self, key: &PropertyKey<'_>) {
fn classify_property_key(&mut self, key: &PropertyKey<'a>) {
match key {
PropertyKey::StaticIdentifier(identifier) => self.candidate(identifier.name.as_str()),
PropertyKey::StaticIdentifier(identifier) => self.candidate(identifier.name),
PropertyKey::PrivateIdentifier(_) => {}
PropertyKey::NumericLiteral(literal) => {
self.occupy(&numeric_key_string(literal.value));
let name = numeric_key_string(literal.value);
self.occupy(Ident::from_str_in(&name, &self.allocator));
}
key => {
if let Some(expression) = key.as_expression() {
@@ -301,10 +326,10 @@ impl<'o> PropertyCollector<'o> {
}
}
impl<'a> Visit<'a> for PropertyCollector<'_> {
impl<'a> Visit<'a> for PropertyCollector<'_, 'a> {
fn visit_directive(&mut self, directive: &Directive<'a>) {
// Directives are not property-name positions.
self.occupy(directive.expression.value.as_str());
self.occupy(Ident::from(directive.expression.value));
}
fn visit_ts_type(&mut self, _ty: &TSType<'a>) {}
@@ -314,7 +339,7 @@ impl<'a> Visit<'a> for PropertyCollector<'_> {
fn visit_ts_method_signature(&mut self, _signature: &TSMethodSignature<'a>) {}
fn visit_static_member_expression(&mut self, expression: &StaticMemberExpression<'a>) {
self.candidate(expression.property.name.as_str());
self.candidate(expression.property.name);
walk::walk_static_member_expression(self, expression);
}
@@ -332,23 +357,25 @@ impl<'a> Visit<'a> for PropertyCollector<'_> {
&mut self,
property: &AssignmentTargetPropertyIdentifier<'a>,
) {
self.candidate(property.binding.name.as_str());
self.candidate(property.binding.name);
walk::walk_assignment_target_property_identifier(self, property);
}
fn visit_jsx_attribute_name(&mut self, name: &JSXAttributeName<'a>) {
match name {
JSXAttributeName::Identifier(identifier) => self.candidate(identifier.name.as_str()),
JSXAttributeName::Identifier(identifier) => {
self.candidate(Ident::from(identifier.name));
}
JSXAttributeName::NamespacedName(name) => {
self.occupy(name.namespace.name.as_str());
self.occupy(name.name.name.as_str());
self.occupy(Ident::from(name.namespace.name));
self.occupy(Ident::from(name.name.name));
}
}
walk::walk_jsx_attribute_name(self, name);
}
fn visit_jsx_member_expression(&mut self, expression: &JSXMemberExpression<'a>) {
self.candidate(expression.property.name.as_str());
self.candidate(Ident::from(expression.property.name));
walk::walk_jsx_member_expression(self, expression);
}
@@ -360,7 +387,7 @@ impl<'a> Visit<'a> for PropertyCollector<'_> {
}
fn visit_string_literal(&mut self, literal: &StringLiteral<'a>) {
self.observe_literal(literal.span, literal.value.as_str());
self.observe_literal(literal.span, Ident::from(literal.value));
}
fn visit_template_literal(&mut self, template: &TemplateLiteral<'a>) {
@@ -368,38 +395,40 @@ impl<'a> Visit<'a> for PropertyCollector<'_> {
&& let [quasi] = template.quasis.as_slice()
&& let Some(cooked) = quasi.value.cooked
{
self.observe_literal(template.span, cooked.as_str());
self.observe_literal(template.span, Ident::from(cooked));
}
walk::walk_template_literal(self, template);
}
}
fn collect(
fn collect<'a>(
options: &ManglePropertiesOptions,
program: &Program<'_>,
program: &Program<'a>,
key_annotated: &FxHashSet<u32>,
) -> PropertyCollectState {
let mut collector = PropertyCollector::new(options, key_annotated);
allocator: &'a Allocator,
) -> PropertyCollectState<'a> {
let mut collector = PropertyCollector::new(options, key_annotated, allocator);
collector.visit_program(program);
collector.state
}
fn debug_name(original: &str, attempt: u32) -> CompactStr {
fn debug_name<'a>(original: &str, attempt: u32, allocator: &'a Allocator) -> Ident<'a> {
if is_identifier_name(original) {
if attempt == 0 {
CompactStr::from(format!("_${original}$_"))
format_ident!(allocator, "_${original}$_")
} else {
CompactStr::from(format!("_${original}${attempt}$_"))
format_ident!(allocator, "_${original}${attempt}$_")
}
} else {
CompactStr::from(format!("_$property{attempt}$_"))
format_ident!(allocator, "_$property{attempt}$_")
}
}
fn assign(
fn assign<'a>(
options: &ManglePropertiesOptions,
state: &PropertyCollectState,
) -> (PropertyMapping, ManglePropertyCache) {
state: &PropertyCollectState<'a>,
allocator: &'a Allocator,
) -> (PropertyMapping<'a>, ManglePropertyCache) {
// Eligibility depends only on the name, so evaluate it once after collection.
let mut names: Vec<_> =
state.frequencies.iter().filter(|(name, _)| is_eligible(options, name.as_str())).collect();
@@ -408,41 +437,51 @@ fn assign(
});
// Generated names must avoid collected names, reservations, cache keys, and pinned targets.
let mut occupied = state.occupied.clone();
occupied.extend(state.frequencies.keys().cloned());
occupied.extend(options.reserved.iter().cloned());
occupied.extend(options.cache.0.keys().cloned());
occupied.extend(options.cache.0.values().flatten().cloned());
let mut occupied = ArenaIdentHashSet::from_iter_in(state.occupied.iter().copied(), allocator);
occupied.extend(state.frequencies.keys().copied());
occupied
.extend(options.reserved.iter().map(|name| Ident::from_str_in(name.as_str(), &allocator)));
occupied
.extend(options.cache.0.keys().map(|name| Ident::from_str_in(name.as_str(), &allocator)));
occupied.extend(
options
.cache
.0
.values()
.flatten()
.map(|name| Ident::from_str_in(name.as_str(), &allocator)),
);
let mut mapping = PropertyMapping::default();
let mut mapping = PropertyMapping::new_in(allocator);
let mut cache = options.cache.clone();
let mut counter = 0u32;
for (original, _) in names {
if let Some(Some(target)) = options.cache.get(original.as_str()) {
mapping.insert(original.clone(), target.clone());
mapping.insert(*original, Ident::from_str_in(target.as_str(), &allocator));
continue;
}
let mut debug_attempt = 0u32;
let target = loop {
let candidate = if options.debug {
let candidate = debug_name(original.as_str(), debug_attempt);
let candidate = debug_name(original.as_str(), debug_attempt, allocator);
debug_attempt =
debug_attempt.checked_add(1).expect("debug property name space exhausted");
candidate
} else {
let candidate = CompactStr::from(base54(counter).as_str());
let name = base54(counter);
let candidate = Ident::from_str_in(name.as_str(), &allocator);
counter = counter.checked_add(1).expect("property name space exhausted");
candidate
};
if !occupied.contains(candidate.as_str()) && !is_hard_reserved(candidate.as_str()) {
if !occupied.contains(&candidate) && !is_hard_reserved(candidate.as_str()) {
break candidate;
}
};
occupied.insert(target.clone());
cache.insert_generated(original.clone(), Some(target.clone()));
mapping.insert(original.clone(), target);
occupied.insert(target);
cache.insert_generated(original.to_compact_str(), Some(target.to_compact_str()));
mapping.insert(*original, target);
}
(mapping, cache)
@@ -450,14 +489,14 @@ fn assign(
// Property keys can share syntax with local bindings. Expand shorthand when needed so only the
// property key changes.
struct PropertyRewriter<'a, 'm> {
mapping: &'m PropertyMapping,
struct PropertyRewriter<'a, 'm, 'p> {
mapping: &'m PropertyMapping<'p>,
mangle_quoted: bool,
key_annotated: &'m FxHashSet<u32>,
ast: AstBuilder<'a>,
}
impl<'a> PropertyRewriter<'a, '_> {
impl<'a, 'p> PropertyRewriter<'a, '_, 'p> {
fn special_literal(&self, span: Span) -> bool {
self.key_annotated.contains(&span.start)
}
@@ -466,15 +505,15 @@ impl<'a> PropertyRewriter<'a, '_> {
self.key_annotated.contains(&span.start) || self.mangle_quoted
}
fn target(&self, original: &str) -> Option<&CompactStr> {
self.mapping.get(original)
fn target(&self, original: Ident<'_>) -> Option<Ident<'p>> {
self.mapping.get(&original).copied()
}
fn rename_string_literal(&self, literal: &mut StringLiteral<'a>) {
if !self.should_rewrite_literal(literal.span) {
return;
}
if let Some(target) = self.target(literal.value.as_str()) {
if let Some(target) = self.target(Ident::from(literal.value)) {
literal.value = Str::from_str_in(target.as_str(), &self.ast);
literal.raw = None;
}
@@ -486,7 +525,7 @@ impl<'a> PropertyRewriter<'a, '_> {
}
if let [quasi] = template.quasis.as_mut_slice()
&& let Some(cooked) = quasi.value.cooked
&& let Some(target) = self.target(cooked.as_str())
&& let Some(target) = self.target(Ident::from(cooked))
{
let target = Str::from_str_in(target.as_str(), &self.ast);
quasi.value.cooked = Some(target);
@@ -516,9 +555,9 @@ impl<'a> PropertyRewriter<'a, '_> {
}
}
fn direct_string_key(key: &PropertyKey<'a>) -> Option<(CompactStr, Span)> {
fn direct_string_key(key: &PropertyKey<'a>) -> Option<(Str<'a>, Span)> {
if let PropertyKey::StringLiteral(literal) = key {
Some((CompactStr::from(literal.value.as_str()), literal.span))
Some((literal.value, literal.span))
} else {
None
}
@@ -528,11 +567,11 @@ impl<'a> PropertyRewriter<'a, '_> {
&self,
key: &mut PropertyKey<'a>,
computed: &mut bool,
original_string: Option<(CompactStr, Span)>,
original_string: Option<(Str<'a>, Span)>,
) {
if let Some((original, span)) = original_string
&& self.should_rewrite_literal(span)
&& let Some(target) = self.target(original.as_str())
&& let Some(target) = self.target(Ident::from(original))
{
*key = PropertyKey::StaticIdentifier(IdentifierName::boxed(
span,
@@ -549,14 +588,14 @@ impl<'a> PropertyRewriter<'a, '_> {
fn rename_static_key(&self, key: &mut PropertyKey<'a>) {
if let PropertyKey::StaticIdentifier(identifier) = key
&& let Some(target) = self.target(identifier.name.as_str())
&& let Some(target) = self.target(identifier.name)
{
identifier.name = Ident::from_str_in(target.as_str(), &self.ast);
}
}
}
impl<'a> VisitMut<'a> for PropertyRewriter<'a, '_> {
impl<'a> VisitMut<'a> for PropertyRewriter<'a, '_, '_> {
// Codegen emits the directive's separate raw value, so leave directives unchanged.
fn visit_directive(&mut self, _directive: &mut Directive<'a>) {}
@@ -567,9 +606,9 @@ impl<'a> VisitMut<'a> for PropertyRewriter<'a, '_> {
fn visit_ts_method_signature(&mut self, _signature: &mut TSMethodSignature<'a>) {}
fn visit_static_member_expression(&mut self, expression: &mut StaticMemberExpression<'a>) {
let original = CompactStr::from(expression.property.name.as_str());
let original = expression.property.name;
walk_mut::walk_static_member_expression(self, expression);
if let Some(target) = self.target(original.as_str()) {
if let Some(target) = self.target(original) {
expression.property.name = Ident::from_str_in(target.as_str(), &self.ast);
}
}
@@ -578,10 +617,10 @@ impl<'a> VisitMut<'a> for PropertyRewriter<'a, '_> {
if let Expression::ComputedMemberExpression(member) = expression
&& let Expression::StringLiteral(literal) = &member.expression
{
let original = CompactStr::from(literal.value.as_str());
let original = literal.value;
let property_span = literal.span;
if self.should_rewrite_literal(property_span)
&& let Some(target) = self.target(original.as_str())
&& let Some(target) = self.target(Ident::from(original))
{
let property = IdentifierName::new(
property_span,
@@ -618,7 +657,7 @@ impl<'a> VisitMut<'a> for PropertyRewriter<'a, '_> {
fn visit_object_property(&mut self, property: &mut ObjectProperty<'a>) {
let original_string = Self::direct_string_key(&property.key);
let expands_shorthand = property.shorthand
&& matches!(&property.key, PropertyKey::StaticIdentifier(identifier) if self.target(identifier.name.as_str()).is_some());
&& matches!(&property.key, PropertyKey::StaticIdentifier(identifier) if self.target(identifier.name).is_some());
walk_mut::walk_object_property(self, property);
if expands_shorthand {
property.shorthand = false;
@@ -629,7 +668,7 @@ impl<'a> VisitMut<'a> for PropertyRewriter<'a, '_> {
fn visit_binding_property(&mut self, property: &mut BindingProperty<'a>) {
let original_string = Self::direct_string_key(&property.key);
let expands_shorthand = property.shorthand
&& matches!(&property.key, PropertyKey::StaticIdentifier(identifier) if self.target(identifier.name.as_str()).is_some());
&& matches!(&property.key, PropertyKey::StaticIdentifier(identifier) if self.target(identifier.name).is_some());
walk_mut::walk_binding_property(self, property);
if expands_shorthand {
property.shorthand = false;
@@ -639,8 +678,8 @@ impl<'a> VisitMut<'a> for PropertyRewriter<'a, '_> {
fn visit_assignment_target_property(&mut self, property: &mut AssignmentTargetProperty<'a>) {
if let AssignmentTargetProperty::AssignmentTargetPropertyIdentifier(identifier) = property {
let original = CompactStr::from(identifier.binding.name.as_str());
let target = self.target(original.as_str()).cloned();
let original = identifier.binding.name;
let target = self.target(original);
walk_mut::walk_assignment_target_property(self, property);
if let Some(target) = target
&& let AssignmentTargetProperty::AssignmentTargetPropertyIdentifier(identifier) =
@@ -733,10 +772,10 @@ impl<'a> VisitMut<'a> for PropertyRewriter<'a, '_> {
fn visit_jsx_attribute_name(&mut self, name: &mut JSXAttributeName<'a>) {
if let JSXAttributeName::Identifier(identifier) = name {
let original = CompactStr::from(identifier.name.as_str());
let original = identifier.name;
walk_mut::walk_jsx_attribute_name(self, name);
if let JSXAttributeName::Identifier(identifier) = name
&& let Some(target) = self.target(original.as_str())
&& let Some(target) = self.target(Ident::from(original))
{
identifier.name = Str::from_str_in(target.as_str(), &self.ast);
}
@@ -746,9 +785,9 @@ impl<'a> VisitMut<'a> for PropertyRewriter<'a, '_> {
}
fn visit_jsx_member_expression(&mut self, expression: &mut JSXMemberExpression<'a>) {
let original = CompactStr::from(expression.property.name.as_str());
let original = expression.property.name;
walk_mut::walk_jsx_member_expression(self, expression);
if let Some(target) = self.target(original.as_str()) {
if let Some(target) = self.target(Ident::from(original)) {
expression.property.name = Str::from_str_in(target.as_str(), &self.ast);
}
}
@@ -768,44 +807,47 @@ impl<'a> VisitMut<'a> for PropertyRewriter<'a, '_> {
}
/// Three-phase property-name mangler.
pub struct PropertyMangler {
pub struct PropertyMangler<'a> {
allocator: &'a Allocator,
options: ManglePropertiesOptions,
state: PropertyCollectState,
mapping: PropertyMapping,
state: PropertyCollectState<'a>,
mapping: PropertyMapping<'a>,
cache: ManglePropertyCache,
}
impl PropertyMangler {
pub fn new(options: ManglePropertiesOptions) -> Self {
impl<'a> PropertyMangler<'a> {
pub fn new_in(options: ManglePropertiesOptions, allocator: &'a Allocator) -> Self {
Self {
allocator,
cache: ManglePropertyCache::default(),
options,
state: PropertyCollectState::default(),
mapping: PropertyMapping::default(),
state: PropertyCollectState::new_in(allocator),
mapping: PropertyMapping::new_in(allocator),
}
}
/// Collect property occurrences and occupied spellings without mutating the AST.
pub fn collect(&mut self, program: &Program<'_>) {
let collection = PropertyMangleCollection::from_program(&self.options, program);
self.merge_collected(collection);
pub fn collect(&mut self, program: &Program<'a>) {
let collection =
PropertyMangleCollection::from_program(&self.options, program, self.allocator);
self.merge_collected(&collection);
}
/// Merge candidates collected independently with equivalent options.
///
/// This allows callers to collect programs in parallel, merge the results, and assign one
/// mapping.
pub fn merge_collected(&mut self, collection: PropertyMangleCollection) {
for (name, frequency) in collection.0.frequencies {
pub fn merge_collected(&mut self, collection: &PropertyMangleCollection<'a>) {
for (&name, &frequency) in &collection.0.frequencies {
let total = self.state.frequencies.entry(name).or_default();
*total = total.saturating_add(frequency);
}
self.state.occupied.extend(collection.0.occupied);
self.state.occupied.extend(collection.0.occupied.iter().copied());
}
/// Assign deterministic names by descending occurrence frequency and lexical tie-break.
pub fn assign(&mut self) -> &PropertyMapping {
let (mapping, cache) = assign(&self.options, &self.state);
pub fn assign(&mut self) -> &PropertyMapping<'a> {
let (mapping, cache) = assign(&self.options, &self.state, self.allocator);
self.mapping = mapping;
self.cache = cache;
&self.mapping
@@ -815,7 +857,7 @@ impl PropertyMangler {
///
/// Do not apply this mapping to already-rewritten code. Cache targets are allowed to equal
/// other cache keys, so a second application can incorrectly follow a mapping chain.
pub fn rewrite<'a>(&self, program: &mut Program<'a>, allocator: &'a Allocator) {
pub fn rewrite(&self, program: &mut Program<'a>) {
if self.mapping.is_empty() {
return;
}
@@ -824,12 +866,12 @@ impl PropertyMangler {
mapping: &self.mapping,
mangle_quoted: self.options.mangle_quoted,
key_annotated: &key_annotated,
ast: AstBuilder::new(allocator),
ast: AstBuilder::new(self.allocator),
};
rewriter.visit_program(program);
}
pub fn mapping(&self) -> &PropertyMapping {
pub fn mapping(&self) -> &PropertyMapping<'a> {
&self.mapping
}
@@ -845,23 +887,29 @@ impl PropertyMangler {
#[cfg(test)]
mod tests {
use super::*;
use oxc_str::static_ident;
fn options(pattern: &str) -> ManglePropertiesOptions {
ManglePropertiesOptions::new(lazy_regex::Regex::new(pattern).unwrap())
}
#[test]
fn collection_is_send() {
fn assert_send<T: Send>() {}
assert_send::<PropertyMangleCollection<'static>>();
}
#[test]
fn assignment_prefers_frequency_then_name() {
let allocator = Allocator::default();
let options = options("^_");
let state = PropertyCollectState {
frequencies: FxHashMap::from_iter([
(CompactStr::from("_rare"), 1),
(CompactStr::from("_often"), 3),
(CompactStr::from("_also_often"), 3),
]),
occupied: FxHashSet::default(),
};
let (mapping, _) = assign(&options, &state);
let mut state = PropertyCollectState::new_in(&allocator);
state.frequencies.extend([
(static_ident!("_rare"), 1),
(static_ident!("_often"), 3),
(static_ident!("_also_often"), 3),
]);
let (mapping, _) = assign(&options, &state, &allocator);
assert_eq!(mapping["_also_often"].as_str(), "e");
assert_eq!(mapping["_often"].as_str(), "t");
assert_eq!(mapping["_rare"].as_str(), "n");
@@ -869,19 +917,19 @@ mod tests {
#[test]
fn assignment_honors_cache_and_allows_duplicate_targets() {
let allocator = Allocator::default();
let mut options = options("^_");
options.cache.insert("_a".into(), Some("A".into())).unwrap();
options.cache.insert("_b".into(), Some("A".into())).unwrap();
options.cache.insert("_keep".into(), None).unwrap();
let state = PropertyCollectState {
frequencies: FxHashMap::from_iter([
(CompactStr::from("_a"), 1),
(CompactStr::from("_b"), 1),
(CompactStr::from("_auto"), 1),
]),
occupied: FxHashSet::from_iter([CompactStr::from("_keep")]),
};
let (mapping, cache) = assign(&options, &state);
let mut state = PropertyCollectState::new_in(&allocator);
state.frequencies.extend([
(static_ident!("_a"), 1),
(static_ident!("_b"), 1),
(static_ident!("_auto"), 1),
]);
state.occupied.insert(static_ident!("_keep"));
let (mapping, cache) = assign(&options, &state, &allocator);
assert_eq!(mapping["_a"].as_str(), "A");
assert_eq!(mapping["_b"].as_str(), "A");
assert_ne!(mapping["_auto"].as_str(), "A");
@@ -890,23 +938,22 @@ mod tests {
#[test]
fn assignment_filters_ineligible_collected_names() {
let allocator = Allocator::default();
let mut options = options("^(?:_|1$|constructor$)");
options.exclude = Some(lazy_regex::Regex::new("^_excluded(?:One|Two)$").unwrap());
options.reserved.insert("_reserved".into());
options.cache.insert("_cached".into(), None).unwrap();
let state = PropertyCollectState {
frequencies: FxHashMap::from_iter([
(CompactStr::from("_mangle"), 1),
(CompactStr::from("_excludedOne"), 10),
(CompactStr::from("_reserved"), 10),
(CompactStr::from("_cached"), 10),
(CompactStr::from("public"), 10),
(CompactStr::from("1"), 10),
(CompactStr::from("constructor"), 10),
]),
occupied: FxHashSet::default(),
};
let (mapping, cache) = assign(&options, &state);
let mut state = PropertyCollectState::new_in(&allocator);
state.frequencies.extend([
(static_ident!("_mangle"), 1),
(static_ident!("_excludedOne"), 10),
(static_ident!("_reserved"), 10),
(static_ident!("_cached"), 10),
(static_ident!("public"), 10),
(static_ident!("1"), 10),
(static_ident!("constructor"), 10),
]);
let (mapping, cache) = assign(&options, &state, &allocator);
assert_eq!(mapping.len(), 1);
assert!(mapping.contains_key("_mangle"));
assert_eq!(cache.get("_cached"), Some(&None));
@@ -28,10 +28,10 @@ fn mangle_with(
let parsed = Parser::new(&allocator, source, source_type).parse();
assert!(parsed.diagnostics.is_empty(), "{:?}", parsed.diagnostics);
let mut program = parsed.program;
let mut mangler = PropertyMangler::new(options);
let mut mangler = PropertyMangler::new_in(options, &allocator);
mangler.collect(&program);
mangler.assign();
mangler.rewrite(&mut program, &allocator);
mangler.rewrite(&mut program);
let code = Codegen::new().build(&program).code;
(code, mangler.into_cache())
}
@@ -492,10 +492,10 @@ fn duplicated_spans_do_not_skip_property_rewrites() {
let mut program = parsed.program;
ZeroSpans.visit_program(&mut program);
let mut mangler = PropertyMangler::new(options("^_"));
let mut mangler = PropertyMangler::new_in(options("^_"), &allocator);
mangler.collect(&program);
mangler.assign();
mangler.rewrite(&mut program, &allocator);
mangler.rewrite(&mut program);
assert_eq!(
Codegen::new().build(&program).code,
@@ -514,12 +514,12 @@ fn one_assignment_can_be_shared_across_programs() {
.parse()
.program;
let mut mangler = PropertyMangler::new(options("^_"));
let mut mangler = PropertyMangler::new_in(options("^_"), &allocator_a);
mangler.collect(&program_a);
mangler.collect(&program_b);
mangler.assign();
mangler.rewrite(&mut program_a, &allocator_a);
mangler.rewrite(&mut program_b, &allocator_b);
mangler.rewrite(&mut program_a);
mangler.rewrite(&mut program_b);
assert_eq!(Codegen::new().build(&program_a).code, codegen("a.e; a.t;", SourceType::mjs()));
assert_eq!(
@@ -540,15 +540,15 @@ fn independently_collected_programs_can_be_merged_before_assignment() {
.program;
let options = options("^_");
let collected_a = PropertyMangleCollection::from_program(&options, &program_a);
let collected_b = PropertyMangleCollection::from_program(&options, &program_b);
let collected_a = PropertyMangleCollection::from_program(&options, &program_a, &allocator_a);
let collected_b = PropertyMangleCollection::from_program(&options, &program_b, &allocator_b);
let mut mangler = PropertyMangler::new(options);
mangler.merge_collected(collected_a);
mangler.merge_collected(collected_b);
let mut mangler = PropertyMangler::new_in(options, &allocator_a);
mangler.merge_collected(&collected_a);
mangler.merge_collected(&collected_b);
mangler.assign();
mangler.rewrite(&mut program_a, &allocator_a);
mangler.rewrite(&mut program_b, &allocator_b);
mangler.rewrite(&mut program_a);
mangler.rewrite(&mut program_b);
assert_eq!(Codegen::new().build(&program_a).code, codegen("a.e; a.t;", SourceType::mjs()));
assert_eq!(
+2 -2
View File
@@ -146,10 +146,10 @@ fn bench_property_mangler(criterion: &mut Criterion) {
allocator.reset();
let mut program = transform_to_js(&allocator, source_text, source_type, path);
runner.run(|| {
let mut mangler = PropertyMangler::new(options.clone());
let mut mangler = PropertyMangler::new_in(options.clone(), &allocator);
mangler.collect(&program);
mangler.assign();
mangler.rewrite(&mut program, &allocator);
mangler.rewrite(&mut program);
std::hint::black_box(mangler.into_cache());
});
});