|
1 | 1 | //! This module contains functions to suggest names for expressions, functions and other items
|
2 | 2 |
|
| 3 | +use std::{collections::hash_map::Entry, str::FromStr}; |
| 4 | + |
3 | 5 | use hir::Semantics;
|
4 | 6 | use itertools::Itertools;
|
5 |
| -use rustc_hash::FxHashSet; |
| 7 | +use rustc_hash::FxHashMap; |
6 | 8 | use stdx::to_lower_snake_case;
|
7 | 9 | use syntax::{
|
8 | 10 | ast::{self, HasName},
|
9 |
| - match_ast, AstNode, Edition, SmolStr, |
| 11 | + match_ast, AstNode, Edition, SmolStr, SmolStrBuilder, |
10 | 12 | };
|
11 | 13 |
|
12 | 14 | use crate::RootDatabase;
|
@@ -62,71 +64,131 @@ const USELESS_METHODS: &[&str] = &[
|
62 | 64 | "into_future",
|
63 | 65 | ];
|
64 | 66 |
|
65 |
| -/// Suggest a name for given type. |
| 67 | +/// Generator for new names |
66 | 68 | ///
|
67 |
| -/// The function will strip references first, and suggest name from the inner type. |
| 69 | +/// The generator keeps track of existing names and suggests new names that do |
| 70 | +/// not conflict with existing names. |
68 | 71 | ///
|
69 |
| -/// - If `ty` is an ADT, it will suggest the name of the ADT. |
70 |
| -/// + If `ty` is wrapped in `Box`, `Option` or `Result`, it will suggest the name from the inner type. |
71 |
| -/// - If `ty` is a trait, it will suggest the name of the trait. |
72 |
| -/// - If `ty` is an `impl Trait`, it will suggest the name of the first trait. |
| 72 | +/// The generator will try to resolve conflicts by adding a numeric suffix to |
| 73 | +/// the name, e.g. `a`, `a1`, `a2`, ... |
73 | 74 | ///
|
74 |
| -/// If the suggested name conflicts with reserved keywords, it will return `None`. |
75 |
| -pub fn for_type(ty: &hir::Type, db: &RootDatabase, edition: Edition) -> Option<String> { |
76 |
| - let ty = ty.strip_references(); |
77 |
| - name_of_type(&ty, db, edition) |
78 |
| -} |
79 |
| - |
80 |
| -/// Suggest a unique name for generic parameter. |
81 |
| -/// |
82 |
| -/// `existing_params` is used to check if the name conflicts with existing |
83 |
| -/// generic parameters. |
| 75 | +/// # Examples |
| 76 | +/// ```rust |
| 77 | +/// let mut generator = NameGenerator::new(); |
| 78 | +/// assert_eq!(generator.suggest_name("a"), "a"); |
| 79 | +/// assert_eq!(generator.suggest_name("a"), "a1"); |
84 | 80 | ///
|
85 |
| -/// The function checks if the name conflicts with existing generic parameters. |
86 |
| -/// If so, it will try to resolve the conflict by adding a number suffix, e.g. |
87 |
| -/// `T`, `T0`, `T1`, ... |
88 |
| -pub fn for_unique_generic_name(name: &str, existing_params: &ast::GenericParamList) -> SmolStr { |
89 |
| - let param_names = existing_params |
90 |
| - .generic_params() |
91 |
| - .map(|param| match param { |
92 |
| - ast::GenericParam::TypeParam(t) => t.name().unwrap().to_string(), |
93 |
| - p => p.to_string(), |
94 |
| - }) |
95 |
| - .collect::<FxHashSet<_>>(); |
96 |
| - let mut name = name.to_owned(); |
97 |
| - let base_len = name.len(); |
98 |
| - let mut count = 0; |
99 |
| - while param_names.contains(&name) { |
100 |
| - name.truncate(base_len); |
101 |
| - name.push_str(&count.to_string()); |
102 |
| - count += 1; |
103 |
| - } |
104 |
| - |
105 |
| - name.into() |
| 81 | +/// assert_eq!(generator.suggest_name("b2"), "b2"); |
| 82 | +/// assert_eq!(generator.suggest_name("b"), "b3"); |
| 83 | +/// ``` |
| 84 | +#[derive(Debug, Default)] |
| 85 | +pub struct NameGenerator { |
| 86 | + pool: FxHashMap<SmolStr, usize>, |
106 | 87 | }
|
107 | 88 |
|
108 |
| -/// Suggest name of impl trait type |
109 |
| -/// |
110 |
| -/// `existing_params` is used to check if the name conflicts with existing |
111 |
| -/// generic parameters. |
112 |
| -/// |
113 |
| -/// # Current implementation |
114 |
| -/// |
115 |
| -/// In current implementation, the function tries to get the name from the first |
116 |
| -/// character of the name for the first type bound. |
117 |
| -/// |
118 |
| -/// If the name conflicts with existing generic parameters, it will try to |
119 |
| -/// resolve the conflict with `for_unique_generic_name`. |
120 |
| -pub fn for_impl_trait_as_generic( |
121 |
| - ty: &ast::ImplTraitType, |
122 |
| - existing_params: &ast::GenericParamList, |
123 |
| -) -> SmolStr { |
124 |
| - let c = ty |
125 |
| - .type_bound_list() |
126 |
| - .and_then(|bounds| bounds.syntax().text().char_at(0.into())) |
127 |
| - .unwrap_or('T'); |
128 |
| - |
129 |
| - for_unique_generic_name(c.encode_utf8(&mut [0; 4]), existing_params) |
| 89 | +impl NameGenerator { |
| 90 | + /// Create a new empty generator |
| 91 | + pub fn new() -> Self { |
| 92 | + Self { pool: FxHashMap::default() } |
| 93 | + } |
| 94 | + |
| 95 | + /// Create a new generator with existing names. When suggesting a name, it will |
| 96 | + /// avoid conflicts with existing names. |
| 97 | + pub fn new_with_names<'a>(existing_names: impl Iterator<Item = &'a str>) -> Self { |
| 98 | + let mut generator = Self::new(); |
| 99 | + existing_names.for_each(|name| generator.insert(name)); |
| 100 | + generator |
| 101 | + } |
| 102 | + |
| 103 | + /// Suggest a name without conflicts. If the name conflicts with existing names, |
| 104 | + /// it will try to resolve the conflict by adding a numeric suffix. |
| 105 | + pub fn suggest_name(&mut self, name: &str) -> SmolStr { |
| 106 | + let (prefix, suffix) = Self::split_numeric_suffix(name); |
| 107 | + let prefix = SmolStr::new(prefix); |
| 108 | + let suffix = suffix.unwrap_or(0); |
| 109 | + |
| 110 | + match self.pool.entry(prefix.clone()) { |
| 111 | + Entry::Vacant(entry) => { |
| 112 | + entry.insert(suffix); |
| 113 | + SmolStr::from_str(name).unwrap() |
| 114 | + } |
| 115 | + Entry::Occupied(mut entry) => { |
| 116 | + let count = entry.get_mut(); |
| 117 | + *count = (*count + 1).max(suffix); |
| 118 | + |
| 119 | + let mut new_name = SmolStrBuilder::new(); |
| 120 | + new_name.push_str(&prefix); |
| 121 | + new_name.push_str(count.to_string().as_str()); |
| 122 | + new_name.finish() |
| 123 | + } |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + /// Suggest a name for given type. |
| 128 | + /// |
| 129 | + /// The function will strip references first, and suggest name from the inner type. |
| 130 | + /// |
| 131 | + /// - If `ty` is an ADT, it will suggest the name of the ADT. |
| 132 | + /// + If `ty` is wrapped in `Box`, `Option` or `Result`, it will suggest the name from the inner type. |
| 133 | + /// - If `ty` is a trait, it will suggest the name of the trait. |
| 134 | + /// - If `ty` is an `impl Trait`, it will suggest the name of the first trait. |
| 135 | + /// |
| 136 | + /// If the suggested name conflicts with reserved keywords, it will return `None`. |
| 137 | + pub fn for_type( |
| 138 | + &mut self, |
| 139 | + ty: &hir::Type, |
| 140 | + db: &RootDatabase, |
| 141 | + edition: Edition, |
| 142 | + ) -> Option<SmolStr> { |
| 143 | + let name = name_of_type(ty, db, edition)?; |
| 144 | + Some(self.suggest_name(&name)) |
| 145 | + } |
| 146 | + |
| 147 | + /// Suggest name of impl trait type |
| 148 | + /// |
| 149 | + /// # Current implementation |
| 150 | + /// |
| 151 | + /// In current implementation, the function tries to get the name from the first |
| 152 | + /// character of the name for the first type bound. |
| 153 | + /// |
| 154 | + /// If the name conflicts with existing generic parameters, it will try to |
| 155 | + /// resolve the conflict with `for_unique_generic_name`. |
| 156 | + pub fn for_impl_trait_as_generic(&mut self, ty: &ast::ImplTraitType) -> SmolStr { |
| 157 | + let c = ty |
| 158 | + .type_bound_list() |
| 159 | + .and_then(|bounds| bounds.syntax().text().char_at(0.into())) |
| 160 | + .unwrap_or('T'); |
| 161 | + |
| 162 | + self.suggest_name(&c.to_string()) |
| 163 | + } |
| 164 | + |
| 165 | + /// Insert a name into the pool |
| 166 | + fn insert(&mut self, name: &str) { |
| 167 | + let (prefix, suffix) = Self::split_numeric_suffix(name); |
| 168 | + let prefix = SmolStr::new(prefix); |
| 169 | + let suffix = suffix.unwrap_or(0); |
| 170 | + |
| 171 | + match self.pool.entry(prefix) { |
| 172 | + Entry::Vacant(entry) => { |
| 173 | + entry.insert(suffix); |
| 174 | + } |
| 175 | + Entry::Occupied(mut entry) => { |
| 176 | + let count = entry.get_mut(); |
| 177 | + *count = (*count).max(suffix); |
| 178 | + } |
| 179 | + } |
| 180 | + } |
| 181 | + |
| 182 | + /// Remove the numeric suffix from the name |
| 183 | + /// |
| 184 | + /// # Examples |
| 185 | + /// `a1b2c3` -> `a1b2c` |
| 186 | + fn split_numeric_suffix(name: &str) -> (&str, Option<usize>) { |
| 187 | + let pos = |
| 188 | + name.rfind(|c: char| !c.is_numeric()).expect("Name cannot be empty or all-numeric"); |
| 189 | + let (prefix, suffix) = name.split_at(pos + 1); |
| 190 | + (prefix, suffix.parse().ok()) |
| 191 | + } |
130 | 192 | }
|
131 | 193 |
|
132 | 194 | /// Suggest name of variable for given expression
|
@@ -290,9 +352,10 @@ fn var_name_from_pat(pat: &ast::Pat) -> Option<ast::Name> {
|
290 | 352 |
|
291 | 353 | fn from_type(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> Option<String> {
|
292 | 354 | let ty = sema.type_of_expr(expr)?.adjusted();
|
| 355 | + let ty = ty.remove_ref().unwrap_or(ty); |
293 | 356 | let edition = sema.scope(expr.syntax())?.krate().edition(sema.db);
|
294 | 357 |
|
295 |
| - for_type(&ty, sema.db, edition) |
| 358 | + name_of_type(&ty, sema.db, edition) |
296 | 359 | }
|
297 | 360 |
|
298 | 361 | fn name_of_type(ty: &hir::Type, db: &RootDatabase, edition: Edition) -> Option<String> {
|
@@ -925,4 +988,29 @@ fn main() {
|
925 | 988 | "bar",
|
926 | 989 | );
|
927 | 990 | }
|
| 991 | + |
| 992 | + #[test] |
| 993 | + fn conflicts_with_existing_names() { |
| 994 | + let mut generator = NameGenerator::new(); |
| 995 | + assert_eq!(generator.suggest_name("a"), "a"); |
| 996 | + assert_eq!(generator.suggest_name("a"), "a1"); |
| 997 | + assert_eq!(generator.suggest_name("a"), "a2"); |
| 998 | + assert_eq!(generator.suggest_name("a"), "a3"); |
| 999 | + |
| 1000 | + assert_eq!(generator.suggest_name("b"), "b"); |
| 1001 | + assert_eq!(generator.suggest_name("b2"), "b2"); |
| 1002 | + assert_eq!(generator.suggest_name("b"), "b3"); |
| 1003 | + assert_eq!(generator.suggest_name("b"), "b4"); |
| 1004 | + assert_eq!(generator.suggest_name("b3"), "b5"); |
| 1005 | + |
| 1006 | + // --------- |
| 1007 | + let mut generator = NameGenerator::new_with_names(["a", "b", "b2", "c4"].into_iter()); |
| 1008 | + assert_eq!(generator.suggest_name("a"), "a1"); |
| 1009 | + assert_eq!(generator.suggest_name("a"), "a2"); |
| 1010 | + |
| 1011 | + assert_eq!(generator.suggest_name("b"), "b3"); |
| 1012 | + assert_eq!(generator.suggest_name("b2"), "b4"); |
| 1013 | + |
| 1014 | + assert_eq!(generator.suggest_name("c"), "c5"); |
| 1015 | + } |
928 | 1016 | }
|
0 commit comments