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.rs302
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);
+}
+ "#,
+ );
+ }
}