//! This is mostly front-end for [`ide_db::rename`], but it also includes the
//! tests. This module also implements a couple of magic tricks, like renaming
//! `self` and to `self` (to switch between associated function and method).
+
use hir::{AsAssocItem, InFile, Semantics};
use ide_db::{
base_db::FileId,
rename::{bail, format_err, source_edit_from_references, IdentifierKind},
RootDatabase,
};
-use stdx::never;
+use itertools::Itertools;
+use stdx::{always, never};
use syntax::{ast, AstNode, SyntaxNode};
use text_edit::TextEdit;
let source_file = sema.parse(position.file_id);
let syntax = source_file.syntax();
- let def = find_definition(&sema, syntax, position)?;
- let frange = def
- .range_for_rename(&sema)
- .ok_or_else(|| format_err!("No references found at position"))?;
- Ok(RangeInfo::new(frange.range, ()))
+ let res = find_definitions(&sema, syntax, position)?
+ .map(|(name_like, def)| {
+ // ensure all ranges are valid
+
+ if def.range_for_rename(&sema).is_none() {
+ bail!("No references found at position")
+ }
+ let frange = sema.original_range(name_like.syntax());
+
+ always!(
+ frange.range.contains_inclusive(position.offset)
+ && frange.file_id == position.file_id
+ );
+ Ok(frange.range)
+ })
+ .reduce(|acc, cur| match (acc, cur) {
+ // ensure all ranges are the same
+ (Ok(acc_inner), Ok(cur_inner)) if acc_inner == cur_inner => Ok(acc_inner),
+ (Err(e), _) => Err(e),
+ _ => bail!("inconsistent text range"),
+ });
+
+ match res {
+ // ensure at least one definition was found
+ Some(res) => res.map(|range| RangeInfo::new(range, ())),
+ None => bail!("No references found at position"),
+ }
}
// Feature: Rename
new_name: &str,
) -> RenameResult<SourceChange> {
let sema = Semantics::new(db);
- rename_with_semantics(&sema, position, new_name)
-}
-
-pub(crate) fn rename_with_semantics(
- sema: &Semantics<RootDatabase>,
- position: FilePosition,
- new_name: &str,
-) -> RenameResult<SourceChange> {
let source_file = sema.parse(position.file_id);
let syntax = source_file.syntax();
- let def = find_definition(sema, syntax, position)?;
+ let defs = find_definitions(&sema, syntax, position)?;
- 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);
- }
- if new_name == "self" {
- cov_mark::hit!(rename_to_self);
- return rename_to_self(sema, local);
- }
- }
+ let ops: RenameResult<Vec<SourceChange>> = defs
+ .map(|(_namelike, 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);
+ }
+ if new_name == "self" {
+ cov_mark::hit!(rename_to_self);
+ return rename_to_self(&sema, local);
+ }
+ }
+ def.rename(&sema, new_name)
+ })
+ .collect();
- def.rename(sema, new_name)
+ ops?.into_iter()
+ .reduce(|acc, elem| acc.merge(elem))
+ .ok_or_else(|| format_err!("No references found at position"))
}
/// Called by the client when it is about to rename a file.
Some(change)
}
-fn find_definition(
+fn find_definitions(
sema: &Semantics<RootDatabase>,
syntax: &SyntaxNode,
position: FilePosition,
-) -> RenameResult<Definition> {
- match sema
- .find_node_at_offset_with_descend(syntax, position.offset)
- .ok_or_else(|| format_err!("No references found at position"))?
- {
- // renaming aliases would rename the item being aliased as the HIR doesn't track aliases yet
- ast::NameLike::Name(name)
- if name.syntax().parent().map_or(false, |it| ast::Rename::can_cast(it.kind())) =>
- {
- bail!("Renaming aliases is currently unsupported")
- }
- ast::NameLike::Name(name) => {
- NameClass::classify(sema, &name).map(|class| class.referenced_or_defined(sema.db))
- }
- ast::NameLike::NameRef(name_ref) => {
- if let Some(def) =
- NameRefClass::classify(sema, &name_ref).map(|class| class.referenced(sema.db))
- {
- // if the name differs from the definitions name it has to be an alias
- if def.name(sema.db).map_or(false, |it| it.to_string() != name_ref.text()) {
- bail!("Renaming aliases is currently unsupported");
+) -> RenameResult<impl Iterator<Item = (ast::NameLike, Definition)>> {
+ let symbols = sema
+ .find_nodes_at_offset_with_descend::<ast::NameLike>(syntax, position.offset)
+ .map(|name_like| {
+ let res = match &name_like {
+ // renaming aliases would rename the item being aliased as the HIR doesn't track aliases yet
+ ast::NameLike::Name(name)
+ if name
+ .syntax()
+ .parent()
+ .map_or(false, |it| ast::Rename::can_cast(it.kind())) =>
+ {
+ bail!("Renaming aliases is currently unsupported")
+ }
+ ast::NameLike::Name(name) => NameClass::classify(sema, name)
+ .map(|class| match class {
+ NameClass::Definition(it) | NameClass::ConstReference(it) => it,
+ NameClass::PatFieldShorthand { local_def, field_ref: _ } => {
+ Definition::Local(local_def)
+ }
+ })
+ .map(|def| (name_like.clone(), def))
+ .ok_or_else(|| format_err!("No references found at position")),
+ ast::NameLike::NameRef(name_ref) => {
+ NameRefClass::classify(sema, name_ref)
+ .map(|class| match class {
+ NameRefClass::Definition(def) => def,
+ NameRefClass::FieldShorthand { local_ref, field_ref: _ } => {
+ Definition::Local(local_ref)
+ }
+ })
+ .ok_or_else(|| format_err!("No references found at position"))
+ .and_then(|def| {
+ // if the name differs from the definitions name it has to be an alias
+ if def
+ .name(sema.db)
+ .map_or(false, |it| it.to_smol_str() != name_ref.text().as_str())
+ {
+ Err(format_err!("Renaming aliases is currently unsupported"))
+ } else {
+ Ok((name_like.clone(), def))
+ }
+ })
}
- Some(def)
+ ast::NameLike::Lifetime(lifetime) => {
+ NameRefClass::classify_lifetime(sema, lifetime)
+ .and_then(|class| match class {
+ NameRefClass::Definition(def) => Some(def),
+ _ => None,
+ })
+ .or_else(|| {
+ NameClass::classify_lifetime(sema, lifetime).and_then(|it| match it {
+ NameClass::Definition(it) => Some(it),
+ _ => None,
+ })
+ })
+ .map(|def| (name_like, def))
+ .ok_or_else(|| format_err!("No references found at position"))
+ }
+ };
+ res
+ });
+
+ let res: RenameResult<Vec<_>> = symbols.collect();
+ match res {
+ Ok(v) => {
+ if v.is_empty() {
+ // FIXME: some semantic duplication between "empty vec" and "Err()"
+ Err(format_err!("No references found at position"))
} else {
- None
+ // remove duplicates, comparing `Definition`s
+ Ok(v.into_iter().unique_by(|t| t.1))
}
}
- ast::NameLike::Lifetime(lifetime) => NameRefClass::classify_lifetime(sema, &lifetime)
- .map(|class| NameRefClass::referenced(class, sema.db))
- .or_else(|| {
- NameClass::classify_lifetime(sema, &lifetime)
- .map(|it| it.referenced_or_defined(sema.db))
- }),
+ Err(e) => Err(e),
}
- .ok_or_else(|| format_err!("No references found at position"))
}
fn rename_to_self(sema: &Semantics<RootDatabase>, local: hir::Local) -> RenameResult<SourceChange> {
_ => bail!("Cannot rename local to self outside of function"),
};
- if let Some(_) = fn_def.self_param(sema.db) {
+ if fn_def.self_param(sema.db).is_some() {
bail!("Method already has a self parameter");
}
use super::{RangeInfo, RenameError};
+ #[track_caller]
fn check(new_name: &str, ra_fixture_before: &str, ra_fixture_after: &str) {
let ra_fixture_after = &trim_indent(ra_fixture_after);
let (analysis, position) = fixture::position(ra_fixture_before);
.skip("error:".len())
.collect::<String>();
assert_eq!(error_message.trim(), err.to_string());
- return;
} else {
panic!("Rename to '{}' failed unexpectedly: {}", new_name, err)
}
fn test_prepare_rename_namelikes() {
check_prepare(r"fn name$0<'lifetime>() {}", expect![[r#"3..7: name"#]]);
check_prepare(r"fn name<'lifetime$0>() {}", expect![[r#"8..17: 'lifetime"#]]);
- check_prepare(r"fn name<'lifetime>() { name$0(); }", expect![[r#"3..7: name"#]]);
+ check_prepare(r"fn name<'lifetime>() { name$0(); }", expect![[r#"23..27: name"#]]);
}
#[test]
);
}
+ #[test]
+ fn test_rename_macro_multiple_occurrences() {
+ check(
+ "Baaah",
+ r#"macro_rules! foo {
+ ($ident:ident) => {
+ const $ident: () = ();
+ struct $ident {}
+ };
+}
+
+foo!($0Foo);
+const _: () = Foo;
+const _: Foo = Foo {};
+ "#,
+ r#"
+macro_rules! foo {
+ ($ident:ident) => {
+ const $ident: () = ();
+ struct $ident {}
+ };
+}
+
+foo!(Baaah);
+const _: () = Baaah;
+const _: Baaah = Baaah {};
+ "#,
+ )
+ }
+
#[test]
fn test_rename_for_macro_args() {
check(
#[test]
fn test_rename_struct_field() {
check(
- "j",
+ "foo",
r#"
-struct Foo { i$0: i32 }
+struct Foo { field$0: i32 }
impl Foo {
fn new(i: i32) -> Self {
- Self { i: i }
+ Self { field: i }
}
}
"#,
r#"
-struct Foo { j: i32 }
+struct Foo { foo: i32 }
impl Foo {
fn new(i: i32) -> Self {
- Self { j: i }
+ Self { foo: i }
}
}
"#,
fn test_rename_field_in_field_shorthand() {
cov_mark::check!(test_rename_field_in_field_shorthand);
check(
- "j",
+ "field",
r#"
-struct Foo { i$0: i32 }
+struct Foo { foo$0: i32 }
impl Foo {
- fn new(i: i32) -> Self {
- Self { i }
+ fn new(foo: i32) -> Self {
+ Self { foo }
}
}
"#,
r#"
-struct Foo { j: i32 }
+struct Foo { field: i32 }
impl Foo {
- fn new(i: i32) -> Self {
- Self { j: i }
+ fn new(foo: i32) -> Self {
+ Self { field: foo }
}
}
"#,
check(
"baz",
r#"
-mod $0foo { pub fn bar() {} }
+mod $0foo {
+ pub use self::bar as qux;
+ pub fn bar() {}
+}
fn main() { foo::bar(); }
"#,
r#"
-mod baz { pub fn bar() {} }
+mod baz {
+ pub use self::bar as qux;
+ pub fn bar() {}
+}
fn main() { baz::bar(); }
"#,
struct Foo { baz: i32 }
fn foo(foo: Foo) {
- let Foo { ref baz @ qux } = foo;
+ let Foo { baz: ref baz @ qux } = foo;
+ let _ = qux;
+}
+"#,
+ );
+ check(
+ "baz",
+ r#"
+struct Foo { i$0: i32 }
+
+fn foo(foo: Foo) {
+ let Foo { i: ref baz } = foo;
+ let _ = qux;
+}
+"#,
+ r#"
+struct Foo { baz: i32 }
+
+fn foo(foo: Foo) {
+ let Foo { ref baz } = foo;
+ let _ = qux;
+}
+"#,
+ );
+ }
+
+ #[test]
+ fn test_struct_local_pat_into_shorthand() {
+ cov_mark::check!(test_rename_local_put_init_shorthand_pat);
+ check(
+ "field",
+ r#"
+struct Foo { field: i32 }
+
+fn foo(foo: Foo) {
+ let Foo { field: qux$0 } = foo;
let _ = qux;
}
+"#,
+ r#"
+struct Foo { field: i32 }
+
+fn foo(foo: Foo) {
+ let Foo { field } = foo;
+ let _ = field;
+}
+"#,
+ );
+ check(
+ "field",
+ r#"
+struct Foo { field: i32 }
+
+fn foo(foo: Foo) {
+ let Foo { field: x @ qux$0 } = foo;
+ let _ = qux;
+}
+"#,
+ r#"
+struct Foo { field: i32 }
+
+fn foo(foo: Foo) {
+ let Foo { field: x @ field } = foo;
+ let _ = field;
+}
"#,
);
}
i: i32
}
-fn foo(Foo { i }: foo) -> i32 {
+fn foo(Foo { i }: Foo) -> i32 {
i$0
}
"#,
i: i32
}
-fn foo(Foo { i: bar }: foo) -> i32 {
+fn foo(Foo { i: bar }: Foo) -> i32 {
bar
}
"#,
#[test]
fn test_struct_field_complex_ident_pat() {
+ cov_mark::check!(rename_record_pat_field_name_split);
check(
"baz",
r#"
);
}
+ #[test]
+ fn test_rename_trait_method_prefix_of_second() {
+ check(
+ "qux",
+ r#"
+trait Foo {
+ fn foo$0() {}
+ fn foobar() {}
+}
+"#,
+ r#"
+trait Foo {
+ fn qux() {}
+ fn foobar() {}
+}
+"#,
+ );
+ }
+
#[test]
fn test_rename_trait_const() {
let res = r"
}
#[test]
- fn macros_are_broken_lol() {
- cov_mark::check!(macros_are_broken_lol);
+ fn defs_from_macros_arent_renamed() {
check(
"lol",
r#"
macro_rules! m { () => { fn f() {} } }
m!();
fn main() { f$0() }
+"#,
+ "error: No identifier available to rename",
+ )
+ }
+
+ #[test]
+ fn attributed_item() {
+ check(
+ "function",
+ r#"
+//- proc_macros: identity
+
+#[proc_macros::identity]
+fn func$0() {
+ func();
+}
"#,
r#"
-macro_rules! m { () => { fn f() {} } }
-lol
-fn main() { lol() }
+
+#[proc_macros::identity]
+fn function() {
+ function();
+}
+"#,
+ )
+ }
+
+ #[test]
+ fn in_macro_multi_mapping() {
+ check(
+ "a",
+ r#"
+fn foo() {
+ macro_rules! match_ast2 {
+ ($node:ident {
+ $( $res:expr, )*
+ }) => {{
+ $( if $node { $res } else )*
+ { loop {} }
+ }};
+ }
+ let $0d = 3;
+ match_ast2! {
+ d {
+ d,
+ d,
+ }
+ };
+}
+"#,
+ r#"
+fn foo() {
+ macro_rules! match_ast2 {
+ ($node:ident {
+ $( $res:expr, )*
+ }) => {{
+ $( if $node { $res } else )*
+ { loop {} }
+ }};
+ }
+ let a = 3;
+ match_ast2! {
+ a {
+ a,
+ a,
+ }
+ };
+}
"#,
)
}