diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 12:02:58 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-17 12:02:58 +0000 |
commit | 698f8c2f01ea549d77d7dc3338a12e04c11057b9 (patch) | |
tree | 173a775858bd501c378080a10dca74132f05bc50 /src/tools/rust-analyzer/crates/ide-db/src/rename.rs | |
parent | Initial commit. (diff) | |
download | rustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.tar.xz rustc-698f8c2f01ea549d77d7dc3338a12e04c11057b9.zip |
Adding upstream version 1.64.0+dfsg1.upstream/1.64.0+dfsg1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/tools/rust-analyzer/crates/ide-db/src/rename.rs')
-rw-r--r-- | src/tools/rust-analyzer/crates/ide-db/src/rename.rs | 540 |
1 files changed, 540 insertions, 0 deletions
diff --git a/src/tools/rust-analyzer/crates/ide-db/src/rename.rs b/src/tools/rust-analyzer/crates/ide-db/src/rename.rs new file mode 100644 index 000000000..517fe3f24 --- /dev/null +++ b/src/tools/rust-analyzer/crates/ide-db/src/rename.rs @@ -0,0 +1,540 @@ +//! Rename infrastructure for rust-analyzer. It is used primarily for the +//! literal "rename" in the ide (look for tests there), but it is also available +//! as a general-purpose service. For example, it is used by the fix for the +//! "incorrect case" diagnostic. +//! +//! It leverages the [`crate::search`] functionality to find what needs to be +//! renamed. The actual renames are tricky -- field shorthands need special +//! attention, and, when renaming modules, you also want to rename files on the +//! file system. +//! +//! Another can of worms are macros: +//! +//! ```ignore +//! macro_rules! m { () => { fn f() {} } } +//! m!(); +//! fn main() { +//! f() // <- rename me +//! } +//! ``` +//! +//! The correct behavior in such cases is probably to show a dialog to the user. +//! Our current behavior is ¯\_(ツ)_/¯. +use std::fmt; + +use base_db::{AnchoredPathBuf, FileId, FileRange}; +use either::Either; +use hir::{FieldSource, HasSource, InFile, ModuleSource, Semantics}; +use stdx::never; +use syntax::{ + ast::{self, HasName}, + AstNode, SyntaxKind, TextRange, T, +}; +use text_edit::{TextEdit, TextEditBuilder}; + +use crate::{ + defs::Definition, + search::FileReference, + source_change::{FileSystemEdit, SourceChange}, + syntax_helpers::node_ext::expr_as_name_ref, + traits::convert_to_def_in_trait, + RootDatabase, +}; + +pub type Result<T, E = RenameError> = std::result::Result<T, E>; + +#[derive(Debug)] +pub struct RenameError(pub String); + +impl fmt::Display for RenameError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +#[macro_export] +macro_rules! _format_err { + ($fmt:expr) => { RenameError(format!($fmt)) }; + ($fmt:expr, $($arg:tt)+) => { RenameError(format!($fmt, $($arg)+)) } +} +pub use _format_err as format_err; + +#[macro_export] +macro_rules! _bail { + ($($tokens:tt)*) => { return Err(format_err!($($tokens)*)) } +} +pub use _bail as bail; + +impl Definition { + pub fn rename( + &self, + sema: &Semantics<'_, RootDatabase>, + new_name: &str, + ) -> Result<SourceChange> { + match *self { + Definition::Module(module) => rename_mod(sema, module, new_name), + Definition::BuiltinType(_) => { + bail!("Cannot rename builtin type") + } + Definition::SelfType(_) => bail!("Cannot rename `Self`"), + def => rename_reference(sema, def, new_name), + } + } + + /// Textual range of the identifier which will change when renaming this + /// `Definition`. Note that some definitions, like buitin types, can't be + /// renamed. + pub fn range_for_rename(self, sema: &Semantics<'_, RootDatabase>) -> Option<FileRange> { + let res = match self { + Definition::Macro(mac) => { + let src = mac.source(sema.db)?; + let name = match &src.value { + Either::Left(it) => it.name()?, + Either::Right(it) => it.name()?, + }; + src.with_value(name.syntax()).original_file_range_opt(sema.db) + } + Definition::Field(field) => { + let src = field.source(sema.db)?; + match &src.value { + FieldSource::Named(record_field) => { + let name = record_field.name()?; + src.with_value(name.syntax()).original_file_range_opt(sema.db) + } + FieldSource::Pos(_) => None, + } + } + Definition::Module(module) => { + let src = module.declaration_source(sema.db)?; + let name = src.value.name()?; + src.with_value(name.syntax()).original_file_range_opt(sema.db) + } + Definition::Function(it) => name_range(it, sema), + Definition::Adt(adt) => match adt { + hir::Adt::Struct(it) => name_range(it, sema), + hir::Adt::Union(it) => name_range(it, sema), + hir::Adt::Enum(it) => name_range(it, sema), + }, + Definition::Variant(it) => name_range(it, sema), + Definition::Const(it) => name_range(it, sema), + Definition::Static(it) => name_range(it, sema), + Definition::Trait(it) => name_range(it, sema), + Definition::TypeAlias(it) => name_range(it, sema), + Definition::Local(local) => { + let src = local.source(sema.db); + let name = match &src.value { + Either::Left(bind_pat) => bind_pat.name()?, + Either::Right(_) => return None, + }; + src.with_value(name.syntax()).original_file_range_opt(sema.db) + } + Definition::GenericParam(generic_param) => match generic_param { + hir::GenericParam::LifetimeParam(lifetime_param) => { + let src = lifetime_param.source(sema.db)?; + src.with_value(src.value.lifetime()?.syntax()).original_file_range_opt(sema.db) + } + _ => { + let x = match generic_param { + hir::GenericParam::TypeParam(it) => it.merge(), + hir::GenericParam::ConstParam(it) => it.merge(), + hir::GenericParam::LifetimeParam(_) => return None, + }; + let src = x.source(sema.db)?; + let name = match &src.value { + Either::Left(x) => x.name()?, + Either::Right(_) => return None, + }; + src.with_value(name.syntax()).original_file_range_opt(sema.db) + } + }, + Definition::Label(label) => { + let src = label.source(sema.db); + let lifetime = src.value.lifetime()?; + src.with_value(lifetime.syntax()).original_file_range_opt(sema.db) + } + Definition::BuiltinType(_) => return None, + Definition::SelfType(_) => return None, + Definition::BuiltinAttr(_) => return None, + Definition::ToolModule(_) => return None, + // FIXME: This should be doable in theory + Definition::DeriveHelper(_) => return None, + }; + return res; + + fn name_range<D>(def: D, sema: &Semantics<'_, RootDatabase>) -> Option<FileRange> + where + D: HasSource, + D::Ast: ast::HasName, + { + let src = def.source(sema.db)?; + let name = src.value.name()?; + src.with_value(name.syntax()).original_file_range_opt(sema.db) + } + } +} + +fn rename_mod( + sema: &Semantics<'_, RootDatabase>, + module: hir::Module, + new_name: &str, +) -> Result<SourceChange> { + if IdentifierKind::classify(new_name)? != IdentifierKind::Ident { + bail!("Invalid name `{0}`: cannot rename module to {0}", new_name); + } + + let mut source_change = SourceChange::default(); + + if module.is_crate_root(sema.db) { + return Ok(source_change); + } + + let InFile { file_id, value: def_source } = module.definition_source(sema.db); + if let ModuleSource::SourceFile(..) = def_source { + let anchor = file_id.original_file(sema.db); + + let is_mod_rs = module.is_mod_rs(sema.db); + let has_detached_child = module.children(sema.db).any(|child| !child.is_inline(sema.db)); + + // Module exists in a named file + if !is_mod_rs { + let path = format!("{}.rs", new_name); + let dst = AnchoredPathBuf { anchor, path }; + source_change.push_file_system_edit(FileSystemEdit::MoveFile { src: anchor, dst }) + } + + // Rename the dir if: + // - Module source is in mod.rs + // - Module has submodules defined in separate files + let dir_paths = match (is_mod_rs, has_detached_child, module.name(sema.db)) { + // Go up one level since the anchor is inside the dir we're trying to rename + (true, _, Some(mod_name)) => { + Some((format!("../{}", mod_name), format!("../{}", new_name))) + } + // The anchor is on the same level as target dir + (false, true, Some(mod_name)) => Some((mod_name.to_string(), new_name.to_string())), + _ => None, + }; + + if let Some((src, dst)) = dir_paths { + let src = AnchoredPathBuf { anchor, path: src }; + let dst = AnchoredPathBuf { anchor, path: dst }; + source_change.push_file_system_edit(FileSystemEdit::MoveDir { + src, + src_id: anchor, + dst, + }) + } + } + + if let Some(src) = module.declaration_source(sema.db) { + let file_id = src.file_id.original_file(sema.db); + match src.value.name() { + Some(name) => { + if let Some(file_range) = + src.with_value(name.syntax()).original_file_range_opt(sema.db) + { + source_change.insert_source_edit( + file_id, + TextEdit::replace(file_range.range, new_name.to_string()), + ) + }; + } + _ => never!("Module source node is missing a name"), + } + } + + let def = Definition::Module(module); + let usages = def.usages(sema).all(); + let ref_edits = usages.iter().map(|(&file_id, references)| { + (file_id, source_edit_from_references(references, def, new_name)) + }); + source_change.extend(ref_edits); + + Ok(source_change) +} + +fn rename_reference( + sema: &Semantics<'_, RootDatabase>, + def: Definition, + new_name: &str, +) -> Result<SourceChange> { + let ident_kind = IdentifierKind::classify(new_name)?; + + if matches!( + def, + Definition::GenericParam(hir::GenericParam::LifetimeParam(_)) | Definition::Label(_) + ) { + match ident_kind { + IdentifierKind::Ident | IdentifierKind::Underscore => { + cov_mark::hit!(rename_not_a_lifetime_ident_ref); + bail!("Invalid name `{}`: not a lifetime identifier", new_name); + } + IdentifierKind::Lifetime => cov_mark::hit!(rename_lifetime), + } + } else { + match ident_kind { + IdentifierKind::Lifetime => { + cov_mark::hit!(rename_not_an_ident_ref); + bail!("Invalid name `{}`: not an identifier", new_name); + } + IdentifierKind::Ident => cov_mark::hit!(rename_non_local), + IdentifierKind::Underscore => (), + } + } + + let def = convert_to_def_in_trait(sema.db, def); + let usages = def.usages(sema).all(); + + if !usages.is_empty() && ident_kind == IdentifierKind::Underscore { + cov_mark::hit!(rename_underscore_multiple); + bail!("Cannot rename reference to `_` as it is being referenced multiple times"); + } + let mut source_change = SourceChange::default(); + source_change.extend(usages.iter().map(|(&file_id, references)| { + (file_id, source_edit_from_references(references, def, new_name)) + })); + + let mut insert_def_edit = |def| { + let (file_id, edit) = source_edit_from_def(sema, def, new_name)?; + source_change.insert_source_edit(file_id, edit); + Ok(()) + }; + match def { + Definition::Local(l) => l + .associated_locals(sema.db) + .iter() + .try_for_each(|&local| insert_def_edit(Definition::Local(local))), + def => insert_def_edit(def), + }?; + Ok(source_change) +} + +pub fn source_edit_from_references( + references: &[FileReference], + def: Definition, + new_name: &str, +) -> TextEdit { + let mut edit = TextEdit::builder(); + // macros can cause multiple refs to occur for the same text range, so keep track of what we have edited so far + let mut edited_ranges = Vec::new(); + for &FileReference { range, ref name, .. } in references { + let name_range = name.syntax().text_range(); + if name_range.len() != range.len() { + // This usage comes from a different token kind that was downmapped to a NameLike in a macro + // Renaming this will most likely break things syntax-wise + continue; + } + let has_emitted_edit = match name { + // if the ranges differ then the node is inside a macro call, we can't really attempt + // to make special rewrites like shorthand syntax and such, so just rename the node in + // the macro input + ast::NameLike::NameRef(name_ref) if name_range == range => { + source_edit_from_name_ref(&mut edit, name_ref, new_name, def) + } + ast::NameLike::Name(name) if name_range == range => { + source_edit_from_name(&mut edit, name, new_name) + } + _ => false, + }; + if !has_emitted_edit { + if !edited_ranges.contains(&range.start()) { + edit.replace(range, new_name.to_string()); + edited_ranges.push(range.start()); + } + } + } + + edit.finish() +} + +fn source_edit_from_name(edit: &mut TextEditBuilder, name: &ast::Name, new_name: &str) -> bool { + if ast::RecordPatField::for_field_name(name).is_some() { + if let Some(ident_pat) = name.syntax().parent().and_then(ast::IdentPat::cast) { + cov_mark::hit!(rename_record_pat_field_name_split); + // Foo { ref mut field } -> Foo { new_name: ref mut field } + // ^ insert `new_name: ` + + // FIXME: instead of splitting the shorthand, recursively trigger a rename of the + // other name https://github.com/rust-lang/rust-analyzer/issues/6547 + edit.insert(ident_pat.syntax().text_range().start(), format!("{}: ", new_name)); + return true; + } + } + + false +} + +fn source_edit_from_name_ref( + edit: &mut TextEditBuilder, + name_ref: &ast::NameRef, + new_name: &str, + def: Definition, +) -> bool { + if name_ref.super_token().is_some() { + return true; + } + + if let Some(record_field) = ast::RecordExprField::for_name_ref(name_ref) { + let rcf_name_ref = record_field.name_ref(); + let rcf_expr = record_field.expr(); + match &(rcf_name_ref, rcf_expr.and_then(|it| expr_as_name_ref(&it))) { + // field: init-expr, check if we can use a field init shorthand + (Some(field_name), Some(init)) => { + if field_name == name_ref { + if init.text() == new_name { + cov_mark::hit!(test_rename_field_put_init_shorthand); + // Foo { field: local } -> Foo { local } + // ^^^^^^^ delete this + + // same names, we can use a shorthand here instead. + // we do not want to erase attributes hence this range start + let s = field_name.syntax().text_range().start(); + let e = init.syntax().text_range().start(); + edit.delete(TextRange::new(s, e)); + return true; + } + } else if init == name_ref { + if field_name.text() == new_name { + cov_mark::hit!(test_rename_local_put_init_shorthand); + // Foo { field: local } -> Foo { field } + // ^^^^^^^ delete this + + // same names, we can use a shorthand here instead. + // we do not want to erase attributes hence this range start + let s = field_name.syntax().text_range().end(); + let e = init.syntax().text_range().end(); + edit.delete(TextRange::new(s, e)); + return true; + } + } + } + // init shorthand + (None, Some(_)) if matches!(def, Definition::Field(_)) => { + cov_mark::hit!(test_rename_field_in_field_shorthand); + // Foo { field } -> Foo { new_name: field } + // ^ insert `new_name: ` + let offset = name_ref.syntax().text_range().start(); + edit.insert(offset, format!("{}: ", new_name)); + return true; + } + (None, Some(_)) if matches!(def, Definition::Local(_)) => { + cov_mark::hit!(test_rename_local_in_field_shorthand); + // Foo { field } -> Foo { field: new_name } + // ^ insert `: new_name` + let offset = name_ref.syntax().text_range().end(); + edit.insert(offset, format!(": {}", new_name)); + return true; + } + _ => (), + } + } else if let Some(record_field) = ast::RecordPatField::for_field_name_ref(name_ref) { + let rcf_name_ref = record_field.name_ref(); + let rcf_pat = record_field.pat(); + match (rcf_name_ref, rcf_pat) { + // field: rename + (Some(field_name), Some(ast::Pat::IdentPat(pat))) + if field_name == *name_ref && pat.at_token().is_none() => + { + // field name is being renamed + if let Some(name) = pat.name() { + if name.text() == new_name { + cov_mark::hit!(test_rename_field_put_init_shorthand_pat); + // Foo { field: ref mut local } -> Foo { ref mut field } + // ^^^^^^^ delete this + // ^^^^^ replace this with `field` + + // same names, we can use a shorthand here instead/ + // we do not want to erase attributes hence this range start + let s = field_name.syntax().text_range().start(); + let e = pat.syntax().text_range().start(); + edit.delete(TextRange::new(s, e)); + edit.replace(name.syntax().text_range(), new_name.to_string()); + return true; + } + } + } + _ => (), + } + } + false +} + +fn source_edit_from_def( + sema: &Semantics<'_, RootDatabase>, + def: Definition, + new_name: &str, +) -> Result<(FileId, TextEdit)> { + let FileRange { file_id, range } = def + .range_for_rename(sema) + .ok_or_else(|| format_err!("No identifier available to rename"))?; + + let mut edit = TextEdit::builder(); + if let Definition::Local(local) = def { + if let Either::Left(pat) = local.source(sema.db).value { + // special cases required for renaming fields/locals in Record patterns + if let Some(pat_field) = pat.syntax().parent().and_then(ast::RecordPatField::cast) { + let name_range = pat.name().unwrap().syntax().text_range(); + if let Some(name_ref) = pat_field.name_ref() { + if new_name == name_ref.text() && pat.at_token().is_none() { + // Foo { field: ref mut local } -> Foo { ref mut field } + // ^^^^^^ delete this + // ^^^^^ replace this with `field` + cov_mark::hit!(test_rename_local_put_init_shorthand_pat); + edit.delete( + name_ref + .syntax() + .text_range() + .cover_offset(pat.syntax().text_range().start()), + ); + edit.replace(name_range, name_ref.text().to_string()); + } else { + // Foo { field: ref mut local @ local 2} -> Foo { field: ref mut new_name @ local2 } + // Foo { field: ref mut local } -> Foo { field: ref mut new_name } + // ^^^^^ replace this with `new_name` + edit.replace(name_range, new_name.to_string()); + } + } else { + // Foo { ref mut field } -> Foo { field: ref mut new_name } + // ^ insert `field: ` + // ^^^^^ replace this with `new_name` + edit.insert( + pat.syntax().text_range().start(), + format!("{}: ", pat_field.field_name().unwrap()), + ); + edit.replace(name_range, new_name.to_string()); + } + } + } + } + if edit.is_empty() { + edit.replace(range, new_name.to_string()); + } + Ok((file_id, edit.finish())) +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum IdentifierKind { + Ident, + Lifetime, + Underscore, +} + +impl IdentifierKind { + pub fn classify(new_name: &str) -> Result<IdentifierKind> { + match parser::LexedStr::single_token(new_name) { + Some(res) => match res { + (SyntaxKind::IDENT, _) => Ok(IdentifierKind::Ident), + (T![_], _) => Ok(IdentifierKind::Underscore), + (SyntaxKind::LIFETIME_IDENT, _) if new_name != "'static" && new_name != "'_" => { + Ok(IdentifierKind::Lifetime) + } + (SyntaxKind::LIFETIME_IDENT, _) => { + bail!("Invalid name `{}`: not a lifetime identifier", new_name) + } + (_, Some(syntax_error)) => bail!("Invalid name `{}`: {}", new_name, syntax_error), + (_, None) => bail!("Invalid name `{}`: not an identifier", new_name), + }, + None => bail!("Invalid name `{}`: not an identifier", new_name), + } + } +} |