mirror of
https://github.com/rust-lang/rust-analyzer.git
synced 2025-10-28 10:39:45 +00:00
fix: avoid generating colliding names in extract_variable
This commit is contained in:
parent
59bc7b49d0
commit
f8c04166dc
5 changed files with 126 additions and 91 deletions
|
|
@ -6,7 +6,6 @@ use ide_db::{
|
||||||
text_edit::TextRange,
|
text_edit::TextRange,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use syntax::SmolStr;
|
|
||||||
use syntax::{
|
use syntax::{
|
||||||
ast::{self, make, AstNode, FieldExpr, HasName, IdentPat},
|
ast::{self, make, AstNode, FieldExpr, HasName, IdentPat},
|
||||||
ted,
|
ted,
|
||||||
|
|
@ -134,17 +133,8 @@ fn collect_data(ident_pat: IdentPat, ctx: &AssistContext<'_>) -> Option<TupleDat
|
||||||
.map(|(_, refs)| refs.to_vec())
|
.map(|(_, refs)| refs.to_vec())
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut name_generator = {
|
let mut name_generator =
|
||||||
let mut names = vec![];
|
suggest_name::NameGenerator::new_from_scope_locals(ctx.sema.scope(ident_pat.syntax()));
|
||||||
if let Some(scope) = ctx.sema.scope(ident_pat.syntax()) {
|
|
||||||
scope.process_all_names(&mut |name, scope| {
|
|
||||||
if let hir::ScopeDef::Local(_) = scope {
|
|
||||||
names.push(name.as_str().into())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
suggest_name::NameGenerator::new_with_names(names.iter().map(|s: &SmolStr| s.as_str()))
|
|
||||||
};
|
|
||||||
|
|
||||||
let field_names = field_types
|
let field_names = field_types
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
|
||||||
|
|
@ -336,10 +336,12 @@ impl ExtractionKind {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut name_generator =
|
||||||
|
suggest_name::NameGenerator::new_from_scope_locals(ctx.sema.scope(to_extract.syntax()));
|
||||||
let var_name = if let Some(literal_name) = get_literal_name(ctx, to_extract) {
|
let var_name = if let Some(literal_name) = get_literal_name(ctx, to_extract) {
|
||||||
literal_name
|
name_generator.suggest_name(&literal_name)
|
||||||
} else {
|
} else {
|
||||||
suggest_name::for_variable(to_extract, &ctx.sema)
|
name_generator.for_variable(to_extract, &ctx.sema)
|
||||||
};
|
};
|
||||||
|
|
||||||
let var_name = match self {
|
let var_name = match self {
|
||||||
|
|
@ -352,10 +354,10 @@ impl ExtractionKind {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_literal_name(ctx: &AssistContext<'_>, expr: &ast::Expr) -> Option<String> {
|
fn get_literal_name(ctx: &AssistContext<'_>, expr: &ast::Expr) -> Option<String> {
|
||||||
let literal = match expr {
|
let ast::Expr::Literal(literal) = expr else {
|
||||||
ast::Expr::Literal(literal) => literal,
|
return None;
|
||||||
_ => return None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let inner = match literal.kind() {
|
let inner = match literal.kind() {
|
||||||
ast::LiteralKind::String(string) => string.value().ok()?.into_owned(),
|
ast::LiteralKind::String(string) => string.value().ok()?.into_owned(),
|
||||||
ast::LiteralKind::ByteString(byte_string) => {
|
ast::LiteralKind::ByteString(byte_string) => {
|
||||||
|
|
@ -2632,4 +2634,33 @@ fn foo() {
|
||||||
"Extract into static",
|
"Extract into static",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_variable_name_conflicts() {
|
||||||
|
check_assist_by_label(
|
||||||
|
extract_variable,
|
||||||
|
r#"
|
||||||
|
struct S { x: i32 };
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let s = 2;
|
||||||
|
let t = $0S { x: 1 }$0;
|
||||||
|
let t2 = t;
|
||||||
|
let x = s;
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
r#"
|
||||||
|
struct S { x: i32 };
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let s = 2;
|
||||||
|
let $0s1 = S { x: 1 };
|
||||||
|
let t = s1;
|
||||||
|
let t2 = t;
|
||||||
|
let x = s;
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
"Extract into variable",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ use crate::{AssistContext, AssistId, AssistKind, Assists};
|
||||||
// ```
|
// ```
|
||||||
// fn main() {
|
// fn main() {
|
||||||
// let x = Some(1);
|
// let x = Some(1);
|
||||||
// if let Some(${0:x}) = x {}
|
// if let Some(${0:x1}) = x {}
|
||||||
// }
|
// }
|
||||||
// ```
|
// ```
|
||||||
pub(crate) fn replace_is_method_with_if_let_method(
|
pub(crate) fn replace_is_method_with_if_let_method(
|
||||||
|
|
@ -40,10 +40,13 @@ pub(crate) fn replace_is_method_with_if_let_method(
|
||||||
"is_some" | "is_ok" => {
|
"is_some" | "is_ok" => {
|
||||||
let receiver = call_expr.receiver()?;
|
let receiver = call_expr.receiver()?;
|
||||||
|
|
||||||
|
let mut name_generator = suggest_name::NameGenerator::new_from_scope_locals(
|
||||||
|
ctx.sema.scope(if_expr.syntax()),
|
||||||
|
);
|
||||||
let var_name = if let ast::Expr::PathExpr(path_expr) = receiver.clone() {
|
let var_name = if let ast::Expr::PathExpr(path_expr) = receiver.clone() {
|
||||||
path_expr.path()?.to_string()
|
name_generator.suggest_name(&path_expr.path()?.to_string())
|
||||||
} else {
|
} else {
|
||||||
suggest_name::for_variable(&receiver, &ctx.sema)
|
name_generator.for_variable(&receiver, &ctx.sema)
|
||||||
};
|
};
|
||||||
|
|
||||||
let (assist_id, message, text) = if name_ref.text() == "is_some" {
|
let (assist_id, message, text) = if name_ref.text() == "is_some" {
|
||||||
|
|
@ -98,7 +101,7 @@ fn main() {
|
||||||
r#"
|
r#"
|
||||||
fn main() {
|
fn main() {
|
||||||
let x = Some(1);
|
let x = Some(1);
|
||||||
if let Some(${0:x}) = x {}
|
if let Some(${0:x1}) = x {}
|
||||||
}
|
}
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
|
@ -150,7 +153,7 @@ fn main() {
|
||||||
r#"
|
r#"
|
||||||
fn main() {
|
fn main() {
|
||||||
let x = Ok(1);
|
let x = Ok(1);
|
||||||
if let Ok(${0:x}) = x {}
|
if let Ok(${0:x1}) = x {}
|
||||||
}
|
}
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2885,7 +2885,7 @@ fn main() {
|
||||||
r#####"
|
r#####"
|
||||||
fn main() {
|
fn main() {
|
||||||
let x = Some(1);
|
let x = Some(1);
|
||||||
if let Some(${0:x}) = x {}
|
if let Some(${0:x1}) = x {}
|
||||||
}
|
}
|
||||||
"#####,
|
"#####,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
|
|
||||||
use std::{collections::hash_map::Entry, str::FromStr};
|
use std::{collections::hash_map::Entry, str::FromStr};
|
||||||
|
|
||||||
use hir::Semantics;
|
use hir::{Semantics, SemanticsScope};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use stdx::to_lower_snake_case;
|
use stdx::to_lower_snake_case;
|
||||||
use syntax::{
|
use syntax::{
|
||||||
ast::{self, HasName},
|
ast::{self, HasName},
|
||||||
match_ast, AstNode, Edition, SmolStr, SmolStrBuilder,
|
match_ast, AstNode, Edition, SmolStr, SmolStrBuilder, ToSmolStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::RootDatabase;
|
use crate::RootDatabase;
|
||||||
|
|
@ -100,6 +100,19 @@ impl NameGenerator {
|
||||||
generator
|
generator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_from_scope_locals(scope: Option<SemanticsScope<'_>>) -> Self {
|
||||||
|
let mut generator = Self::new();
|
||||||
|
if let Some(scope) = scope {
|
||||||
|
scope.process_all_names(&mut |name, scope| {
|
||||||
|
if let hir::ScopeDef::Local(_) = scope {
|
||||||
|
generator.insert(name.as_str());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
generator
|
||||||
|
}
|
||||||
|
|
||||||
/// Suggest a name without conflicts. If the name conflicts with existing names,
|
/// Suggest a name without conflicts. If the name conflicts with existing names,
|
||||||
/// it will try to resolve the conflict by adding a numeric suffix.
|
/// it will try to resolve the conflict by adding a numeric suffix.
|
||||||
pub fn suggest_name(&mut self, name: &str) -> SmolStr {
|
pub fn suggest_name(&mut self, name: &str) -> SmolStr {
|
||||||
|
|
@ -162,6 +175,59 @@ impl NameGenerator {
|
||||||
self.suggest_name(&c.to_string())
|
self.suggest_name(&c.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Suggest name of variable for given expression
|
||||||
|
///
|
||||||
|
/// 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.
|
||||||
|
pub fn for_variable(
|
||||||
|
&mut self,
|
||||||
|
expr: &ast::Expr,
|
||||||
|
sema: &Semantics<'_, RootDatabase>,
|
||||||
|
) -> SmolStr {
|
||||||
|
// `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 self.suggest_name(&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 self.suggest_name(&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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.suggest_name("var_name")
|
||||||
|
}
|
||||||
|
|
||||||
/// Insert a name into the pool
|
/// Insert a name into the pool
|
||||||
fn insert(&mut self, name: &str) {
|
fn insert(&mut self, name: &str) {
|
||||||
let (prefix, suffix) = Self::split_numeric_suffix(name);
|
let (prefix, suffix) = Self::split_numeric_suffix(name);
|
||||||
|
|
@ -191,63 +257,8 @@ impl NameGenerator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Suggest name of variable for given expression
|
fn normalize(name: &str) -> Option<SmolStr> {
|
||||||
///
|
let name = to_lower_snake_case(name).to_smolstr();
|
||||||
/// **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()) {
|
if USELESS_NAMES.contains(&name.as_str()) {
|
||||||
return None;
|
return None;
|
||||||
|
|
@ -280,11 +291,11 @@ fn is_useless_method(method: &ast::MethodCallExpr) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_call(expr: &ast::Expr) -> Option<String> {
|
fn from_call(expr: &ast::Expr) -> Option<SmolStr> {
|
||||||
from_func_call(expr).or_else(|| from_method_call(expr))
|
from_func_call(expr).or_else(|| from_method_call(expr))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_func_call(expr: &ast::Expr) -> Option<String> {
|
fn from_func_call(expr: &ast::Expr) -> Option<SmolStr> {
|
||||||
let call = match expr {
|
let call = match expr {
|
||||||
ast::Expr::CallExpr(call) => call,
|
ast::Expr::CallExpr(call) => call,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
|
|
@ -297,7 +308,7 @@ fn from_func_call(expr: &ast::Expr) -> Option<String> {
|
||||||
normalize(ident.text())
|
normalize(ident.text())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_method_call(expr: &ast::Expr) -> Option<String> {
|
fn from_method_call(expr: &ast::Expr) -> Option<SmolStr> {
|
||||||
let method = match expr {
|
let method = match expr {
|
||||||
ast::Expr::MethodCallExpr(call) => call,
|
ast::Expr::MethodCallExpr(call) => call,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
|
|
@ -319,7 +330,7 @@ fn from_method_call(expr: &ast::Expr) -> Option<String> {
|
||||||
normalize(name)
|
normalize(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_param(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> Option<String> {
|
fn from_param(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> Option<SmolStr> {
|
||||||
let arg_list = expr.syntax().parent().and_then(ast::ArgList::cast)?;
|
let arg_list = expr.syntax().parent().and_then(ast::ArgList::cast)?;
|
||||||
let args_parent = arg_list.syntax().parent()?;
|
let args_parent = arg_list.syntax().parent()?;
|
||||||
let func = match_ast! {
|
let func = match_ast! {
|
||||||
|
|
@ -338,7 +349,7 @@ fn from_param(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> Option<St
|
||||||
let param = func.params().into_iter().nth(idx)?;
|
let param = func.params().into_iter().nth(idx)?;
|
||||||
let pat = sema.source(param)?.value.right()?.pat()?;
|
let pat = sema.source(param)?.value.right()?.pat()?;
|
||||||
let name = var_name_from_pat(&pat)?;
|
let name = var_name_from_pat(&pat)?;
|
||||||
normalize(&name.to_string())
|
normalize(&name.to_smolstr())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn var_name_from_pat(pat: &ast::Pat) -> Option<ast::Name> {
|
fn var_name_from_pat(pat: &ast::Pat) -> Option<ast::Name> {
|
||||||
|
|
@ -350,7 +361,7 @@ fn var_name_from_pat(pat: &ast::Pat) -> Option<ast::Name> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_type(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> Option<String> {
|
fn from_type(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> Option<SmolStr> {
|
||||||
let ty = sema.type_of_expr(expr)?.adjusted();
|
let ty = sema.type_of_expr(expr)?.adjusted();
|
||||||
let ty = ty.remove_ref().unwrap_or(ty);
|
let ty = ty.remove_ref().unwrap_or(ty);
|
||||||
let edition = sema.scope(expr.syntax())?.krate().edition(sema.db);
|
let edition = sema.scope(expr.syntax())?.krate().edition(sema.db);
|
||||||
|
|
@ -358,7 +369,7 @@ fn from_type(expr: &ast::Expr, sema: &Semantics<'_, RootDatabase>) -> Option<Str
|
||||||
name_of_type(&ty, sema.db, edition)
|
name_of_type(&ty, sema.db, edition)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn name_of_type(ty: &hir::Type, db: &RootDatabase, edition: Edition) -> Option<String> {
|
fn name_of_type(ty: &hir::Type, db: &RootDatabase, edition: Edition) -> Option<SmolStr> {
|
||||||
let name = if let Some(adt) = ty.as_adt() {
|
let name = if let Some(adt) = ty.as_adt() {
|
||||||
let name = adt.name(db).display(db, edition).to_string();
|
let name = adt.name(db).display(db, edition).to_string();
|
||||||
|
|
||||||
|
|
@ -393,7 +404,7 @@ fn trait_name(trait_: &hir::Trait, db: &RootDatabase, edition: Edition) -> Optio
|
||||||
Some(name)
|
Some(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_field_name(expr: &ast::Expr) -> Option<String> {
|
fn from_field_name(expr: &ast::Expr) -> Option<SmolStr> {
|
||||||
let field = match expr {
|
let field = match expr {
|
||||||
ast::Expr::FieldExpr(field) => field,
|
ast::Expr::FieldExpr(field) => field,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
|
|
@ -424,7 +435,7 @@ mod tests {
|
||||||
frange.range,
|
frange.range,
|
||||||
"selection is not an expression(yet contained in one)"
|
"selection is not an expression(yet contained in one)"
|
||||||
);
|
);
|
||||||
let name = for_variable(&expr, &sema);
|
let name = NameGenerator::new().for_variable(&expr, &sema);
|
||||||
assert_eq!(&name, expected);
|
assert_eq!(&name, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue