Skip to content

Commit a081215

Browse files
authored
Merge pull request #18791 from roife/fix-18786
fix: avoid generating colliding names in extract_variable
2 parents 259eaf9 + 9016a4c commit a081215

File tree

5 files changed

+126
-91
lines changed

5 files changed

+126
-91
lines changed

src/tools/rust-analyzer/crates/ide-assists/src/handlers/destructure_tuple_binding.rs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use ide_db::{
66
text_edit::TextRange,
77
};
88
use itertools::Itertools;
9-
use syntax::SmolStr;
109
use syntax::{
1110
ast::{self, make, AstNode, FieldExpr, HasName, IdentPat},
1211
ted,
@@ -134,17 +133,8 @@ fn collect_data(ident_pat: IdentPat, ctx: &AssistContext<'_>) -> Option<TupleDat
134133
.map(|(_, refs)| refs.to_vec())
135134
});
136135

137-
let mut name_generator = {
138-
let mut names = vec![];
139-
if let Some(scope) = ctx.sema.scope(ident_pat.syntax()) {
140-
scope.process_all_names(&mut |name, scope| {
141-
if let hir::ScopeDef::Local(_) = scope {
142-
names.push(name.as_str().into())
143-
}
144-
})
145-
}
146-
suggest_name::NameGenerator::new_with_names(names.iter().map(|s: &SmolStr| s.as_str()))
147-
};
136+
let mut name_generator =
137+
suggest_name::NameGenerator::new_from_scope_locals(ctx.sema.scope(ident_pat.syntax()));
148138

149139
let field_names = field_types
150140
.into_iter()

src/tools/rust-analyzer/crates/ide-assists/src/handlers/extract_variable.rs

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -336,10 +336,12 @@ impl ExtractionKind {
336336
}
337337
}
338338

339+
let mut name_generator =
340+
suggest_name::NameGenerator::new_from_scope_locals(ctx.sema.scope(to_extract.syntax()));
339341
let var_name = if let Some(literal_name) = get_literal_name(ctx, to_extract) {
340-
literal_name
342+
name_generator.suggest_name(&literal_name)
341343
} else {
342-
suggest_name::for_variable(to_extract, &ctx.sema)
344+
name_generator.for_variable(to_extract, &ctx.sema)
343345
};
344346

345347
let var_name = match self {
@@ -352,10 +354,10 @@ impl ExtractionKind {
352354
}
353355

354356
fn get_literal_name(ctx: &AssistContext<'_>, expr: &ast::Expr) -> Option<String> {
355-
let literal = match expr {
356-
ast::Expr::Literal(literal) => literal,
357-
_ => return None,
357+
let ast::Expr::Literal(literal) = expr else {
358+
return None;
358359
};
360+
359361
let inner = match literal.kind() {
360362
ast::LiteralKind::String(string) => string.value().ok()?.into_owned(),
361363
ast::LiteralKind::ByteString(byte_string) => {
@@ -2632,4 +2634,33 @@ fn foo() {
26322634
"Extract into static",
26332635
);
26342636
}
2637+
2638+
#[test]
2639+
fn extract_variable_name_conflicts() {
2640+
check_assist_by_label(
2641+
extract_variable,
2642+
r#"
2643+
struct S { x: i32 };
2644+
2645+
fn main() {
2646+
let s = 2;
2647+
let t = $0S { x: 1 }$0;
2648+
let t2 = t;
2649+
let x = s;
2650+
}
2651+
"#,
2652+
r#"
2653+
struct S { x: i32 };
2654+
2655+
fn main() {
2656+
let s = 2;
2657+
let $0s1 = S { x: 1 };
2658+
let t = s1;
2659+
let t2 = t;
2660+
let x = s;
2661+
}
2662+
"#,
2663+
"Extract into variable",
2664+
);
2665+
}
26352666
}

src/tools/rust-analyzer/crates/ide-assists/src/handlers/replace_is_method_with_if_let_method.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::{AssistContext, AssistId, AssistKind, Assists};
2020
// ```
2121
// fn main() {
2222
// let x = Some(1);
23-
// if let Some(${0:x}) = x {}
23+
// if let Some(${0:x1}) = x {}
2424
// }
2525
// ```
2626
pub(crate) fn replace_is_method_with_if_let_method(
@@ -40,10 +40,13 @@ pub(crate) fn replace_is_method_with_if_let_method(
4040
"is_some" | "is_ok" => {
4141
let receiver = call_expr.receiver()?;
4242

43+
let mut name_generator = suggest_name::NameGenerator::new_from_scope_locals(
44+
ctx.sema.scope(if_expr.syntax()),
45+
);
4346
let var_name = if let ast::Expr::PathExpr(path_expr) = receiver.clone() {
44-
path_expr.path()?.to_string()
47+
name_generator.suggest_name(&path_expr.path()?.to_string())
4548
} else {
46-
suggest_name::for_variable(&receiver, &ctx.sema)
49+
name_generator.for_variable(&receiver, &ctx.sema)
4750
};
4851

4952
let (assist_id, message, text) = if name_ref.text() == "is_some" {
@@ -98,7 +101,7 @@ fn main() {
98101
r#"
99102
fn main() {
100103
let x = Some(1);
101-
if let Some(${0:x}) = x {}
104+
if let Some(${0:x1}) = x {}
102105
}
103106
"#,
104107
);
@@ -150,7 +153,7 @@ fn main() {
150153
r#"
151154
fn main() {
152155
let x = Ok(1);
153-
if let Ok(${0:x}) = x {}
156+
if let Ok(${0:x1}) = x {}
154157
}
155158
"#,
156159
);

src/tools/rust-analyzer/crates/ide-assists/src/tests/generated.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2885,7 +2885,7 @@ fn main() {
28852885
r#####"
28862886
fn main() {
28872887
let x = Some(1);
2888-
if let Some(${0:x}) = x {}
2888+
if let Some(${0:x1}) = x {}
28892889
}
28902890
"#####,
28912891
)

src/tools/rust-analyzer/crates/ide-db/src/syntax_helpers/suggest_name.rs

Lines changed: 79 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
33
use std::{collections::hash_map::Entry, str::FromStr};
44

5-
use hir::Semantics;
5+
use hir::{Semantics, SemanticsScope};
66
use itertools::Itertools;
77
use rustc_hash::FxHashMap;
88
use stdx::to_lower_snake_case;
99
use syntax::{
1010
ast::{self, HasName},
11-
match_ast, AstNode, Edition, SmolStr, SmolStrBuilder,
11+
match_ast, AstNode, Edition, SmolStr, SmolStrBuilder, ToSmolStr,
1212
};
1313

1414
use crate::RootDatabase;
@@ -100,6 +100,19 @@ impl NameGenerator {
100100
generator
101101
}
102102

103+
pub fn new_from_scope_locals(scope: Option<SemanticsScope<'_>>) -> Self {
104+
let mut generator = Self::new();
105+
if let Some(scope) = scope {
106+
scope.process_all_names(&mut |name, scope| {
107+
if let hir::ScopeDef::Local(_) = scope {
108+
generator.insert(name.as_str());
109+
}
110+
});
111+
}
112+
113+
generator
114+
}
115+
103116
/// Suggest a name without conflicts. If the name conflicts with existing names,
104117
/// it will try to resolve the conflict by adding a numeric suffix.
105118
pub fn suggest_name(&mut self, name: &str) -> SmolStr {
@@ -162,6 +175,59 @@ impl NameGenerator {
162175
self.suggest_name(&c.to_string())
163176
}
164177

178+
/// Suggest name of variable for given expression
179+
///
180+
/// In current implementation, the function tries to get the name from
181+
/// the following sources:
182+
///
183+
/// * if expr is an argument to function/method, use parameter name
184+
/// * if expr is a function/method call, use function name
185+
/// * expression type name if it exists (E.g. `()`, `fn() -> ()` or `!` do not have names)
186+
/// * fallback: `var_name`
187+
///
188+
/// It also applies heuristics to filter out less informative names
189+
///
190+
/// Currently it sticks to the first name found.
191+
pub fn for_variable(
192+
&mut self,
193+
expr: &ast::Expr,
194+
sema: &Semantics<'_, RootDatabase>,
195+
) -> SmolStr {
196+
// `from_param` does not benefit from stripping it need the largest
197+
// context possible so we check firstmost
198+
if let Some(name) = from_param(expr, sema) {
199+
return self.suggest_name(&name);
200+
}
201+
202+
let mut next_expr = Some(expr.clone());
203+
while let Some(expr) = next_expr {
204+
let name = from_call(&expr)
205+
.or_else(|| from_type(&expr, sema))
206+
.or_else(|| from_field_name(&expr));
207+
if let Some(name) = name {
208+
return self.suggest_name(&name);
209+
}
210+
211+
match expr {
212+
ast::Expr::RefExpr(inner) => next_expr = inner.expr(),
213+
ast::Expr::AwaitExpr(inner) => next_expr = inner.expr(),
214+
// ast::Expr::BlockExpr(block) => expr = block.tail_expr(),
215+
ast::Expr::CastExpr(inner) => next_expr = inner.expr(),
216+
ast::Expr::MethodCallExpr(method) if is_useless_method(&method) => {
217+
next_expr = method.receiver();
218+
}
219+
ast::Expr::ParenExpr(inner) => next_expr = inner.expr(),
220+
ast::Expr::TryExpr(inner) => next_expr = inner.expr(),
221+
ast::Expr::PrefixExpr(prefix) if prefix.op_kind() == Some(ast::UnaryOp::Deref) => {
222+
next_expr = prefix.expr()
223+
}
224+
_ => break,
225+
}
226+
}
227+
228+
self.suggest_name("var_name")
229+
}
230+
165231
/// Insert a name into the pool
166232
fn insert(&mut self, name: &str) {
167233
let (prefix, suffix) = Self::split_numeric_suffix(name);
@@ -191,63 +257,8 @@ impl NameGenerator {
191257
}
192258
}
193259

194-
/// Suggest name of variable for given expression
195-
///
196-
/// **NOTE**: it is caller's responsibility to guarantee uniqueness of the name.
197-
/// I.e. it doesn't look for names in scope.
198-
///
199-
/// # Current implementation
200-
///
201-
/// In current implementation, the function tries to get the name from
202-
/// the following sources:
203-
///
204-
/// * if expr is an argument to function/method, use parameter name
205-
/// * if expr is a function/method call, use function name
206-
/// * expression type name if it exists (E.g. `()`, `fn() -> ()` or `!` do not have names)
207-
/// * fallback: `var_name`
208-
///
209-
/// It also applies heuristics to filter out less informative names
210-
///
211-
/// Currently it sticks to the first name found.
212-
// FIXME: Microoptimize and return a `SmolStr` here.
213-
pub fn for_variable(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> String {
214-
// `from_param` does not benefit from stripping
215-
// it need the largest context possible
216-
// so we check firstmost
217-
if let Some(name) = from_param(expr, sema) {
218-
return name;
219-
}
220-
221-
let mut next_expr = Some(expr.clone());
222-
while let Some(expr) = next_expr {
223-
let name =
224-
from_call(&expr).or_else(|| from_type(&expr, sema)).or_else(|| from_field_name(&expr));
225-
if let Some(name) = name {
226-
return name;
227-
}
228-
229-
match expr {
230-
ast::Expr::RefExpr(inner) => next_expr = inner.expr(),
231-
ast::Expr::AwaitExpr(inner) => next_expr = inner.expr(),
232-
// ast::Expr::BlockExpr(block) => expr = block.tail_expr(),
233-
ast::Expr::CastExpr(inner) => next_expr = inner.expr(),
234-
ast::Expr::MethodCallExpr(method) if is_useless_method(&method) => {
235-
next_expr = method.receiver();
236-
}
237-
ast::Expr::ParenExpr(inner) => next_expr = inner.expr(),
238-
ast::Expr::TryExpr(inner) => next_expr = inner.expr(),
239-
ast::Expr::PrefixExpr(prefix) if prefix.op_kind() == Some(ast::UnaryOp::Deref) => {
240-
next_expr = prefix.expr()
241-
}
242-
_ => break,
243-
}
244-
}
245-
246-
"var_name".to_owned()
247-
}
248-
249-
fn normalize(name: &str) -> Option<String> {
250-
let name = to_lower_snake_case(name);
260+
fn normalize(name: &str) -> Option<SmolStr> {
261+
let name = to_lower_snake_case(name).to_smolstr();
251262

252263
if USELESS_NAMES.contains(&name.as_str()) {
253264
return None;
@@ -280,11 +291,11 @@ fn is_useless_method(method: &ast::MethodCallExpr) -> bool {
280291
}
281292
}
282293

283-
fn from_call(expr: &ast::Expr) -> Option<String> {
294+
fn from_call(expr: &ast::Expr) -> Option<SmolStr> {
284295
from_func_call(expr).or_else(|| from_method_call(expr))
285296
}
286297

287-
fn from_func_call(expr: &ast::Expr) -> Option<String> {
298+
fn from_func_call(expr: &ast::Expr) -> Option<SmolStr> {
288299
let call = match expr {
289300
ast::Expr::CallExpr(call) => call,
290301
_ => return None,
@@ -297,7 +308,7 @@ fn from_func_call(expr: &ast::Expr) -> Option<String> {
297308
normalize(ident.text())
298309
}
299310

300-
fn from_method_call(expr: &ast::Expr) -> Option<String> {
311+
fn from_method_call(expr: &ast::Expr) -> Option<SmolStr> {
301312
let method = match expr {
302313
ast::Expr::MethodCallExpr(call) => call,
303314
_ => return None,
@@ -319,7 +330,7 @@ fn from_method_call(expr: &ast::Expr) -> Option<String> {
319330
normalize(name)
320331
}
321332

322-
fn from_param(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> Option<String> {
333+
fn from_param(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> Option<SmolStr> {
323334
let arg_list = expr.syntax().parent().and_then(ast::ArgList::cast)?;
324335
let args_parent = arg_list.syntax().parent()?;
325336
let func = match_ast! {
@@ -338,7 +349,7 @@ fn from_param(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> Option<St
338349
let param = func.params().into_iter().nth(idx)?;
339350
let pat = sema.source(param)?.value.right()?.pat()?;
340351
let name = var_name_from_pat(&pat)?;
341-
normalize(&name.to_string())
352+
normalize(&name.to_smolstr())
342353
}
343354

344355
fn var_name_from_pat(pat: &ast::Pat) -> Option<ast::Name> {
@@ -350,15 +361,15 @@ fn var_name_from_pat(pat: &ast::Pat) -> Option<ast::Name> {
350361
}
351362
}
352363

353-
fn from_type(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> Option<String> {
364+
fn from_type(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> Option<SmolStr> {
354365
let ty = sema.type_of_expr(expr)?.adjusted();
355366
let ty = ty.remove_ref().unwrap_or(ty);
356367
let edition = sema.scope(expr.syntax())?.krate().edition(sema.db);
357368

358369
name_of_type(&ty, sema.db, edition)
359370
}
360371

361-
fn name_of_type(ty: &hir::Type, db: &RootDatabase, edition: Edition) -> Option<String> {
372+
fn name_of_type(ty: &hir::Type, db: &RootDatabase, edition: Edition) -> Option<SmolStr> {
362373
let name = if let Some(adt) = ty.as_adt() {
363374
let name = adt.name(db).display(db, edition).to_string();
364375

@@ -393,7 +404,7 @@ fn trait_name(trait_: &hir::Trait, db: &RootDatabase, edition: Edition) -> Optio
393404
Some(name)
394405
}
395406

396-
fn from_field_name(expr: &ast::Expr) -> Option<String> {
407+
fn from_field_name(expr: &ast::Expr) -> Option<SmolStr> {
397408
let field = match expr {
398409
ast::Expr::FieldExpr(field) => field,
399410
_ => return None,
@@ -424,7 +435,7 @@ mod tests {
424435
frange.range,
425436
"selection is not an expression(yet contained in one)"
426437
);
427-
let name = for_variable(&expr, &sema);
438+
let name = NameGenerator::new().for_variable(&expr, &sema);
428439
assert_eq!(&name, expected);
429440
}
430441

0 commit comments

Comments
 (0)