diff --git a/crates/hir/src/semantics.rs b/crates/hir/src/semantics.rs index caa6700de9f9..ef570ff93254 100644 --- a/crates/hir/src/semantics.rs +++ b/crates/hir/src/semantics.rs @@ -222,6 +222,20 @@ impl Semantics<'_, DB> { self.imp.descend_node_at_offset(node, offset).filter_map(|mut it| it.find_map(N::cast)) } + /// Find an AstNode by offset inside SyntaxNode, if it is inside + /// an attribute macro call, descend it and find again. Do not + /// care if the found name doesn't match the original name. + // FIXME: Rethink this API + pub fn find_nodes_at_offset_with_descend_any_name<'slf, N: AstNode + 'slf>( + &'slf self, + node: &SyntaxNode, + offset: TextSize, + ) -> impl Iterator + 'slf { + self.imp + .descend_node_at_offset_any_name(node, offset) + .filter_map(|mut it| it.find_map(N::cast)) + } + pub fn resolve_range_pat(&self, range_pat: &ast::RangePat) -> Option { self.imp.resolve_range_pat(range_pat).map(Struct::from) } @@ -972,6 +986,29 @@ impl<'db> SemanticsImpl<'db> { r } + /// Descends the token into expansions, returning the tokens that matches the input + /// token's [`SyntaxKind`]. + pub fn descend_into_macros_exact_any_name( + &self, + token: SyntaxToken, + ) -> SmallVec<[SyntaxToken; 1]> { + let mut r = smallvec![]; + let kind = token.kind(); + + self.descend_into_macros_cb(token.clone(), |InFile { value, file_id: _ }, ctx| { + let mapped_kind = value.kind(); + let any_ident_match = || kind.is_any_identifier() && value.kind().is_any_identifier(); + let matches = (kind == mapped_kind || any_ident_match()) && !ctx.is_opaque(self.db); + if matches { + r.push(value); + } + }); + if r.is_empty() { + r.push(token); + } + r + } + /// Descends the token into expansions, returning the first token that matches the input /// token's [`SyntaxKind`] and text. pub fn descend_into_macros_single_exact(&self, token: SyntaxToken) -> SyntaxToken { @@ -1252,6 +1289,29 @@ impl<'db> SemanticsImpl<'db> { }) } + // Note this return type is deliberate as [`find_nodes_at_offset_with_descend`] wants to stop + // traversing the inner iterator when it finds a node. + // The outer iterator is over the tokens descendants + // The inner iterator is the ancestors of a descendant + fn descend_node_at_offset_any_name( + &self, + node: &SyntaxNode, + offset: TextSize, + ) -> impl Iterator + '_> + '_ { + node.token_at_offset(offset) + .map(move |token| self.descend_into_macros_exact_any_name(token)) + .map(|descendants| { + descendants.into_iter().map(move |it| self.token_ancestors_with_macros(it)) + }) + // re-order the tokens from token_at_offset by returning the ancestors with the smaller first nodes first + // See algo::ancestors_at_offset, which uses the same approach + .kmerge_by(|left, right| { + left.clone() + .map(|node| node.text_range().len()) + .lt(right.clone().map(|node| node.text_range().len())) + }) + } + /// Attempts to map the node out of macro expanded files returning the original file range. /// If upmapping is not possible, this will fall back to the range of the macro call of the /// macro file the node resides in. diff --git a/crates/ide-assists/src/handlers/remove_underscore.rs b/crates/ide-assists/src/handlers/remove_underscore.rs index 912e1936b593..a8e27416d5ce 100644 --- a/crates/ide-assists/src/handlers/remove_underscore.rs +++ b/crates/ide-assists/src/handlers/remove_underscore.rs @@ -1,6 +1,7 @@ use ide_db::{ assists::AssistId, defs::{Definition, NameClass, NameRefClass}, + rename::RenameDefinition, }; use syntax::{AstNode, ast}; @@ -61,7 +62,7 @@ pub(crate) fn remove_underscore(acc: &mut Assists, ctx: &AssistContext<'_>) -> O "Remove underscore from a used variable", text_range, |builder| { - let changes = def.rename(&ctx.sema, new_name).unwrap(); + let changes = def.rename(&ctx.sema, new_name, RenameDefinition::Yes).unwrap(); builder.source_change = changes; }, ) diff --git a/crates/ide-db/src/rename.rs b/crates/ide-db/src/rename.rs index fa2a46a0f7c2..8a2133ec387c 100644 --- a/crates/ide-db/src/rename.rs +++ b/crates/ide-db/src/rename.rs @@ -75,6 +75,7 @@ impl Definition { &self, sema: &Semantics<'_, RootDatabase>, new_name: &str, + rename_definition: RenameDefinition, ) -> Result { // We append `r#` if needed. let new_name = new_name.trim_start_matches("r#"); @@ -103,8 +104,10 @@ impl Definition { bail!("Cannot rename a builtin attr.") } Definition::SelfType(_) => bail!("Cannot rename `Self`"), - Definition::Macro(mac) => rename_reference(sema, Definition::Macro(mac), new_name), - def => rename_reference(sema, def, new_name), + Definition::Macro(mac) => { + rename_reference(sema, Definition::Macro(mac), new_name, RenameDefinition::Yes) + } + def => rename_reference(sema, def, new_name, rename_definition), } } @@ -328,6 +331,7 @@ fn rename_reference( sema: &Semantics<'_, RootDatabase>, def: Definition, new_name: &str, + rename_definition: RenameDefinition, ) -> Result { let ident_kind = IdentifierKind::classify(new_name)?; @@ -369,11 +373,20 @@ fn rename_reference( // This needs to come after the references edits, because we change the annotation of existing edits // if a conflict is detected. - let (file_id, edit) = source_edit_from_def(sema, def, new_name, &mut source_change)?; - source_change.insert_source_edit(file_id, edit); + if rename_definition == RenameDefinition::Yes { + let (file_id, edit) = source_edit_from_def(sema, def, new_name, &mut source_change)?; + + source_change.insert_source_edit(file_id, edit); + } Ok(source_change) } +#[derive(Copy, Clone, PartialEq)] +pub enum RenameDefinition { + Yes, + No, +} + pub fn source_edit_from_references( references: &[FileReference], def: Definition, diff --git a/crates/ide-diagnostics/src/handlers/incorrect_case.rs b/crates/ide-diagnostics/src/handlers/incorrect_case.rs index 38f10c778d69..519ff192799d 100644 --- a/crates/ide-diagnostics/src/handlers/incorrect_case.rs +++ b/crates/ide-diagnostics/src/handlers/incorrect_case.rs @@ -1,5 +1,5 @@ use hir::{CaseType, InFile, db::ExpandDatabase}; -use ide_db::{assists::Assist, defs::NameClass}; +use ide_db::{assists::Assist, defs::NameClass, rename::RenameDefinition}; use syntax::AstNode; use crate::{ @@ -44,7 +44,7 @@ fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::IncorrectCase) -> Option> = match alias_fallback { Some(_) => defs + .definitions + .into_iter() // FIXME: This can use the `ide_db::rename_reference` (or def.rename) method once we can // properly find "direct" usages/references. .map(|(.., def)| { @@ -127,21 +134,43 @@ pub(crate) fn rename( Ok(source_change) }) .collect(), - None => defs - .map(|(.., def)| { + None => { + fn one( + sema: &Semantics<'_, RootDatabase>, + def: Definition, + new_name: &str, + rename_definition: RenameDefinition, + ) -> Result { 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); + 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); + return rename_to_self(sema, local); } } - def.rename(&sema, new_name) - }) - .collect(), + def.rename(sema, new_name, rename_definition) + } + + let FoundDefinitions { original_name, definitions, related } = defs; + + let direct_renames = definitions + .into_iter() + .map(|(.., def)| one(&sema, def, new_name, RenameDefinition::Yes)); + + let related_renames = related.into_iter().map(|(related_name, (.., def))| { + let new_name = match &original_name { + Some(original_name) => &related_name.replace(original_name, new_name), + None => new_name, + }; + + one(&sema, def, &new_name, RenameDefinition::No) + }); + + direct_renames.chain(related_renames).collect() + } }; ops?.into_iter() @@ -159,7 +188,7 @@ pub(crate) fn will_rename_file( let sema = Semantics::new(db); let module = sema.file_to_module_def(file_id)?; let def = Definition::Module(module); - let mut change = def.rename(&sema, new_name_stem).ok()?; + let mut change = def.rename(&sema, new_name_stem, RenameDefinition::Yes).ok()?; change.file_system_edits.clear(); Some(change) } @@ -196,113 +225,143 @@ fn alias_fallback( Some(builder.finish()) } +#[derive(Debug)] +struct FoundDefinitions { + original_name: Option, + definitions: Vec, + related: Vec<(String, FoundDefinition)>, +} + +// #[derive(Debug)] +// struct FoundDefinition(FileRange, SyntaxKind, Definition); +type FoundDefinition = (FileRange, SyntaxKind, Definition); + fn find_definitions( sema: &Semantics<'_, RootDatabase>, syntax: &SyntaxNode, FilePosition { file_id, offset }: FilePosition, -) -> RenameResult> { +) -> RenameResult { let token = syntax.token_at_offset(offset).find(|t| matches!(t.kind(), SyntaxKind::STRING)); if let Some((range, Some(resolution))) = token.and_then(|token| sema.check_for_format_args_template(token, offset)) { - return Ok(vec![( - FileRange { file_id, range }, - SyntaxKind::STRING, - Definition::from(resolution), - )] - .into_iter()); - } - - let symbols = - sema.find_nodes_at_offset_with_descend::(syntax, offset).map(|name_like| { - let kind = name_like.syntax().kind(); - let range = sema - .original_range_opt(name_like.syntax()) - .ok_or_else(|| format_err!("No references found at position"))?; - 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().is_some_and(|it| ast::Rename::can_cast(it.kind())) - // FIXME: uncomment this once we resolve to usages to extern crate declarations - // && name - // .syntax() - // .ancestors() - // .nth(2) - // .map_or(true, |it| !ast::ExternCrate::can_cast(it.kind())) - => - { - bail!("Renaming aliases is currently unsupported") - } - ast::NameLike::Name(name) => NameClass::classify(sema, name) + return Ok(FoundDefinitions { + original_name: None, + definitions: vec![( + FileRange { file_id, range }, + SyntaxKind::STRING, + Definition::from(resolution), + )], + related: vec![], + }); + } + + // JPG: What happens if we trigger a rename on *not* the defining usage? + let token = syntax.token_at_offset(offset).find(|t| matches!(t.kind(), SyntaxKind::IDENT)); + let original_name = token.map(|t| t.text().to_owned()); + + let mut definitions = HashMap::new(); + let mut related = HashMap::new(); + + for name_like in + sema.find_nodes_at_offset_with_descend_any_name::(syntax, offset) + { + let kind = name_like.syntax().kind(); + let range = sema + .original_range_opt(name_like.syntax()) + .ok_or_else(|| format_err!("No references found at position"))?; + + 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().is_some_and(|it| ast::Rename::can_cast(it.kind())) + // FIXME: uncomment this once we resolve to usages to extern crate declarations + // && name + // .syntax() + // .ancestors() + // .nth(2) + // .map_or(true, |it| !ast::ExternCrate::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: _, adt_subst: _ } => { + Definition::Local(local_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 { - NameClass::Definition(it) | NameClass::ConstReference(it) => it, - NameClass::PatFieldShorthand { local_def, field_ref: _, adt_subst: _ } => { - Definition::Local(local_def) + NameRefClass::Definition(def, _) => def, + NameRefClass::FieldShorthand { local_ref, field_ref: _, adt_subst: _ } => { + Definition::Local(local_ref) + } + NameRefClass::ExternCrateShorthand { decl, .. } => { + Definition::ExternCrateDecl(decl) } }) - .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: _, adt_subst: _ } => { - Definition::Local(local_ref) - } - NameRefClass::ExternCrateShorthand { decl, .. } => { - Definition::ExternCrateDecl(decl) - } - }) - // FIXME: uncomment this once we resolve to usages to extern crate declarations - .filter(|def| !matches!(def, Definition::ExternCrateDecl(..))) - .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).is_some_and(|it| it.as_str() != name_ref.text().trim_start_matches("r#")) - { - Err(format_err!("Renaming aliases is currently unsupported")) - } else { - Ok(def) - } - }) - } - ast::NameLike::Lifetime(lifetime) => { - NameRefClass::classify_lifetime(sema, lifetime) - .and_then(|class| match class { - NameRefClass::Definition(def, _) => Some(def), + // FIXME: uncomment this once we resolve to usages to extern crate declarations + .filter(|def| !matches!(def, Definition::ExternCrateDecl(..))) + .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).is_some_and(|it| it.as_str() != name_ref.text().trim_start_matches("r#")) + { + Err(format_err!("Renaming aliases is currently unsupported")) + } else { + Ok(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, }) - .or_else(|| { - NameClass::classify_lifetime(sema, lifetime).and_then(|it| match it { - NameClass::Definition(it) => Some(it), - _ => None, - }) - }) - .ok_or_else(|| format_err!("No references found at position")) - } - }; - res.map(|def| (range, kind, def)) - }); - - let res: RenameResult> = 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 { - // remove duplicates, comparing `Definition`s - Ok(v.into_iter() - .unique_by(|&(.., def)| def) - .map(|(a, b, c)| (a.into_file_id(sema.db), b, c)) - .collect::>() - .into_iter()) + }) + .ok_or_else(|| format_err!("No references found at position")) } + }; + + let range = range.into_file_id(sema.db); + let def = (range, kind, res?); + + let related_name = name_like.text(); + let related_name = related_name.as_str(); + + let name_matches = + original_name.as_deref().is_some_and(|original_name| original_name == related_name); + + // remove duplicates, comparing `Definition`s + if name_matches { + definitions.insert(def.2, def); + } else { + related.insert(def.2, (related_name.to_owned(), def)); } - Err(e) => Err(e), + } + + if definitions.is_empty() { + // FIXME: some semantic duplication between "empty vec" and "Err()" + Err(format_err!("No references found at position")) + } else { + let definitions = definitions.into_values().collect(); + let related = related.into_values().collect(); + + Ok(FoundDefinitions { original_name, definitions, related }) } } @@ -3259,6 +3318,109 @@ trait Trait { trait Trait { fn foo() -> impl use Trait {} } +"#, + ); + } + + #[test] + fn rename_macro_generated_type_from_type_with_a_suffix() { + check( + "Bar", + r#" +//- proc_macros: generate_suffixed_type +#[proc_macros::generate_suffixed_type] +struct Foo$0; + +fn usage(_: FooSuffix) {} +usage(FooSuffix); +"#, + r#" +#[proc_macros::generate_suffixed_type] +struct Bar; + +fn usage(_: BarSuffix) {} +usage(BarSuffix); +"#, + ); + } + + #[test] + fn rename_macro_generated_type_from_type_usage_with_a_suffix() { + check( + "Bar", + r#" +//- proc_macros: generate_suffixed_type +#[proc_macros::generate_suffixed_type] +struct Foo; + +fn usage(_: FooSuffix) {} +usage(FooSuffix); + +fn other_place() { Foo$0; } +"#, + r#" +#[proc_macros::generate_suffixed_type] +struct Bar; + +fn usage(_: BarSuffix) {} +usage(BarSuffix); + +fn other_place() { Bar; } +"#, + ); + } + + #[test] + fn rename_macro_generated_type_from_variant_with_a_suffix() { + check( + "Bar", + r#" +//- proc_macros: generate_suffixed_type +#[proc_macros::generate_suffixed_type] +enum Quux { + Foo$0, +} + +fn usage(_: FooSuffix) {} +usage(FooSuffix); +"#, + r#" +#[proc_macros::generate_suffixed_type] +enum Quux { + Bar, +} + +fn usage(_: BarSuffix) {} +usage(BarSuffix); +"#, + ); + } + + + #[test] + fn rename_macro_generated_type_from_variant_usage_with_a_suffix() { + check( + "Bar", + r#" +//- proc_macros: generate_suffixed_type +#[proc_macros::generate_suffixed_type] +enum Quux { + Foo, +} + +fn usage(_: FooSuffix) {} +usage(FooSuffix); + +fn other_place() { Quux::Foo$0; } +"#, + r#" +#[proc_macros::generate_suffixed_type] +enum Quux { + Bar, +} + +fn usage(_: BarSuffix) {} +usage(BartSuffix); "#, ); } diff --git a/crates/test-fixture/src/lib.rs b/crates/test-fixture/src/lib.rs index 96e1301f227e..9f4499fc9ee9 100644 --- a/crates/test-fixture/src/lib.rs +++ b/crates/test-fixture/src/lib.rs @@ -538,6 +538,21 @@ pub fn disallow_cfg(_attr: TokenStream, input: TokenStream) -> TokenStream { disabled: false, }, ), + ( + r#" +#[proc_macro_attribute] +pub fn generate_suffixed_type(_attr: TokenStream, input: TokenStream) -> TokenStream { + input +} +"# + .into(), + ProcMacro { + name: Symbol::intern("generate_suffixed_type"), + kind: ProcMacroKind::Attr, + expander: sync::Arc::new(GenerateSuffixedTypeProcMacroExpander), + disabled: false, + }, + ), ]) } @@ -919,3 +934,60 @@ impl ProcMacroExpander for DisallowCfgProcMacroExpander { Ok(subtree.clone()) } } + +// Generates a new type by adding a suffix to the original name +#[derive(Debug)] +struct GenerateSuffixedTypeProcMacroExpander; +impl ProcMacroExpander for GenerateSuffixedTypeProcMacroExpander { + fn expand( + &self, + subtree: &TopSubtree, + _attrs: Option<&TopSubtree>, + _env: &Env, + _def_site: Span, + call_site: Span, + _mixed_site: Span, + _current_dir: String, + ) -> Result { + dbg!(subtree); + + let TokenTree::Leaf(Leaf::Ident(ident)) = &subtree.0[1] else { + return Err(ProcMacroExpansionError::Panic("incorrect Input".into())); + }; + + let ident = match ident.sym.as_str() { + "struct" => { + let TokenTree::Leaf(Leaf::Ident(ident)) = &subtree.0[2] else { + return Err(ProcMacroExpansionError::Panic("incorrect Input".into())); + }; + ident + } + + "enum" => { + let TokenTree::Leaf(Leaf::Ident(ident)) = &subtree.0[4] else { + return Err(ProcMacroExpansionError::Panic("incorrect Input".into())); + }; + ident + } + + _ => { + return Err(ProcMacroExpansionError::Panic("incorrect Input".into())); + } + }; + + + let generated_ident = tt::Ident { + sym: Symbol::intern(&format!("{}Suffix", ident.sym)), + span: ident.span, + is_raw: tt::IdentIsRaw::No, + }; + + let ret = quote! { call_site => + #subtree + + struct #generated_ident; + }; + + Ok(ret) + } +}