Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide-db/src/syntax_helpers/suggest_name.rs')
| -rw-r--r-- | crates/ide-db/src/syntax_helpers/suggest_name.rs | 1016 |
1 files changed, 1016 insertions, 0 deletions
diff --git a/crates/ide-db/src/syntax_helpers/suggest_name.rs b/crates/ide-db/src/syntax_helpers/suggest_name.rs new file mode 100644 index 0000000000..2679cbef61 --- /dev/null +++ b/crates/ide-db/src/syntax_helpers/suggest_name.rs @@ -0,0 +1,1016 @@ +//! This module contains functions to suggest names for expressions, functions and other items + +use std::{collections::hash_map::Entry, str::FromStr}; + +use hir::Semantics; +use itertools::Itertools; +use rustc_hash::FxHashMap; +use stdx::to_lower_snake_case; +use syntax::{ + ast::{self, HasName}, + match_ast, AstNode, Edition, SmolStr, SmolStrBuilder, +}; + +use crate::RootDatabase; + +/// Trait names, that will be ignored when in `impl Trait` and `dyn Trait` +const USELESS_TRAITS: &[&str] = &["Send", "Sync", "Copy", "Clone", "Eq", "PartialEq"]; + +/// Identifier names that won't be suggested, ever +/// +/// **NOTE**: they all must be snake lower case +const USELESS_NAMES: &[&str] = + &["new", "default", "option", "some", "none", "ok", "err", "str", "string", "from", "into"]; + +const USELESS_NAME_PREFIXES: &[&str] = &["from_", "with_", "into_"]; + +/// Generic types replaced by their first argument +/// +/// # Examples +/// `Option<Name>` -> `Name` +/// `Result<User, Error>` -> `User` +const WRAPPER_TYPES: &[&str] = &["Box", "Option", "Result"]; + +/// Prefixes to strip from methods names +/// +/// # Examples +/// `vec.as_slice()` -> `slice` +/// `args.into_config()` -> `config` +/// `bytes.to_vec()` -> `vec` +const USELESS_METHOD_PREFIXES: &[&str] = &["into_", "as_", "to_"]; + +/// Useless methods that are stripped from expression +/// +/// # Examples +/// `var.name().to_string()` -> `var.name()` +const USELESS_METHODS: &[&str] = &[ + "to_string", + "as_str", + "to_owned", + "as_ref", + "clone", + "cloned", + "expect", + "expect_none", + "unwrap", + "unwrap_none", + "unwrap_or", + "unwrap_or_default", + "unwrap_or_else", + "unwrap_unchecked", + "iter", + "into_iter", + "iter_mut", + "into_future", +]; + +/// Generator for new names +/// +/// The generator keeps track of existing names and suggests new names that do +/// not conflict with existing names. +/// +/// The generator will try to resolve conflicts by adding a numeric suffix to +/// the name, e.g. `a`, `a1`, `a2`, ... +/// +/// # Examples +/// ```rust +/// let mut generator = NameGenerator::new(); +/// assert_eq!(generator.suggest_name("a"), "a"); +/// assert_eq!(generator.suggest_name("a"), "a1"); +/// +/// assert_eq!(generator.suggest_name("b2"), "b2"); +/// assert_eq!(generator.suggest_name("b"), "b3"); +/// ``` +#[derive(Debug, Default)] +pub struct NameGenerator { + pool: FxHashMap<SmolStr, usize>, +} + +impl NameGenerator { + /// Create a new empty generator + pub fn new() -> Self { + Self { pool: FxHashMap::default() } + } + + /// Create a new generator with existing names. When suggesting a name, it will + /// avoid conflicts with existing names. + pub fn new_with_names<'a>(existing_names: impl Iterator<Item = &'a str>) -> Self { + let mut generator = Self::new(); + existing_names.for_each(|name| generator.insert(name)); + generator + } + + /// Suggest a name without conflicts. If the name conflicts with existing names, + /// it will try to resolve the conflict by adding a numeric suffix. + pub fn suggest_name(&mut self, name: &str) -> SmolStr { + let (prefix, suffix) = Self::split_numeric_suffix(name); + let prefix = SmolStr::new(prefix); + let suffix = suffix.unwrap_or(0); + + match self.pool.entry(prefix.clone()) { + Entry::Vacant(entry) => { + entry.insert(suffix); + SmolStr::from_str(name).unwrap() + } + Entry::Occupied(mut entry) => { + let count = entry.get_mut(); + *count = (*count + 1).max(suffix); + + let mut new_name = SmolStrBuilder::new(); + new_name.push_str(&prefix); + new_name.push_str(count.to_string().as_str()); + new_name.finish() + } + } + } + + /// Suggest a name for given type. + /// + /// The function will strip references first, and suggest name from the inner type. + /// + /// - If `ty` is an ADT, it will suggest the name of the ADT. + /// + If `ty` is wrapped in `Box`, `Option` or `Result`, it will suggest the name from the inner type. + /// - If `ty` is a trait, it will suggest the name of the trait. + /// - If `ty` is an `impl Trait`, it will suggest the name of the first trait. + /// + /// If the suggested name conflicts with reserved keywords, it will return `None`. + pub fn for_type( + &mut self, + ty: &hir::Type, + db: &RootDatabase, + edition: Edition, + ) -> Option<SmolStr> { + let name = name_of_type(ty, db, edition)?; + Some(self.suggest_name(&name)) + } + + /// Suggest name of impl trait type + /// + /// # Current implementation + /// + /// In current implementation, the function tries to get the name from the first + /// character of the name for the first type bound. + /// + /// If the name conflicts with existing generic parameters, it will try to + /// resolve the conflict with `for_unique_generic_name`. + pub fn for_impl_trait_as_generic(&mut self, ty: &ast::ImplTraitType) -> SmolStr { + let c = ty + .type_bound_list() + .and_then(|bounds| bounds.syntax().text().char_at(0.into())) + .unwrap_or('T'); + + self.suggest_name(&c.to_string()) + } + + /// Insert a name into the pool + fn insert(&mut self, name: &str) { + let (prefix, suffix) = Self::split_numeric_suffix(name); + let prefix = SmolStr::new(prefix); + let suffix = suffix.unwrap_or(0); + + match self.pool.entry(prefix) { + Entry::Vacant(entry) => { + entry.insert(suffix); + } + Entry::Occupied(mut entry) => { + let count = entry.get_mut(); + *count = (*count).max(suffix); + } + } + } + + /// Remove the numeric suffix from the name + /// + /// # Examples + /// `a1b2c3` -> `a1b2c` + fn split_numeric_suffix(name: &str) -> (&str, Option<usize>) { + let pos = + name.rfind(|c: char| !c.is_numeric()).expect("Name cannot be empty or all-numeric"); + let (prefix, suffix) = name.split_at(pos + 1); + (prefix, suffix.parse().ok()) + } +} + +/// Suggest name of variable for given expression +/// +/// **NOTE**: it is caller's responsibility to guarantee uniqueness of the name. +/// I.e. it doesn't look for names in scope. +/// +/// # Current implementation +/// +/// In current implementation, the function tries to get the name from +/// the following sources: +/// +/// * if expr is an argument to function/method, use parameter name +/// * if expr is a function/method call, use function name +/// * expression type name if it exists (E.g. `()`, `fn() -> ()` or `!` do not have names) +/// * fallback: `var_name` +/// +/// It also applies heuristics to filter out less informative names +/// +/// Currently it sticks to the first name found. +// FIXME: Microoptimize and return a `SmolStr` here. +pub fn for_variable(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> String { + // `from_param` does not benefit from stripping + // it need the largest context possible + // so we check firstmost + if let Some(name) = from_param(expr, sema) { + return name; + } + + let mut next_expr = Some(expr.clone()); + while let Some(expr) = next_expr { + let name = + from_call(&expr).or_else(|| from_type(&expr, sema)).or_else(|| from_field_name(&expr)); + if let Some(name) = name { + return name; + } + + match expr { + ast::Expr::RefExpr(inner) => next_expr = inner.expr(), + ast::Expr::AwaitExpr(inner) => next_expr = inner.expr(), + // ast::Expr::BlockExpr(block) => expr = block.tail_expr(), + ast::Expr::CastExpr(inner) => next_expr = inner.expr(), + ast::Expr::MethodCallExpr(method) if is_useless_method(&method) => { + next_expr = method.receiver(); + } + ast::Expr::ParenExpr(inner) => next_expr = inner.expr(), + ast::Expr::TryExpr(inner) => next_expr = inner.expr(), + ast::Expr::PrefixExpr(prefix) if prefix.op_kind() == Some(ast::UnaryOp::Deref) => { + next_expr = prefix.expr() + } + _ => break, + } + } + + "var_name".to_owned() +} + +fn normalize(name: &str) -> Option<String> { + let name = to_lower_snake_case(name); + + if USELESS_NAMES.contains(&name.as_str()) { + return None; + } + + if USELESS_NAME_PREFIXES.iter().any(|prefix| name.starts_with(prefix)) { + return None; + } + + if !is_valid_name(&name) { + return None; + } + + Some(name) +} + +fn is_valid_name(name: &str) -> bool { + matches!( + super::LexedStr::single_token(syntax::Edition::CURRENT_FIXME, name), + Some((syntax::SyntaxKind::IDENT, _error)) + ) +} + +fn is_useless_method(method: &ast::MethodCallExpr) -> bool { + let ident = method.name_ref().and_then(|it| it.ident_token()); + + match ident { + Some(ident) => USELESS_METHODS.contains(&ident.text()), + None => false, + } +} + +fn from_call(expr: &ast::Expr) -> Option<String> { + from_func_call(expr).or_else(|| from_method_call(expr)) +} + +fn from_func_call(expr: &ast::Expr) -> Option<String> { + let call = match expr { + ast::Expr::CallExpr(call) => call, + _ => return None, + }; + let func = match call.expr()? { + ast::Expr::PathExpr(path) => path, + _ => return None, + }; + let ident = func.path()?.segment()?.name_ref()?.ident_token()?; + normalize(ident.text()) +} + +fn from_method_call(expr: &ast::Expr) -> Option<String> { + let method = match expr { + ast::Expr::MethodCallExpr(call) => call, + _ => return None, + }; + let ident = method.name_ref()?.ident_token()?; + let mut name = ident.text(); + + if USELESS_METHODS.contains(&name) { + return None; + } + + for prefix in USELESS_METHOD_PREFIXES { + if let Some(suffix) = name.strip_prefix(prefix) { + name = suffix; + break; + } + } + + normalize(name) +} + +fn from_param(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> Option<String> { + let arg_list = expr.syntax().parent().and_then(ast::ArgList::cast)?; + let args_parent = arg_list.syntax().parent()?; + let func = match_ast! { + match args_parent { + ast::CallExpr(call) => { + let func = call.expr()?; + let func_ty = sema.type_of_expr(&func)?.adjusted(); + func_ty.as_callable(sema.db)? + }, + ast::MethodCallExpr(method) => sema.resolve_method_call_as_callable(&method)?, + _ => return None, + } + }; + + let (idx, _) = arg_list.args().find_position(|it| it == expr).unwrap(); + let param = func.params().into_iter().nth(idx)?; + let pat = sema.source(param)?.value.right()?.pat()?; + let name = var_name_from_pat(&pat)?; + normalize(&name.to_string()) +} + +fn var_name_from_pat(pat: &ast::Pat) -> Option<ast::Name> { + match pat { + ast::Pat::IdentPat(var) => var.name(), + ast::Pat::RefPat(ref_pat) => var_name_from_pat(&ref_pat.pat()?), + ast::Pat::BoxPat(box_pat) => var_name_from_pat(&box_pat.pat()?), + _ => None, + } +} + +fn from_type(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> Option<String> { + let ty = sema.type_of_expr(expr)?.adjusted(); + let ty = ty.remove_ref().unwrap_or(ty); + let edition = sema.scope(expr.syntax())?.krate().edition(sema.db); + + name_of_type(&ty, sema.db, edition) +} + +fn name_of_type(ty: &hir::Type, db: &RootDatabase, edition: Edition) -> Option<String> { + let name = if let Some(adt) = ty.as_adt() { + let name = adt.name(db).display(db, edition).to_string(); + + if WRAPPER_TYPES.contains(&name.as_str()) { + let inner_ty = ty.type_arguments().next()?; + return name_of_type(&inner_ty, db, edition); + } + + name + } else if let Some(trait_) = ty.as_dyn_trait() { + trait_name(&trait_, db, edition)? + } else if let Some(traits) = ty.as_impl_traits(db) { + let mut iter = traits.filter_map(|t| trait_name(&t, db, edition)); + let name = iter.next()?; + if iter.next().is_some() { + return None; + } + name + } else { + return None; + }; + normalize(&name) +} + +fn trait_name(trait_: &hir::Trait, db: &RootDatabase, edition: Edition) -> Option<String> { + let name = trait_.name(db).display(db, edition).to_string(); + if USELESS_TRAITS.contains(&name.as_str()) { + return None; + } + Some(name) +} + +fn from_field_name(expr: &ast::Expr) -> Option<String> { + let field = match expr { + ast::Expr::FieldExpr(field) => field, + _ => return None, + }; + let ident = field.name_ref()?.ident_token()?; + normalize(ident.text()) +} + +#[cfg(test)] +mod tests { + use hir::FileRange; + use test_fixture::WithFixture; + + use super::*; + + #[track_caller] + fn check(ra_fixture: &str, expected: &str) { + let (db, file_id, range_or_offset) = RootDatabase::with_range_or_offset(ra_fixture); + let frange = FileRange { file_id, range: range_or_offset.into() }; + + let sema = Semantics::new(&db); + let source_file = sema.parse(frange.file_id); + let element = source_file.syntax().covering_element(frange.range); + let expr = + element.ancestors().find_map(ast::Expr::cast).expect("selection is not an expression"); + assert_eq!( + expr.syntax().text_range(), + frange.range, + "selection is not an expression(yet contained in one)" + ); + let name = for_variable(&expr, &sema); + assert_eq!(&name, expected); + } + + #[test] + fn no_args() { + check(r#"fn foo() { $0bar()$0 }"#, "bar"); + check(r#"fn foo() { $0bar.frobnicate()$0 }"#, "frobnicate"); + } + + #[test] + fn single_arg() { + check(r#"fn foo() { $0bar(1)$0 }"#, "bar"); + } + + #[test] + fn many_args() { + check(r#"fn foo() { $0bar(1, 2, 3)$0 }"#, "bar"); + } + + #[test] + fn path() { + check(r#"fn foo() { $0i32::bar(1, 2, 3)$0 }"#, "bar"); + } + + #[test] + fn generic_params() { + check(r#"fn foo() { $0bar::<i32>(1, 2, 3)$0 }"#, "bar"); + check(r#"fn foo() { $0bar.frobnicate::<i32, u32>()$0 }"#, "frobnicate"); + } + + #[test] + fn to_name() { + check( + r#" +struct Args; +struct Config; +impl Args { + fn to_config(&self) -> Config {} +} +fn foo() { + $0Args.to_config()$0; +} +"#, + "config", + ); + } + + #[test] + fn plain_func() { + check( + r#" +fn bar(n: i32, m: u32); +fn foo() { bar($01$0, 2) } +"#, + "n", + ); + } + + #[test] + fn mut_param() { + check( + r#" +fn bar(mut n: i32, m: u32); +fn foo() { bar($01$0, 2) } +"#, + "n", + ); + } + + #[test] + fn func_does_not_exist() { + check(r#"fn foo() { bar($01$0, 2) }"#, "var_name"); + } + + #[test] + fn unnamed_param() { + check( + r#" +fn bar(_: i32, m: u32); +fn foo() { bar($01$0, 2) } +"#, + "var_name", + ); + } + + #[test] + fn tuple_pat() { + check( + r#" +fn bar((n, k): (i32, i32), m: u32); +fn foo() { + bar($0(1, 2)$0, 3) +} +"#, + "var_name", + ); + } + + #[test] + fn ref_pat() { + check( + r#" +fn bar(&n: &i32, m: u32); +fn foo() { bar($0&1$0, 3) } +"#, + "n", + ); + } + + #[test] + fn box_pat() { + check( + r#" +fn bar(box n: &i32, m: u32); +fn foo() { bar($01$0, 3) } +"#, + "n", + ); + } + + #[test] + fn param_out_of_index() { + check( + r#" +fn bar(n: i32, m: u32); +fn foo() { bar(1, 2, $03$0) } +"#, + "var_name", + ); + } + + #[test] + fn generic_param_resolved() { + check( + r#" +fn bar<T>(n: T, m: u32); +fn foo() { bar($01$0, 2) } +"#, + "n", + ); + } + + #[test] + fn generic_param_unresolved() { + check( + r#" +fn bar<T>(n: T, m: u32); +fn foo<T>(x: T) { bar($0x$0, 2) } +"#, + "n", + ); + } + + #[test] + fn method() { + check( + r#" +struct S; +impl S { fn bar(&self, n: i32, m: u32); } +fn foo() { S.bar($01$0, 2) } +"#, + "n", + ); + } + + #[test] + fn method_on_impl_trait() { + check( + r#" +struct S; +trait T { + fn bar(&self, n: i32, m: u32); +} +impl T for S { fn bar(&self, n: i32, m: u32); } +fn foo() { S.bar($01$0, 2) } +"#, + "n", + ); + } + + #[test] + fn method_ufcs() { + check( + r#" +struct S; +impl S { fn bar(&self, n: i32, m: u32); } +fn foo() { S::bar(&S, $01$0, 2) } +"#, + "n", + ); + } + + #[test] + fn method_self() { + check( + r#" +struct S; +impl S { fn bar(&self, n: i32, m: u32); } +fn foo() { S::bar($0&S$0, 1, 2) } +"#, + "s", + ); + } + + #[test] + fn method_self_named() { + check( + r#" +struct S; +impl S { fn bar(strukt: &Self, n: i32, m: u32); } +fn foo() { S::bar($0&S$0, 1, 2) } +"#, + "strukt", + ); + } + + #[test] + fn i32() { + check(r#"fn foo() { let _: i32 = $01$0; }"#, "var_name"); + } + + #[test] + fn u64() { + check(r#"fn foo() { let _: u64 = $01$0; }"#, "var_name"); + } + + #[test] + fn bool() { + check(r#"fn foo() { let _: bool = $0true$0; }"#, "var_name"); + } + + #[test] + fn struct_unit() { + check( + r#" +struct Seed; +fn foo() { let _ = $0Seed$0; } +"#, + "seed", + ); + } + + #[test] + fn struct_unit_to_snake() { + check( + r#" +struct SeedState; +fn foo() { let _ = $0SeedState$0; } +"#, + "seed_state", + ); + } + + #[test] + fn struct_single_arg() { + check( + r#" +struct Seed(u32); +fn foo() { let _ = $0Seed(0)$0; } +"#, + "seed", + ); + } + + #[test] + fn struct_with_fields() { + check( + r#" +struct Seed { value: u32 } +fn foo() { let _ = $0Seed { value: 0 }$0; } +"#, + "seed", + ); + } + + #[test] + fn enum_() { + check( + r#" +enum Kind { A, B } +fn foo() { let _ = $0Kind::A$0; } +"#, + "kind", + ); + } + + #[test] + fn enum_generic_resolved() { + check( + r#" +enum Kind<T> { A { x: T }, B } +fn foo() { let _ = $0Kind::A { x:1 }$0; } +"#, + "kind", + ); + } + + #[test] + fn enum_generic_unresolved() { + check( + r#" +enum Kind<T> { A { x: T }, B } +fn foo<T>(x: T) { let _ = $0Kind::A { x }$0; } +"#, + "kind", + ); + } + + #[test] + fn dyn_trait() { + check( + r#" +trait DynHandler {} +fn bar() -> dyn DynHandler {} +fn foo() { $0(bar())$0; } +"#, + "dyn_handler", + ); + } + + #[test] + fn impl_trait() { + check( + r#" +trait StaticHandler {} +fn bar() -> impl StaticHandler {} +fn foo() { $0(bar())$0; } +"#, + "static_handler", + ); + } + + #[test] + fn impl_trait_plus_clone() { + check( + r#" +trait StaticHandler {} +trait Clone {} +fn bar() -> impl StaticHandler + Clone {} +fn foo() { $0(bar())$0; } +"#, + "static_handler", + ); + } + + #[test] + fn impl_trait_plus_lifetime() { + check( + r#" +trait StaticHandler {} +trait Clone {} +fn bar<'a>(&'a i32) -> impl StaticHandler + 'a {} +fn foo() { $0(bar(&1))$0; } +"#, + "static_handler", + ); + } + + #[test] + fn impl_trait_plus_trait() { + check( + r#" +trait Handler {} +trait StaticHandler {} +fn bar() -> impl StaticHandler + Handler {} +fn foo() { $0(bar())$0; } +"#, + "bar", + ); + } + + #[test] + fn ref_value() { + check( + r#" +struct Seed; +fn bar() -> &Seed {} +fn foo() { $0(bar())$0; } +"#, + "seed", + ); + } + + #[test] + fn box_value() { + check( + r#" +struct Box<T>(*const T); +struct Seed; +fn bar() -> Box<Seed> {} +fn foo() { $0(bar())$0; } +"#, + "seed", + ); + } + + #[test] + fn box_generic() { + check( + r#" +struct Box<T>(*const T); +fn bar<T>() -> Box<T> {} +fn foo<T>() { $0(bar::<T>())$0; } +"#, + "bar", + ); + } + + #[test] + fn option_value() { + check( + r#" +enum Option<T> { Some(T) } +struct Seed; +fn bar() -> Option<Seed> {} +fn foo() { $0(bar())$0; } +"#, + "seed", + ); + } + + #[test] + fn result_value() { + check( + r#" +enum Result<T, E> { Ok(T), Err(E) } +struct Seed; +struct Error; +fn bar() -> Result<Seed, Error> {} +fn foo() { $0(bar())$0; } +"#, + "seed", + ); + } + + #[test] + fn ref_call() { + check( + r#" +fn foo() { $0&bar(1, 3)$0 } +"#, + "bar", + ); + } + + #[test] + fn name_to_string() { + check( + r#" +fn foo() { $0function.name().to_string()$0 } +"#, + "name", + ); + } + + #[test] + fn nested_useless_method() { + check( + r#" +fn foo() { $0function.name().as_ref().unwrap().to_string()$0 } +"#, + "name", + ); + } + + #[test] + fn struct_field_name() { + check( + r#" +struct S<T> { + some_field: T; +} +fn foo<T>(some_struct: S<T>) { $0some_struct.some_field$0 } +"#, + "some_field", + ); + } + + #[test] + fn from_and_to_func() { + check( + r#" +//- minicore: from +struct Foo; +struct Bar; + +impl From<Foo> for Bar { + fn from(_: Foo) -> Self { + Bar; + } +} + +fn f(_: Bar) {} + +fn main() { + let foo = Foo {}; + f($0Bar::from(foo)$0); +} +"#, + "bar", + ); + + check( + r#" +//- minicore: from +struct Foo; +struct Bar; + +impl From<Foo> for Bar { + fn from(_: Foo) -> Self { + Bar; + } +} + +fn f(_: Bar) {} + +fn main() { + let foo = Foo {}; + f($0Into::<Bar>::into(foo)$0); +} +"#, + "bar", + ); + } + + #[test] + fn useless_name_prefix() { + check( + r#" +struct Foo; +struct Bar; + +impl Bar { + fn from_foo(_: Foo) -> Self { + Foo {} + } +} + +fn main() { + let foo = Foo {}; + let _ = $0Bar::from_foo(foo)$0; +} +"#, + "bar", + ); + + check( + r#" +struct Foo; +struct Bar; + +impl Bar { + fn with_foo(_: Foo) -> Self { + Bar {} + } +} + +fn main() { + let foo = Foo {}; + let _ = $0Bar::with_foo(foo)$0; +} +"#, + "bar", + ); + } + + #[test] + fn conflicts_with_existing_names() { + let mut generator = NameGenerator::new(); + assert_eq!(generator.suggest_name("a"), "a"); + assert_eq!(generator.suggest_name("a"), "a1"); + assert_eq!(generator.suggest_name("a"), "a2"); + assert_eq!(generator.suggest_name("a"), "a3"); + + assert_eq!(generator.suggest_name("b"), "b"); + assert_eq!(generator.suggest_name("b2"), "b2"); + assert_eq!(generator.suggest_name("b"), "b3"); + assert_eq!(generator.suggest_name("b"), "b4"); + assert_eq!(generator.suggest_name("b3"), "b5"); + + // --------- + let mut generator = NameGenerator::new_with_names(["a", "b", "b2", "c4"].into_iter()); + assert_eq!(generator.suggest_name("a"), "a1"); + assert_eq!(generator.suggest_name("a"), "a2"); + + assert_eq!(generator.suggest_name("b"), "b3"); + assert_eq!(generator.suggest_name("b2"), "b4"); + + assert_eq!(generator.suggest_name("c"), "c5"); + } +} |