Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide/src/rename.rs')
| -rw-r--r-- | crates/ide/src/rename.rs | 302 |
1 files changed, 237 insertions, 65 deletions
diff --git a/crates/ide/src/rename.rs b/crates/ide/src/rename.rs index a07c647c2c..8922a8eb48 100644 --- a/crates/ide/src/rename.rs +++ b/crates/ide/src/rename.rs @@ -13,8 +13,11 @@ use ide_db::{ }; use itertools::Itertools; use std::fmt::Write; -use stdx::{always, never}; -use syntax::{AstNode, SyntaxKind, SyntaxNode, TextRange, TextSize, ast}; +use stdx::{always, format_to, never}; +use syntax::{ + AstNode, SyntaxKind, SyntaxNode, TextRange, TextSize, + ast::{self, HasArgList, prec::ExprPrecedence}, +}; use ide_db::text_edit::TextEdit; @@ -24,6 +27,27 @@ pub use ide_db::rename::RenameError; type RenameResult<T> = Result<T, RenameError>; +/// This is similar to `collect::<Result<Vec<_>, _>>`, but unlike it, it succeeds if there is *any* `Ok` item. +fn ok_if_any<T, E>(iter: impl Iterator<Item = Result<T, E>>) -> Result<Vec<T>, E> { + let mut err = None; + let oks = iter + .filter_map(|item| match item { + Ok(it) => Some(it), + Err(it) => { + err = Some(it); + None + } + }) + .collect::<Vec<_>>(); + if !oks.is_empty() { + Ok(oks) + } else if let Some(err) = err { + Err(err) + } else { + Ok(Vec::new()) + } +} + /// Prepares a rename. The sole job of this function is to return the TextRange of the thing that is /// being targeted for a rename. pub(crate) fn prepare_rename( @@ -35,13 +59,8 @@ pub(crate) fn prepare_rename( let syntax = source_file.syntax(); let res = find_definitions(&sema, syntax, position, &Name::new_symbol_root(sym::underscore))? - .map(|(frange, kind, def, _, _)| { - // ensure all ranges are valid - - if def.range_for_rename(&sema).is_none() { - bail!("No references found at position") - } - + .filter(|(_, _, def, _, _)| def.range_for_rename(&sema).is_some()) + .map(|(frange, kind, _, _, _)| { always!( frange.range.contains_inclusive(position.offset) && frange.file_id == position.file_id @@ -97,58 +116,57 @@ pub(crate) fn rename( alias_fallback(syntax, position, &new_name.display(db, edition).to_string()); let ops: RenameResult<Vec<SourceChange>> = match alias_fallback { - Some(_) => defs - // FIXME: This can use the `ide_db::rename_reference` (or def.rename) method once we can - // properly find "direct" usages/references. - .map(|(.., def, new_name, _)| { - match kind { - IdentifierKind::Ident => (), - IdentifierKind::Lifetime => { - bail!("Cannot alias reference to a lifetime identifier") - } - IdentifierKind::Underscore => bail!("Cannot alias reference to `_`"), - IdentifierKind::LowercaseSelf => { - bail!("Cannot rename alias reference to `self`") - } - }; - let mut usages = def.usages(&sema).all(); - - // FIXME: hack - removes the usage that triggered this rename operation. - match usages.references.get_mut(&file_id).and_then(|refs| { - refs.iter() - .position(|ref_| ref_.range.contains_inclusive(position.offset)) - .map(|idx| refs.remove(idx)) - }) { - Some(_) => (), - None => never!(), - }; - - let mut source_change = SourceChange::default(); - source_change.extend(usages.references.get_mut(&file_id).iter().map(|refs| { - ( - position.file_id, - source_edit_from_references(db, refs, def, &new_name, edition), - ) - })); - - Ok(source_change) - }) - .collect(), - None => defs - .map(|(.., def, new_name, rename_def)| { - if let Definition::Local(local) = def { - if let Some(self_param) = local.as_self_param(sema.db) { - cov_mark::hit!(rename_self_to_param); - return rename_self_to_param(&sema, local, self_param, &new_name, kind); - } - if kind == IdentifierKind::LowercaseSelf { - cov_mark::hit!(rename_to_self); - return rename_to_self(&sema, local); - } + Some(_) => ok_if_any( + defs + // FIXME: This can use the `ide_db::rename_reference` (or def.rename) method once we can + // properly find "direct" usages/references. + .map(|(.., def, new_name, _)| { + match kind { + IdentifierKind::Ident => (), + IdentifierKind::Lifetime => { + bail!("Cannot alias reference to a lifetime identifier") + } + IdentifierKind::Underscore => bail!("Cannot alias reference to `_`"), + IdentifierKind::LowercaseSelf => { + bail!("Cannot rename alias reference to `self`") + } + }; + let mut usages = def.usages(&sema).all(); + + // FIXME: hack - removes the usage that triggered this rename operation. + match usages.references.get_mut(&file_id).and_then(|refs| { + refs.iter() + .position(|ref_| ref_.range.contains_inclusive(position.offset)) + .map(|idx| refs.remove(idx)) + }) { + Some(_) => (), + None => never!(), + }; + + let mut source_change = SourceChange::default(); + source_change.extend(usages.references.get_mut(&file_id).iter().map(|refs| { + ( + position.file_id, + source_edit_from_references(db, refs, def, &new_name, edition), + ) + })); + + Ok(source_change) + }), + ), + None => ok_if_any(defs.map(|(.., def, new_name, rename_def)| { + if let Definition::Local(local) = def { + if let Some(self_param) = local.as_self_param(sema.db) { + cov_mark::hit!(rename_self_to_param); + return rename_self_to_param(&sema, local, self_param, &new_name, kind); } - def.rename(&sema, new_name.as_str(), rename_def) - }) - .collect(), + if kind == IdentifierKind::LowercaseSelf { + cov_mark::hit!(rename_to_self); + return rename_to_self(&sema, local); + } + } + def.rename(&sema, new_name.as_str(), rename_def) + })), }; ops?.into_iter() @@ -322,7 +340,7 @@ fn find_definitions( }) }); - let res: RenameResult<Vec<_>> = symbols.filter_map(Result::transpose).collect(); + let res: RenameResult<Vec<_>> = ok_if_any(symbols.filter_map(Result::transpose)); match res { Ok(v) => { // remove duplicates, comparing `Definition`s @@ -336,6 +354,85 @@ fn find_definitions( } } +fn transform_assoc_fn_into_method_call( + sema: &Semantics<'_, RootDatabase>, + source_change: &mut SourceChange, + f: hir::Function, +) { + let calls = Definition::Function(f).usages(sema).all(); + for (file_id, calls) in calls { + for call in calls { + let Some(fn_name) = call.name.as_name_ref() else { continue }; + let Some(path) = fn_name.syntax().parent().and_then(ast::PathSegment::cast) else { + continue; + }; + let path = path.parent_path(); + // The `PathExpr` is the direct parent, above it is the `CallExpr`. + let Some(call) = + path.syntax().parent().and_then(|it| ast::CallExpr::cast(it.parent()?)) + else { + continue; + }; + + let Some(arg_list) = call.arg_list() else { continue }; + let mut args = arg_list.args(); + let Some(mut self_arg) = args.next() else { continue }; + let second_arg = args.next(); + + // Strip (de)references, as they will be taken automatically by auto(de)ref. + loop { + let self_ = match &self_arg { + ast::Expr::RefExpr(self_) => self_.expr(), + ast::Expr::ParenExpr(self_) => self_.expr(), + ast::Expr::PrefixExpr(self_) + if self_.op_kind() == Some(ast::UnaryOp::Deref) => + { + self_.expr() + } + _ => break, + }; + self_arg = match self_ { + Some(it) => it, + None => break, + }; + } + + let self_needs_parens = + self_arg.precedence().needs_parentheses_in(ExprPrecedence::Postfix); + + let replace_start = path.syntax().text_range().start(); + let replace_end = match second_arg { + Some(second_arg) => second_arg.syntax().text_range().start(), + None => arg_list + .r_paren_token() + .map(|it| it.text_range().start()) + .unwrap_or_else(|| arg_list.syntax().text_range().end()), + }; + let replace_range = TextRange::new(replace_start, replace_end); + + let Some(macro_mapped_self) = sema.original_range_opt(self_arg.syntax()) else { + continue; + }; + let mut replacement = String::new(); + if self_needs_parens { + replacement.push('('); + } + replacement.push_str(macro_mapped_self.text(sema.db)); + if self_needs_parens { + replacement.push(')'); + } + replacement.push('.'); + format_to!(replacement, "{fn_name}"); + replacement.push('('); + + source_change.insert_source_edit( + file_id.file_id(sema.db), + TextEdit::replace(replace_range, replacement), + ); + } + } +} + fn rename_to_self( sema: &Semantics<'_, RootDatabase>, local: hir::Local, @@ -413,6 +510,7 @@ fn rename_to_self( file_id.original_file(sema.db).file_id(sema.db), TextEdit::replace(param_source.syntax().text_range(), String::from(self_param)), ); + transform_assoc_fn_into_method_call(sema, &mut source_change, fn_def); Ok(source_change) } @@ -499,10 +597,10 @@ mod tests { ) { let ra_fixture_after = &trim_indent(ra_fixture_after); let (analysis, position) = fixture::position(ra_fixture_before); - if !ra_fixture_after.starts_with("error: ") { - if let Err(err) = analysis.prepare_rename(position).unwrap() { - panic!("Prepare rename to '{new_name}' was failed: {err}") - } + if !ra_fixture_after.starts_with("error: ") + && let Err(err) = analysis.prepare_rename(position).unwrap() + { + panic!("Prepare rename to '{new_name}' was failed: {err}") } let rename_result = analysis .rename(position, new_name) @@ -3417,4 +3515,78 @@ fn other_place() { Quux::Bar$0; } "#, ); } + + #[test] + fn rename_to_self_callers() { + check( + "self", + r#" +//- minicore: add +struct Foo; +impl core::ops::Add for Foo { + type Target = Foo; + fn add(self, _: Self) -> Foo { Foo } +} + +impl Foo { + fn foo(th$0is: &Self) {} +} + +fn bar(v: &Foo) { + Foo::foo(v); +} + +fn baz() { + Foo::foo(&Foo); + Foo::foo(Foo + Foo); +} + "#, + r#" +struct Foo; +impl core::ops::Add for Foo { + type Target = Foo; + fn add(self, _: Self) -> Foo { Foo } +} + +impl Foo { + fn foo(&self) {} +} + +fn bar(v: &Foo) { + v.foo(); +} + +fn baz() { + Foo.foo(); + (Foo + Foo).foo(); +} + "#, + ); + // Multiple arguments: + check( + "self", + r#" +struct Foo; + +impl Foo { + fn foo(th$0is: &Self, v: i32) {} +} + +fn bar(v: Foo) { + Foo::foo(&v, 123); +} + "#, + r#" +struct Foo; + +impl Foo { + fn foo(&self, v: i32) {} +} + +fn bar(v: Foo) { + v.foo(123); +} + "#, + ); + } } |