use std::str;
use itoa::Buffer as ItoaBuffer;
use rustc_hash::FxHashMap;
use oxc_allocator::{Allocator, StringBuilder as ArenaStringBuilder};
use oxc_semantic::Scoping;
use oxc_span::Atom;
pub struct UidGenerator<'a> {
names: FxHashMap<&'a str, UidName>,
allocator: &'a Allocator,
}
#[cfg_attr(target_pointer_width = "64", repr(align(8)))]
#[derive(Clone, Copy)]
struct UidName {
postfix: u32,
underscore_count: u32,
}
impl<'a> UidGenerator<'a> {
pub(super) fn new(scoping: &Scoping, allocator: &'a Allocator) -> Self {
let mut generator = Self { names: FxHashMap::default(), allocator };
for name in scoping.symbol_names() {
generator.add(name);
}
for &name in scoping.root_unresolved_references().keys() {
generator.add(name);
}
generator
}
fn add(&mut self, name: &str) {
if name.as_bytes().first() != Some(&b'_') {
return;
}
let original_len = name.len();
let name = unsafe { name.get_unchecked(1..) };
let mut name = name.trim_start_matches('_');
#[expect(clippy::cast_possible_truncation)]
let underscore_count = (original_len - name.len()) as u32;
let mut uid_name = UidName { underscore_count, postfix: 1 };
let last_non_digit_index = name.as_bytes().iter().rposition(|&b| !b.is_ascii_digit());
let parts = match last_non_digit_index {
Some(last_non_digit_index) => {
if last_non_digit_index == name.len() - 1 {
None
} else {
let digit_index = last_non_digit_index + 1;
debug_assert!(name.as_bytes().get(digit_index).is_some_and(u8::is_ascii_digit));
unsafe {
let without_digits = name.get_unchecked(..digit_index);
let digits = name.get_unchecked(digit_index..);
Some((without_digits, digits))
}
}
}
None => {
if name.is_empty() {
None
} else {
Some(("", name))
}
}
};
if let Some((without_digits, digits)) = parts {
const U32_MAX_LEN: usize = "4294967295".len(); let first_digit = unsafe { *digits.as_bytes().get_unchecked(0) };
if first_digit == b'0' || digits.len() > U32_MAX_LEN {
return;
}
if let Ok(n) = digits.parse::<u32>() {
if n == 1 {
return;
}
name = without_digits;
uid_name.postfix = n;
} else {
return;
}
}
if let Some(existing_uid_name) = self.names.get_mut(name) {
if uid_name.underscore_count > existing_uid_name.underscore_count
|| (uid_name.underscore_count == existing_uid_name.underscore_count
&& uid_name.postfix > existing_uid_name.postfix)
{
*existing_uid_name = uid_name;
}
} else {
let name = self.allocator.alloc_str(name);
self.names.insert(name, uid_name);
}
}
pub(super) fn create(&mut self, name: &str) -> Atom<'a> {
let mut bytes = name.as_bytes();
while bytes.first() == Some(&b'_') {
bytes = &bytes[1..];
}
while matches!(bytes.last(), Some(b) if b.is_ascii_digit()) {
bytes = &bytes[0..bytes.len() - 1];
}
let base = unsafe { str::from_utf8_unchecked(bytes) };
if let Some(uid_name) = self.names.get_mut(base) {
if uid_name.postfix < u32::MAX {
uid_name.postfix += 1;
} else {
uid_name.underscore_count += 1;
uid_name.postfix = 2;
}
let mut buffer = ItoaBuffer::new();
let digits = buffer.format(uid_name.postfix);
if uid_name.underscore_count == 1 {
Atom::from_strs_array_in(["_", base, digits], self.allocator)
} else {
let mut uid = ArenaStringBuilder::with_capacity_in(
uid_name.underscore_count as usize + base.len() + digits.len(),
self.allocator,
);
uid.push_ascii_byte_repeat(b'_', uid_name.underscore_count as usize);
uid.push_str(base);
uid.push_str(digits);
Atom::from(uid)
}
} else {
let uid = Atom::from_strs_array_in(["_", base], self.allocator);
let base = unsafe { uid.as_str().get_unchecked(1..) };
self.names.insert(base, UidName { underscore_count: 1, postfix: 1 });
uid
}
}
}
#[cfg(test)]
#[test]
fn uids() {
#[expect(clippy::type_complexity)]
let cases: &[(&[&str], &[(&str, &str)])] = &[
(&[], &[("foo", "_foo"), ("foo", "_foo2"), ("foo", "_foo3")]),
(
&["foo", "foo0", "foo1", "foo2", "foo10", "_bar"],
&[("foo", "_foo"), ("foo", "_foo2"), ("foo", "_foo3")],
),
(
&["_foo0", "_foo1", "__foo0", "____foo1", "_foo01", "_foo012345", "_foo000000"],
&[("foo", "_foo"), ("foo", "_foo2"), ("foo", "_foo3")],
),
(&[], &[("_foo", "_foo"), ("__foo", "_foo2"), ("_____foo", "_foo3")]),
(&[], &[("_foo123", "_foo"), ("__foo456", "_foo2"), ("_____foo789", "_foo3")]),
(&["_foo"], &[("foo", "_foo2"), ("foo", "_foo3"), ("foo", "_foo4")]),
(&["_foo3"], &[("foo", "_foo4"), ("foo", "_foo5"), ("foo", "_foo6")]),
(&["__foo"], &[("foo", "__foo2"), ("foo", "__foo3"), ("foo", "__foo4")]),
(&["__foo8"], &[("foo", "__foo9"), ("foo", "__foo10"), ("foo", "__foo11")]),
(&["_foo999", "____foo"], &[("foo", "____foo2"), ("foo", "____foo3"), ("foo", "____foo4")]),
(
&["_foo4294967293"],
&[
("foo", "_foo4294967294"),
("foo", "_foo4294967295"),
("foo", "__foo2"),
("foo", "__foo3"),
],
),
(
&["___foo4294967293"],
&[
("foo", "___foo4294967294"),
("foo", "___foo4294967295"),
("foo", "____foo2"),
("foo", "____foo3"),
],
),
(&[], &[("_", "_"), ("_", "_2"), ("_", "_3")]),
(
&["_0", "_1", "__0", "____1", "_01", "_012345", "_000000"],
&[("_", "_"), ("_", "_2"), ("_", "_3")],
),
(&[], &[("___", "_"), ("_____", "_2"), ("_____", "_3")]),
(&["_"], &[("_", "_2"), ("_", "_3"), ("_", "_4")]),
(&["_4"], &[("_", "_5"), ("_", "_6"), ("_", "_7")]),
(&["___"], &[("_", "___2"), ("_", "___3"), ("_", "___4")]),
(&["___99"], &[("_", "___100"), ("_", "___101"), ("_", "___102")]),
(&["_"], &[("_123", "_2"), ("__456", "_3"), ("___789", "_4")]),
];
let allocator = Allocator::default();
for &(used_names, created) in cases {
let mut generator = UidGenerator { names: FxHashMap::default(), allocator: &allocator };
for &used_name in used_names {
generator.add(used_name);
}
for &(name, uid) in created {
assert_eq!(generator.create(name), uid);
}
}
}