use core::cmp::Reverse;
use std::collections::hash_map::Entry;
use oxc_str::CompactStr;
use rolldown_common::{ModuleTable, SymbolRef, SymbolRefDb};
use rolldown_utils::{concat_string, rustc_hash::FxHashMapExt};
use rustc_hash::FxHashMap;
#[derive(Debug, Default)]
pub struct ConflictResolver {
used: FxHashMap<CompactStr, u32>,
}
impl ConflictResolver {
pub fn new(reserved: impl IntoIterator<Item = CompactStr>) -> Self {
Self { used: reserved.into_iter().map(|name| (name, 0)).collect() }
}
pub fn with_capacity(capacity: usize) -> Self {
Self { used: FxHashMap::with_capacity(capacity) }
}
pub fn reserve(&mut self, name: CompactStr) {
self.used.entry(name).or_insert(0);
}
pub fn contains(&self, name: &str) -> bool {
self.used.contains_key(name)
}
#[inline]
pub fn resolve(&mut self, base: CompactStr, accept: impl Fn(&str, bool) -> bool) -> CompactStr {
let (base, mut conflict_index) = match self.used.entry(base) {
Entry::Vacant(slot) => {
if accept(slot.key().as_str(), true) {
let name = slot.key().clone();
slot.insert(0);
return name;
}
(slot.into_key(), 1)
}
Entry::Occupied(mut slot) => {
*slot.get_mut() += 1;
(slot.key().clone(), *slot.get())
}
};
loop {
let candidate: CompactStr =
concat_string!(base, "$", itoa::Buffer::new().format(conflict_index)).into();
if let Some(idx) = self.used.get_mut(&candidate) {
*idx += 1;
conflict_index = *idx;
continue;
}
if !accept(&candidate, false) {
conflict_index += 1;
continue;
}
self.used.insert(candidate.clone(), 0);
return candidate;
}
}
}
pub fn deconflict_order_key<'a>(
symbol_ref: SymbolRef,
module_table: &ModuleTable,
symbol_db: &'a SymbolRefDb,
) -> (Reverse<u32>, &'a str) {
let exec_order = module_table[symbol_ref.owner].exec_order();
(Reverse(exec_order), symbol_ref.name(symbol_db))
}
#[cfg(test)]
mod tests {
use super::*;
fn cs(s: &str) -> CompactStr {
CompactStr::new(s)
}
#[test]
fn bare_name_when_free() {
let mut r = ConflictResolver::new(std::iter::empty());
assert_eq!(r.resolve(cs("foo"), |_, _| true).as_str(), "foo");
}
#[test]
fn suffix_on_collision() {
let mut r = ConflictResolver::new(std::iter::empty());
assert_eq!(r.resolve(cs("foo"), |_, _| true).as_str(), "foo");
assert_eq!(r.resolve(cs("foo"), |_, _| true).as_str(), "foo$1");
assert_eq!(r.resolve(cs("foo"), |_, _| true).as_str(), "foo$2");
}
#[test]
fn reserved_names_are_avoided() {
let mut r = ConflictResolver::new([cs("foo")]);
assert_eq!(r.resolve(cs("foo"), |_, _| true).as_str(), "foo$1");
}
#[test]
fn accept_veto_skips_candidate() {
let mut r = ConflictResolver::new(std::iter::empty());
assert_eq!(r.resolve(cs("foo"), |_, _| true).as_str(), "foo"); assert_eq!(r.resolve(cs("foo"), |cand, _is_original| cand != "foo$1").as_str(), "foo$2");
}
#[test]
fn jump_uses_stored_counter_of_occupied_candidate() {
let mut r = ConflictResolver::new(std::iter::empty());
r.reserve(cs("foo"));
r.reserve(cs("foo$1"));
assert_eq!(r.resolve(cs("foo"), |_, _| true).as_str(), "foo$2");
}
#[test]
fn jump_walks_multi_hop_chain() {
let mut r = ConflictResolver::new(std::iter::empty());
r.reserve(cs("foo"));
r.reserve(cs("foo$1"));
r.reserve(cs("foo$2"));
assert_eq!(r.resolve(cs("foo"), |_, _| true).as_str(), "foo$3");
}
#[test]
fn contains_reports_reserved() {
let mut r = ConflictResolver::new([cs("a")]);
assert!(r.contains("a"));
assert!(!r.contains("b"));
r.reserve(cs("b"));
assert!(r.contains("b"));
}
#[test]
fn order_key_is_reverse_exec_order_then_name() {
use core::cmp::Reverse;
let a = (Reverse(5u32), CompactStr::new("b"));
let b = (Reverse(5u32), CompactStr::new("a"));
let c = (Reverse(6u32), CompactStr::new("z"));
let mut v = vec![a.clone(), b.clone(), c.clone()];
v.sort();
assert_eq!(v, vec![c, b, a]);
}
}