oxc/crates/oxc_traverse/src/context/scoping.rs
overlookmotel f2f31a8d7a
fix(traverse)!: remove unsound APIs (#7514)
It's essential to `oxc_traverse`'s safety scheme that the user cannot create a `TraverseAncestry`, because they could then substitute it for the one stored in `TraverseCtx`, and cause a buffer underrun when an ancestor gets popped off stack which should never be empty - but it is because user has sneakily swapped it for another one.

Not being able to create a `TraverseAncestry` also requires that user cannot obtain an owned `TraverseCtx` either, because you can obtain an owned `TraverseAncestry` from an owned `TraverseCtx`.

Therefore, it's unsound for `TraverseCtx::new` to be public.

However, it is useful in minifier to be able to re-use the same `TraverseCtx` over and over, which requires having an owned `TraverseCtx`.

To support this use case, introduce `ReusableTraverseCtx`. It is an opaque wrapper around `TraverseCtx`, which prevents accessing the `TraverseCtx` inside it. It's safe for user to own a `ReusableTraverseCtx`, because there's nothing they can do with it except for using it to traverse via `traverse_mut_with_ctx`, which ensures the safety invariants are upheld.

At some point, we'll hopefully be able to reduce the number of passes in the minifier, and so remove the need for `ReusableTraverseCtx`.But in the meantime, this keeps `Traverse`'s API safe from unsound abuse.

Note: Strictly speaking, there is still room to abuse the API and produce UB by initiating a 2nd traversal of a different AST in an `Traverse` visitor, and then `mem::swap` the 2 x `&mut TraverseCtx`s. But this is a completely bizarre thing to do, and would basically require you to write malicious code specifically designed to cause UB, so it's not a real risk in practice. We'd need branded lifetimes to close that hole too.

So this PR doesn't 100% ensure safety in a formal sense, but it at least makes it very hard to trigger UB *by accident*, which was the risk before.
2024-11-28 10:30:53 +08:00

700 lines
28 KiB
Rust

use std::str;
use compact_str::CompactString;
use itoa::Buffer as ItoaBuffer;
use rustc_hash::FxHashSet;
use oxc_ast::{ast::*, visit::Visit};
use oxc_semantic::{NodeId, Reference, ScopeTree, SymbolTable};
use oxc_span::{CompactStr, SPAN};
use oxc_syntax::{
reference::{ReferenceFlags, ReferenceId},
scope::{ScopeFlags, ScopeId},
symbol::{SymbolFlags, SymbolId},
};
use crate::{scopes_collector::ChildScopeCollector, BoundIdentifier};
/// Traverse scope context.
///
/// Contains the scope tree and symbols table, and provides methods to access them.
///
/// `current_scope_id` is the ID of current scope during traversal.
/// `walk_*` functions update this field when entering/exiting a scope.
pub struct TraverseScoping {
scopes: ScopeTree,
symbols: SymbolTable,
uid_names: Option<FxHashSet<CompactStr>>,
current_scope_id: ScopeId,
current_hoist_scope_id: ScopeId,
}
// Public methods
impl TraverseScoping {
/// Get current scope ID
#[inline]
pub fn current_scope_id(&self) -> ScopeId {
self.current_scope_id
}
/// Get current var hoisting scope ID
#[inline]
pub(crate) fn current_hoist_scope_id(&self) -> ScopeId {
self.current_hoist_scope_id
}
/// Get current scope flags
#[inline]
pub fn current_scope_flags(&self) -> ScopeFlags {
self.scopes.get_flags(self.current_scope_id)
}
/// Get scopes tree
#[inline]
pub fn scopes(&self) -> &ScopeTree {
&self.scopes
}
/// Get mutable scopes tree
#[inline]
pub fn scopes_mut(&mut self) -> &mut ScopeTree {
&mut self.scopes
}
/// Get symbols table
#[inline]
pub fn symbols(&self) -> &SymbolTable {
&self.symbols
}
/// Get mutable symbols table
#[inline]
pub fn symbols_mut(&mut self) -> &mut SymbolTable {
&mut self.symbols
}
/// Get iterator over scopes, starting with current scope and working up
pub fn ancestor_scopes(&self) -> impl Iterator<Item = ScopeId> + '_ {
self.scopes.ancestors(self.current_scope_id)
}
/// Create new scope as child of provided scope.
///
/// `flags` provided are amended to inherit from parent scope's flags.
pub fn create_child_scope(&mut self, parent_id: ScopeId, flags: ScopeFlags) -> ScopeId {
let flags = self.scopes.get_new_scope_flags(flags, parent_id);
self.scopes.add_scope(Some(parent_id), NodeId::DUMMY, flags)
}
/// Create new scope as child of current scope.
///
/// `flags` provided are amended to inherit from parent scope's flags.
pub fn create_child_scope_of_current(&mut self, flags: ScopeFlags) -> ScopeId {
self.create_child_scope(self.current_scope_id, flags)
}
/// Insert a scope into scope tree below a statement.
///
/// Statement must be in current scope.
/// New scope is created as child of current scope.
/// All child scopes of the statement are reassigned to be children of the new scope.
///
/// `flags` provided are amended to inherit from parent scope's flags.
pub fn insert_scope_below_statement(&mut self, stmt: &Statement, flags: ScopeFlags) -> ScopeId {
let mut collector = ChildScopeCollector::new();
collector.visit_statement(stmt);
self.insert_scope_below(&collector.scope_ids, flags)
}
/// Insert a scope into scope tree below an expression.
///
/// Expression must be in current scope.
/// New scope is created as child of current scope.
/// All child scopes of the expression are reassigned to be children of the new scope.
///
/// `flags` provided are amended to inherit from parent scope's flags.
pub fn insert_scope_below_expression(
&mut self,
expr: &Expression,
flags: ScopeFlags,
) -> ScopeId {
let mut collector = ChildScopeCollector::new();
collector.visit_expression(expr);
self.insert_scope_below(&collector.scope_ids, flags)
}
fn insert_scope_below(&mut self, child_scope_ids: &[ScopeId], flags: ScopeFlags) -> ScopeId {
// Remove these scopes from parent's children
if self.scopes.has_child_ids() {
let current_child_scope_ids = self.scopes.get_child_ids_mut(self.current_scope_id);
current_child_scope_ids.retain(|scope_id| !child_scope_ids.contains(scope_id));
}
// Create new scope as child of parent
let new_scope_id = self.create_child_scope_of_current(flags);
// Set scopes as children of new scope instead
for &child_id in child_scope_ids {
self.scopes.set_parent_id(child_id, Some(new_scope_id));
}
new_scope_id
}
/// Remove scope for an expression from the scope chain.
///
/// Delete the scope and set parent of its child scopes to its parent scope.
/// e.g.:
/// * Starting scopes parentage `A -> B`, `B -> C`, `B -> D`.
/// * Remove scope `B` from chain.
/// * End result: scopes `A -> C`, `A -> D`.
///
/// Use this when removing an expression which owns a scope, without removing its children.
/// For example when unwrapping `(() => foo)()` to just `foo`.
/// `foo` here could be an expression which itself contains scopes.
pub fn remove_scope_for_expression(&mut self, scope_id: ScopeId, expr: &Expression) {
let mut collector = ChildScopeCollector::new();
collector.visit_expression(expr);
let child_ids = collector.scope_ids;
if !child_ids.is_empty() {
let parent_id = self.scopes.get_parent_id(scope_id);
for child_id in child_ids {
self.scopes.set_parent_id(child_id, parent_id);
}
}
self.scopes.delete_scope(scope_id);
}
/// Add binding to [`ScopeTree`] and [`SymbolTable`].
#[inline]
pub(crate) fn add_binding(
&mut self,
name: CompactStr,
scope_id: ScopeId,
flags: SymbolFlags,
) -> SymbolId {
let symbol_id =
self.symbols.create_symbol(SPAN, name.clone(), flags, scope_id, NodeId::DUMMY);
self.scopes.add_binding(scope_id, name, symbol_id);
symbol_id
}
/// Generate binding.
///
/// Creates a symbol with the provided name and flags and adds it to the specified scope.
pub fn generate_binding<'a>(
&mut self,
name: Atom<'a>,
scope_id: ScopeId,
flags: SymbolFlags,
) -> BoundIdentifier<'a> {
let symbol_id = self.add_binding(name.to_compact_str(), scope_id, flags);
BoundIdentifier::new(name, symbol_id)
}
/// Generate binding in current scope.
///
/// Creates a symbol with the provided name and flags and adds it to the current scope.
pub fn generate_binding_in_current_scope<'a>(
&mut self,
name: Atom<'a>,
flags: SymbolFlags,
) -> BoundIdentifier<'a> {
self.generate_binding(name, self.current_scope_id, flags)
}
/// Generate UID var name.
///
/// Finds a unique variable name which does clash with any other variables used in the program.
///
/// Based on Babel's `scope.generateUid` logic.
/// <https://github.com/babel/babel/blob/3b1a3c0be9df65140260a316c1a21adcf948645d/packages/babel-traverse/src/scope/index.ts#L501-L523>
///
/// # Differences from Babel
///
/// This implementation aims to replicate Babel's behavior, but differs from Babel
/// in the following ways:
///
/// 1. Does not check that name is a valid JS identifier name.
/// In most cases, we'll be creating a UID based on an existing variable name, in which case
/// this check is redundant.
/// Caller must ensure `name` is a valid JS identifier, after a `_` is prepended on start.
/// The fact that a `_` will be prepended on start means providing an empty string or a string
/// starting with a digit (0-9) is fine.
///
/// 2. Does not convert to camel case.
/// This seems unimportant.
///
/// 3. Does not check var name against list of globals or "contextVariables"
/// (which Babel does in `hasBinding`).
/// No globals or "contextVariables" start with `_` anyway, so no need for this check.
///
/// 4. Does not check this name is unique if used as a named statement label, only that it's unique
/// as an identifier.
/// If we need to generate unique labels for named statements, we should create a separate method
/// `generate_uid_label`.
///
/// 5. Does not check against list of other UIDs that have been created.
/// `TraverseScoping::generate_uid` adds this name to symbols table, so when creating next UID,
/// this one will be found and avoided, like any other existing binding. So it's not needed.
///
/// # Potential improvements
///
/// TODO(improve-on-babel):
///
/// This function is fairly expensive, because it aims to replicate Babel's output.
///
/// `get_uid_names` iterates through every single binding and unresolved reference in the entire AST,
/// and builds a hashset of symbols which could clash with UIDs.
/// Once that's built, it's cached, but `find_uid_name` still has to do at least one hashset lookup,
/// and a hashset insert. If the first name tried is already in use, it will do another hashset lookup,
/// potentially multiple times until a name which isn't taken is found.
///
/// We could improve this in one of 3 ways:
///
/// 1. Build the hashset in `SemanticBuilder` instead of iterating through all symbols again here.
///
/// 2. Use a much simpler method:
///
/// * During initial semantic pass, check for any existing identifiers starting with `_`.
/// * Calculate what is the highest postfix number on `_...` identifiers (e.g. `_foo1`, `_bar8`).
/// * Store that highest number in a counter which is global across the whole program.
/// * When creating a UID, increment the counter, and make the UID `_<name><counter>`.
///
/// i.e. if source contains identifiers `_foo1` and `_bar15`, create UIDs named `_qux16`,
/// `_temp17` etc. They'll all be unique within the program.
///
/// Minimal cost in semantic, and generating UIDs extremely cheap.
///
/// This is a slightly different method from Babel, and unfortunately produces UID names
/// which differ from Babel for some of its test cases.
///
/// 3. If output is being minified anyway, use a method which produces less debuggable output,
/// but is even simpler:
///
/// * During initial semantic pass, check for any existing identifiers starting with `_`.
/// * Find the highest number of leading `_`s for any existing symbol.
/// * Generate UIDs with a counter starting at 0, prefixed with number of `_`s one greater than
/// what was found in AST.
/// i.e. if source contains identifiers `_foo` and `__bar`, create UIDs names `___0`, `___1`,
/// `___2` etc. They'll all be unique within the program.
#[allow(clippy::missing_panics_doc)]
pub fn generate_uid_name(&mut self, name: &str) -> CompactStr {
// If `uid_names` is not already populated, initialize it
if self.uid_names.is_none() {
self.uid_names = Some(self.get_uid_names());
}
let uid_names = self.uid_names.as_mut().unwrap();
let base = get_uid_name_base(name);
let uid = get_unique_name(base, uid_names);
uid_names.insert(uid.clone());
uid
}
/// Create a reference bound to a `SymbolId`
pub fn create_bound_reference(
&mut self,
symbol_id: SymbolId,
flags: ReferenceFlags,
) -> ReferenceId {
let reference = Reference::new_with_symbol_id(NodeId::DUMMY, symbol_id, flags);
let reference_id = self.symbols.create_reference(reference);
self.symbols.resolved_references[symbol_id].push(reference_id);
reference_id
}
/// Create an unbound reference
pub fn create_unbound_reference(
&mut self,
name: CompactStr,
flags: ReferenceFlags,
) -> ReferenceId {
let reference = Reference::new(NodeId::DUMMY, flags);
let reference_id = self.symbols.create_reference(reference);
self.scopes.add_root_unresolved_reference(name, reference_id);
reference_id
}
/// Create a reference optionally bound to a `SymbolId`.
///
/// If you know if there's a `SymbolId` or not, prefer `TraverseCtx::create_bound_reference`
/// or `TraverseCtx::create_unbound_reference`.
pub fn create_reference(
&mut self,
name: CompactStr,
symbol_id: Option<SymbolId>,
flags: ReferenceFlags,
) -> ReferenceId {
if let Some(symbol_id) = symbol_id {
self.create_bound_reference(symbol_id, flags)
} else {
self.create_unbound_reference(name, flags)
}
}
/// Create reference in current scope, looking up binding for `name`
pub fn create_reference_in_current_scope(
&mut self,
name: CompactStr,
flags: ReferenceFlags,
) -> ReferenceId {
let symbol_id = self.scopes.find_binding(self.current_scope_id, name.as_str());
self.create_reference(name, symbol_id, flags)
}
/// Delete a reference.
///
/// Provided `name` must match `reference_id`.
pub fn delete_reference(&mut self, reference_id: ReferenceId, name: &str) {
let symbol_id = self.symbols.get_reference(reference_id).symbol_id();
if let Some(symbol_id) = symbol_id {
self.symbols.delete_resolved_reference(symbol_id, reference_id);
} else {
self.scopes.delete_root_unresolved_reference(name, reference_id);
}
}
/// Delete reference for an `IdentifierReference`.
pub fn delete_reference_for_identifier(&mut self, ident: &IdentifierReference) {
self.delete_reference(ident.reference_id(), &ident.name);
}
/// Determine whether evaluating the specific input `node` is a consequenceless reference.
///
/// i.e. evaluating it won't result in potentially arbitrary code from being run.
/// The following are allowed and determined not to cause side effects:
///
/// - `this` expressions
/// - `super` expressions
/// - Bound identifiers which are not mutated
///
/// Based on Babel's `scope.isStatic` logic.
/// <https://github.com/babel/babel/blob/419644f27c5c59deb19e71aaabd417a3bc5483ca/packages/babel-traverse/src/scope/index.ts#L557>
#[inline]
pub fn is_static(&self, expr: &Expression) -> bool {
match expr {
Expression::ThisExpression(_) | Expression::Super(_) => true,
Expression::Identifier(ident) => {
self.symbols.get_reference(ident.reference_id()).symbol_id().is_some_and(
|symbol_id| {
self.symbols.get_resolved_references(symbol_id).all(|r| !r.is_write())
},
)
}
_ => false,
}
}
}
// Methods used internally within crate
impl TraverseScoping {
/// Create new `TraverseScoping`
pub(super) fn new(scopes: ScopeTree, symbols: SymbolTable) -> Self {
Self {
scopes,
symbols,
uid_names: None,
// Dummy values. Both immediately overwritten in `walk_program`.
current_scope_id: ScopeId::new(0),
current_hoist_scope_id: ScopeId::new(0),
}
}
/// Consume [`TraverseScoping`] and return [`SymbolTable`] and [`ScopeTree`].
pub(super) fn into_symbol_table_and_scope_tree(self) -> (SymbolTable, ScopeTree) {
(self.symbols, self.scopes)
}
/// Set current scope ID
#[inline]
pub(crate) fn set_current_scope_id(&mut self, scope_id: ScopeId) {
self.current_scope_id = scope_id;
}
/// Set current hoist scope ID
#[inline]
pub(crate) fn set_current_hoist_scope_id(&mut self, scope_id: ScopeId) {
self.current_hoist_scope_id = scope_id;
}
/// Get `uid_names`.
///
/// Iterate through all symbols and unresolved references in AST and identify any var names
/// which could clash with UIDs (start with `_`). Build a hash set containing them.
///
/// Once this set is created, generating a UID is a relatively quick operation, rather than
/// iterating over all symbols and unresolved references every time generate a UID.
fn get_uid_names(&mut self) -> FxHashSet<CompactStr> {
self.scopes
.root_unresolved_references()
.keys()
.chain(self.symbols.names.iter())
.filter_map(|name| {
if name.as_bytes().first() == Some(&b'_') {
Some(name.clone())
} else {
None
}
})
.collect()
}
}
/// Create base for UID name based on provided `name`.
/// Trim `_`s from start and digits from end.
/// i.e. `__foo123` -> `foo`
fn get_uid_name_base(name: &str) -> &str {
// Equivalent to `name.trim_start_matches('_').trim_end_matches(|c: char| c.is_ascii_digit())`
// but more efficient as operates on bytes not chars
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];
}
// SAFETY: We started with a valid UTF8 `&str` and have only trimmed off ASCII characters,
// so remainder must still be valid UTF8
unsafe { str::from_utf8_unchecked(bytes) }
}
fn get_unique_name(base: &str, uid_names: &FxHashSet<CompactStr>) -> CompactStr {
CompactStr::from(get_unique_name_impl(base, uid_names))
}
// TODO: We could make this function more performant, especially when it checks a lot of names
// before it reaches one that is unused.
// This function repeatedly creates strings which have only differ from each other by digits added on end,
// and then hashes each of those strings to test them against the hash set `uid_names`.
// Hashing strings is fairly expensive. As here only the end of the string changes on each iteration,
// we could calculate an "unfinished" hash not including the last block, and then just add the final
// block to "finish" the hash on each iteration. With `FxHash` this would be straight line code and only
// a few operations.
fn get_unique_name_impl(base: &str, uid_names: &FxHashSet<CompactStr>) -> CompactString {
// Create `CompactString` prepending name with `_`, and with 1 byte excess capacity.
// The extra byte is to avoid reallocation if need to add a digit on the end later,
// which will not be too uncommon.
// Having to add 2 digits will be uncommon, so we don't allocate 2 extra bytes for 2 digits.
let mut name = CompactString::with_capacity(base.len() + 2);
name.push('_');
name.push_str(base);
// It's fairly common that UIDs may need a numerical postfix, so we try to keep string
// operations to a minimum for postfixes up to 99 - reusing a single `CompactString`,
// rather than generating a new string on each attempt.
// For speed we manipulate the string as bytes.
// Postfixes greater than 99 should be very uncommon, so don't bother optimizing.
//
// SAFETY: Only modifications to string are replacing last byte/last 2 bytes with ASCII digits.
// These bytes are already ASCII chars, so cannot produce an invalid UTF-8 string.
// Writes are always in bounds (`bytes` is redefined after string grows due to `push`).
unsafe {
let name_is_unique = |bytes: &[u8]| {
let name = str::from_utf8_unchecked(bytes);
!uid_names.contains(name)
};
// Try the name without a numerical postfix (i.e. plain `_temp`)
let bytes = name.as_bytes_mut();
if name_is_unique(bytes) {
return name;
}
// Try single-digit postfixes (i.e. `_temp2`, `_temp3` ... `_temp9`)
name.push('2');
let bytes = name.as_bytes_mut();
if name_is_unique(bytes) {
return name;
}
let last_index = bytes.len() - 1;
for c in b'3'..=b'9' {
*bytes.get_unchecked_mut(last_index) = c;
if name_is_unique(bytes) {
return name;
}
}
// Try double-digit postfixes (i.e. `_temp10` ... `_temp99`)
*bytes.get_unchecked_mut(last_index) = b'1';
name.push('0');
let bytes = name.as_bytes_mut();
let last_index = last_index + 1;
let mut c1 = b'1';
loop {
if name_is_unique(bytes) {
return name;
}
for c2 in b'1'..=b'9' {
*bytes.get_unchecked_mut(last_index) = c2;
if name_is_unique(bytes) {
return name;
}
}
if c1 == b'9' {
break;
}
c1 += 1;
let last_two: &mut [u8; 2] =
bytes.get_unchecked_mut(last_index - 1..=last_index).try_into().unwrap();
*last_two = [c1, b'0'];
}
}
// Try longer postfixes (`_temp100` upwards)
// Reserve space for 1 more byte for the additional 3rd digit.
// Do this here so that `name.push_str(digits)` will never need to grow the string until it reaches
// `n == 1000`, which makes the branch on "is there sufficient capacity to push?" in the loop below
// completely predictable for `n < 1000`.
name.reserve(1);
// At this point, `name` has had 2 digits added on end. `base_len` is length without those 2 digits.
let base_len = name.len() - 2;
let mut buffer = ItoaBuffer::new();
for n in 100..=u32::MAX {
let digits = buffer.format(n);
// SAFETY: `base_len` is always shorter than current `name.len()`, on a UTF-8 char boundary,
// and `name` contains at least `base_len` initialized bytes
unsafe { name.set_len(base_len) };
name.push_str(digits);
if !uid_names.contains(name.as_str()) {
return name;
}
}
// Limit for size of source text is `u32::MAX` bytes, so there cannot be `u32::MAX`
// identifier names in the AST. So loop above cannot fail to find an unused name.
unreachable!();
}
#[cfg(test)]
#[test]
fn test_get_unique_name() {
let cases: &[(&[&str], &str, &str)] = &[
(&[], "foo", "_foo"),
(&["_foo"], "foo", "_foo2"),
(&["_foo0", "_foo1"], "foo", "_foo"),
(&["_foo2", "_foo3", "_foo4"], "foo", "_foo"),
(&["_foo", "_foo2"], "foo", "_foo3"),
(&["_foo", "_foo2", "_foo4"], "foo", "_foo3"),
(&["_foo", "_foo2", "_foo3", "_foo4", "_foo5", "_foo6", "_foo7", "_foo8"], "foo", "_foo9"),
(
&["_foo", "_foo2", "_foo3", "_foo4", "_foo5", "_foo6", "_foo7", "_foo8", "_foo9"],
"foo",
"_foo10",
),
(
&[
"_foo", "_foo2", "_foo3", "_foo4", "_foo5", "_foo6", "_foo7", "_foo8", "_foo9",
"_foo10",
],
"foo",
"_foo11",
),
(
&[
"_foo", "_foo2", "_foo3", "_foo4", "_foo5", "_foo6", "_foo7", "_foo8", "_foo9",
"_foo10", "_foo11",
],
"foo",
"_foo12",
),
(
&[
"_foo", "_foo2", "_foo3", "_foo4", "_foo5", "_foo6", "_foo7", "_foo8", "_foo9",
"_foo10", "_foo11", "_foo12", "_foo13", "_foo14", "_foo15", "_foo16", "_foo17",
"_foo18",
],
"foo",
"_foo19",
),
(
&[
"_foo", "_foo2", "_foo3", "_foo4", "_foo5", "_foo6", "_foo7", "_foo8", "_foo9",
"_foo10", "_foo11", "_foo12", "_foo13", "_foo14", "_foo15", "_foo16", "_foo17",
"_foo18", "_foo19",
],
"foo",
"_foo20",
),
(
&[
"_foo", "_foo2", "_foo3", "_foo4", "_foo5", "_foo6", "_foo7", "_foo8", "_foo9",
"_foo10", "_foo11", "_foo12", "_foo13", "_foo14", "_foo15", "_foo16", "_foo17",
"_foo18", "_foo19", "_foo20",
],
"foo",
"_foo21",
),
(
&[
"_foo", "_foo2", "_foo3", "_foo4", "_foo5", "_foo6", "_foo7", "_foo8", "_foo9",
"_foo10", "_foo11", "_foo12", "_foo13", "_foo14", "_foo15", "_foo16", "_foo17",
"_foo18", "_foo19", "_foo20", "_foo21", "_foo22", "_foo23", "_foo24", "_foo25",
"_foo26", "_foo27", "_foo28", "_foo29", "_foo30", "_foo31", "_foo32", "_foo33",
"_foo34", "_foo35", "_foo36", "_foo37", "_foo38", "_foo39", "_foo40", "_foo41",
"_foo42", "_foo43", "_foo44", "_foo45", "_foo46", "_foo47", "_foo48", "_foo49",
"_foo50", "_foo51", "_foo52", "_foo53", "_foo54", "_foo55", "_foo56", "_foo57",
"_foo58", "_foo59", "_foo60", "_foo61", "_foo62", "_foo63", "_foo64", "_foo65",
"_foo66", "_foo67", "_foo68", "_foo69", "_foo70", "_foo71", "_foo72", "_foo73",
"_foo74", "_foo75", "_foo76", "_foo77", "_foo78", "_foo79", "_foo80", "_foo81",
"_foo82", "_foo83", "_foo84", "_foo85", "_foo86", "_foo87", "_foo88", "_foo89",
"_foo90", "_foo91", "_foo92", "_foo93", "_foo94", "_foo95", "_foo96", "_foo97",
"_foo98",
],
"foo",
"_foo99",
),
(
&[
"_foo", "_foo2", "_foo3", "_foo4", "_foo5", "_foo6", "_foo7", "_foo8", "_foo9",
"_foo10", "_foo11", "_foo12", "_foo13", "_foo14", "_foo15", "_foo16", "_foo17",
"_foo18", "_foo19", "_foo20", "_foo21", "_foo22", "_foo23", "_foo24", "_foo25",
"_foo26", "_foo27", "_foo28", "_foo29", "_foo30", "_foo31", "_foo32", "_foo33",
"_foo34", "_foo35", "_foo36", "_foo37", "_foo38", "_foo39", "_foo40", "_foo41",
"_foo42", "_foo43", "_foo44", "_foo45", "_foo46", "_foo47", "_foo48", "_foo49",
"_foo50", "_foo51", "_foo52", "_foo53", "_foo54", "_foo55", "_foo56", "_foo57",
"_foo58", "_foo59", "_foo60", "_foo61", "_foo62", "_foo63", "_foo64", "_foo65",
"_foo66", "_foo67", "_foo68", "_foo69", "_foo70", "_foo71", "_foo72", "_foo73",
"_foo74", "_foo75", "_foo76", "_foo77", "_foo78", "_foo79", "_foo80", "_foo81",
"_foo82", "_foo83", "_foo84", "_foo85", "_foo86", "_foo87", "_foo88", "_foo89",
"_foo90", "_foo91", "_foo92", "_foo93", "_foo94", "_foo95", "_foo96", "_foo97",
"_foo98", "_foo99",
],
"foo",
"_foo100",
),
(
&[
"_foo", "_foo2", "_foo3", "_foo4", "_foo5", "_foo6", "_foo7", "_foo8", "_foo9",
"_foo10", "_foo11", "_foo12", "_foo13", "_foo14", "_foo15", "_foo16", "_foo17",
"_foo18", "_foo19", "_foo20", "_foo21", "_foo22", "_foo23", "_foo24", "_foo25",
"_foo26", "_foo27", "_foo28", "_foo29", "_foo30", "_foo31", "_foo32", "_foo33",
"_foo34", "_foo35", "_foo36", "_foo37", "_foo38", "_foo39", "_foo40", "_foo41",
"_foo42", "_foo43", "_foo44", "_foo45", "_foo46", "_foo47", "_foo48", "_foo49",
"_foo50", "_foo51", "_foo52", "_foo53", "_foo54", "_foo55", "_foo56", "_foo57",
"_foo58", "_foo59", "_foo60", "_foo61", "_foo62", "_foo63", "_foo64", "_foo65",
"_foo66", "_foo67", "_foo68", "_foo69", "_foo70", "_foo71", "_foo72", "_foo73",
"_foo74", "_foo75", "_foo76", "_foo77", "_foo78", "_foo79", "_foo80", "_foo81",
"_foo82", "_foo83", "_foo84", "_foo85", "_foo86", "_foo87", "_foo88", "_foo89",
"_foo90", "_foo91", "_foo92", "_foo93", "_foo94", "_foo95", "_foo96", "_foo97",
"_foo98", "_foo99", "_foo100",
],
"foo",
"_foo101",
),
];
for (used, name, expected) in cases {
let used = used.iter().map(|name| CompactStr::from(*name)).collect();
assert_eq!(get_unique_name(name, &used), expected);
}
}