From f122a6f2f137649efdf4f6a8561b3be52b4a88bb Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sun, 2 Jun 2024 09:28:34 +0200 Subject: [PATCH] feat: Enable completions within derive helper attributes --- .../rust-analyzer/crates/hir/src/semantics.rs | 221 +++++++++++------- .../ide-completion/src/context/analysis.rs | 86 ++++++- 2 files changed, 209 insertions(+), 98 deletions(-) diff --git a/src/tools/rust-analyzer/crates/hir/src/semantics.rs b/src/tools/rust-analyzer/crates/hir/src/semantics.rs index 53242611f81..43de2a6ee7d 100644 --- a/src/tools/rust-analyzer/crates/hir/src/semantics.rs +++ b/src/tools/rust-analyzer/crates/hir/src/semantics.rs @@ -380,6 +380,27 @@ pub fn is_derive_annotated(&self, adt: &ast::Adt) -> bool { self.with_ctx(|ctx| ctx.has_derives(adt)) } + pub fn derive_helper(&self, attr: &ast::Attr) -> Option> { + let adt = attr.syntax().ancestors().find_map(ast::Item::cast).and_then(|it| match it { + ast::Item::Struct(it) => Some(ast::Adt::Struct(it)), + ast::Item::Enum(it) => Some(ast::Adt::Enum(it)), + ast::Item::Union(it) => Some(ast::Adt::Union(it)), + _ => None, + })?; + let attr_name = attr.path().and_then(|it| it.as_single_name_ref())?.as_name(); + let sa = self.analyze_no_infer(adt.syntax())?; + let id = self.db.ast_id_map(sa.file_id).ast_id(&adt); + let res: Vec<_> = sa + .resolver + .def_map() + .derive_helpers_in_scope(InFile::new(sa.file_id, id))? + .iter() + .filter(|&(name, _, _)| *name == attr_name) + .map(|&(_, macro_, call)| (macro_.into(), call.as_macro_file())) + .collect(); + res.is_empty().not().then_some(res) + } + pub fn is_attr_macro_call(&self, item: &ast::Item) -> bool { let file_id = self.find_file(item.syntax()).file_id; let src = InFile::new(file_id, item.clone()); @@ -409,6 +430,20 @@ pub fn speculative_expand( ) } + pub fn speculative_expand_raw( + &self, + macro_file: MacroFileId, + speculative_args: &SyntaxNode, + token_to_map: SyntaxToken, + ) -> Option<(SyntaxNode, SyntaxToken)> { + hir_expand::db::expand_speculative( + self.db.upcast(), + macro_file.macro_call_id, + speculative_args, + token_to_map, + ) + } + /// Expand the macro call with a different item as the input, mapping the `token_to_map` down into the /// expansion. `token_to_map` should be a token from the `speculative args` node. pub fn speculative_expand_attr_macro( @@ -826,99 +861,109 @@ fn descend_into_macros_impl( // Then check for token trees, that means we are either in a function-like macro or // secondary attribute inputs - let tt = token.parent_ancestors().map_while(ast::TokenTree::cast).last()?; - let parent = tt.syntax().parent()?; - - if tt.left_delimiter_token().map_or(false, |it| it == token) { - return None; - } - if tt.right_delimiter_token().map_or(false, |it| it == token) { - return None; - } - - if let Some(macro_call) = ast::MacroCall::cast(parent.clone()) { - let mcall: hir_expand::files::InFileWrapper = - InFile::new(file_id, macro_call); - let file_id = match mcache.get(&mcall) { - Some(&it) => it, - None => { - let it = sa.expand(self.db, mcall.as_ref())?; - mcache.insert(mcall, it); - it + let tt = token + .parent_ancestors() + .map_while(Either::::cast) + .last()?; + match tt { + Either::Left(tt) => { + if tt.left_delimiter_token().map_or(false, |it| it == token) { + return None; } - }; - let text_range = tt.syntax().text_range(); - // remove any other token in this macro input, all their mappings are the - // same as this one - tokens.retain(|t| !text_range.contains_range(t.text_range())); - - process_expansion_for_token(&mut stack, file_id).or(file_id - .eager_arg(self.db.upcast()) - .and_then(|arg| { - // also descend into eager expansions - process_expansion_for_token(&mut stack, arg.as_macro_file()) - })) - } else if let Some(meta) = ast::Meta::cast(parent) { - // attribute we failed expansion for earlier, this might be a derive invocation - // or derive helper attribute - let attr = meta.parent_attr()?; - let adt = if let Some(adt) = attr.syntax().parent().and_then(ast::Adt::cast) - { - // this might be a derive on an ADT - let derive_call = self.with_ctx(|ctx| { - // so try downmapping the token into the pseudo derive expansion - // see [hir_expand::builtin_attr_macro] for how the pseudo derive expansion works - ctx.attr_to_derive_macro_call( - InFile::new(file_id, &adt), - InFile::new(file_id, attr.clone()), - ) - .map(|(_, call_id, _)| call_id) - }); - - match derive_call { - Some(call_id) => { - // resolved to a derive - let file_id = call_id.as_macro_file(); - let text_range = attr.syntax().text_range(); - // remove any other token in this macro input, all their mappings are the - // same as this - tokens.retain(|t| !text_range.contains_range(t.text_range())); - return process_expansion_for_token(&mut stack, file_id); - } - None => Some(adt), + if tt.right_delimiter_token().map_or(false, |it| it == token) { + return None; } - } else { - // Otherwise this could be a derive helper on a variant or field - attr.syntax().ancestors().find_map(ast::Item::cast).and_then(|it| { - match it { - ast::Item::Struct(it) => Some(ast::Adt::Struct(it)), - ast::Item::Enum(it) => Some(ast::Adt::Enum(it)), - ast::Item::Union(it) => Some(ast::Adt::Union(it)), - _ => None, + let macro_call = tt.syntax().parent().and_then(ast::MacroCall::cast)?; + let mcall: hir_expand::files::InFileWrapper = + InFile::new(file_id, macro_call); + let file_id = match mcache.get(&mcall) { + Some(&it) => it, + None => { + let it = sa.expand(self.db, mcall.as_ref())?; + mcache.insert(mcall, it); + it } - }) - }?; - if !self.with_ctx(|ctx| ctx.has_derives(InFile::new(file_id, &adt))) { - return None; + }; + let text_range = tt.syntax().text_range(); + // remove any other token in this macro input, all their mappings are the + // same as this one + tokens.retain(|t| !text_range.contains_range(t.text_range())); + + process_expansion_for_token(&mut stack, file_id).or(file_id + .eager_arg(self.db.upcast()) + .and_then(|arg| { + // also descend into eager expansions + process_expansion_for_token(&mut stack, arg.as_macro_file()) + })) } - // Not an attribute, nor a derive, so it's either a builtin or a derive helper - // Try to resolve to a derive helper and downmap - let attr_name = - attr.path().and_then(|it| it.as_single_name_ref())?.as_name(); - let id = self.db.ast_id_map(file_id).ast_id(&adt); - let helpers = def_map.derive_helpers_in_scope(InFile::new(file_id, id))?; - let mut res = None; - for (.., derive) in - helpers.iter().filter(|(helper, ..)| *helper == attr_name) - { - res = res.or(process_expansion_for_token( - &mut stack, - derive.as_macro_file(), - )); + Either::Right(meta) => { + // attribute we failed expansion for earlier, this might be a derive invocation + // or derive helper attribute + let attr = meta.parent_attr()?; + let adt = match attr.syntax().parent().and_then(ast::Adt::cast) { + Some(adt) => { + // this might be a derive on an ADT + let derive_call = self.with_ctx(|ctx| { + // so try downmapping the token into the pseudo derive expansion + // see [hir_expand::builtin_attr_macro] for how the pseudo derive expansion works + ctx.attr_to_derive_macro_call( + InFile::new(file_id, &adt), + InFile::new(file_id, attr.clone()), + ) + .map(|(_, call_id, _)| call_id) + }); + + match derive_call { + Some(call_id) => { + // resolved to a derive + let file_id = call_id.as_macro_file(); + let text_range = attr.syntax().text_range(); + // remove any other token in this macro input, all their mappings are the + // same as this + tokens.retain(|t| { + !text_range.contains_range(t.text_range()) + }); + return process_expansion_for_token( + &mut stack, file_id, + ); + } + None => Some(adt), + } + } + None => { + // Otherwise this could be a derive helper on a variant or field + attr.syntax().ancestors().find_map(ast::Item::cast).and_then( + |it| match it { + ast::Item::Struct(it) => Some(ast::Adt::Struct(it)), + ast::Item::Enum(it) => Some(ast::Adt::Enum(it)), + ast::Item::Union(it) => Some(ast::Adt::Union(it)), + _ => None, + }, + ) + } + }?; + if !self.with_ctx(|ctx| ctx.has_derives(InFile::new(file_id, &adt))) { + return None; + } + let attr_name = + attr.path().and_then(|it| it.as_single_name_ref())?.as_name(); + // Not an attribute, nor a derive, so it's either a builtin or a derive helper + // Try to resolve to a derive helper and downmap + let id = self.db.ast_id_map(file_id).ast_id(&adt); + let helpers = + def_map.derive_helpers_in_scope(InFile::new(file_id, id))?; + + let mut res = None; + for (.., derive) in + helpers.iter().filter(|(helper, ..)| *helper == attr_name) + { + res = res.or(process_expansion_for_token( + &mut stack, + derive.as_macro_file(), + )); + } + res } - res - } else { - None } })() .is_none(); diff --git a/src/tools/rust-analyzer/crates/ide-completion/src/context/analysis.rs b/src/tools/rust-analyzer/crates/ide-completion/src/context/analysis.rs index 79c503e0a10..f0c6e7a63b0 100644 --- a/src/tools/rust-analyzer/crates/ide-completion/src/context/analysis.rs +++ b/src/tools/rust-analyzer/crates/ide-completion/src/context/analysis.rs @@ -3,8 +3,9 @@ use hir::{Semantics, Type, TypeInfo, Variant}; use ide_db::{active_parameter::ActiveParameter, RootDatabase}; +use itertools::Either; use syntax::{ - algo::{find_node_at_offset, non_trivia_sibling}, + algo::{ancestors_at_offset, find_node_at_offset, non_trivia_sibling}, ast::{self, AttrKind, HasArgList, HasGenericParams, HasLoopBody, HasName, NameOrNameRef}, match_ast, AstNode, AstToken, Direction, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken, TextRange, TextSize, T, @@ -119,20 +120,45 @@ fn expand( } // No attributes have been expanded, so look for macro_call! token trees or derive token trees - let orig_tt = match find_node_at_offset::(&original_file, offset) { + let orig_tt = match ancestors_at_offset(&original_file, offset) + .map_while(Either::::cast) + .last() + { Some(it) => it, None => break 'expansion, }; - let spec_tt = match find_node_at_offset::(&speculative_file, offset) { + let spec_tt = match ancestors_at_offset(&speculative_file, offset) + .map_while(Either::::cast) + .last() + { Some(it) => it, None => break 'expansion, }; - // Expand pseudo-derive expansion - if let (Some(orig_attr), Some(spec_attr)) = ( - orig_tt.syntax().parent().and_then(ast::Meta::cast).and_then(|it| it.parent_attr()), - spec_tt.syntax().parent().and_then(ast::Meta::cast).and_then(|it| it.parent_attr()), - ) { + let (tts, attrs) = match (orig_tt, spec_tt) { + (Either::Left(orig_tt), Either::Left(spec_tt)) => { + let attrs = orig_tt + .syntax() + .parent() + .and_then(ast::Meta::cast) + .and_then(|it| it.parent_attr()) + .zip( + spec_tt + .syntax() + .parent() + .and_then(ast::Meta::cast) + .and_then(|it| it.parent_attr()), + ); + (Some((orig_tt, spec_tt)), attrs) + } + (Either::Right(orig_path), Either::Right(spec_path)) => { + (None, orig_path.parent_attr().zip(spec_path.parent_attr())) + } + _ => break 'expansion, + }; + + // Expand pseudo-derive expansion aka `derive(Debug$0)` + if let Some((orig_attr, spec_attr)) = attrs { if let (Some(actual_expansion), Some((fake_expansion, fake_mapped_token))) = ( sema.expand_derive_as_pseudo_attr_macro(&orig_attr), sema.speculative_expand_derive_as_pseudo_attr_macro( @@ -147,15 +173,54 @@ fn expand( fake_mapped_token.text_range().start(), orig_attr, )); + break 'expansion; + } + + if let Some(spec_adt) = + spec_attr.syntax().ancestors().find_map(ast::Item::cast).and_then(|it| match it { + ast::Item::Struct(it) => Some(ast::Adt::Struct(it)), + ast::Item::Enum(it) => Some(ast::Adt::Enum(it)), + ast::Item::Union(it) => Some(ast::Adt::Union(it)), + _ => None, + }) + { + // might be the path of derive helper or a token tree inside of one + if let Some(helpers) = sema.derive_helper(&orig_attr) { + for (_mac, file) in helpers { + if let Some((fake_expansion, fake_mapped_token)) = sema + .speculative_expand_raw( + file, + spec_adt.syntax(), + fake_ident_token.clone(), + ) + { + // we are inside a derive helper token tree, treat this as being inside + // the derive expansion + let actual_expansion = sema.parse_or_expand(file.into()); + let new_offset = fake_mapped_token.text_range().start(); + if new_offset + relative_offset > actual_expansion.text_range().end() { + // offset outside of bounds from the original expansion, + // stop here to prevent problems from happening + break 'expansion; + } + original_file = actual_expansion; + speculative_file = fake_expansion; + fake_ident_token = fake_mapped_token; + offset = new_offset; + continue 'expansion; + } + } + } } // at this point we won't have any more successful expansions, so stop break 'expansion; } // Expand fn-like macro calls + let Some((orig_tt, spec_tt)) = tts else { break 'expansion }; if let (Some(actual_macro_call), Some(macro_call_with_fake_ident)) = ( - orig_tt.syntax().ancestors().find_map(ast::MacroCall::cast), - spec_tt.syntax().ancestors().find_map(ast::MacroCall::cast), + orig_tt.syntax().parent().and_then(ast::MacroCall::cast), + spec_tt.syntax().parent().and_then(ast::MacroCall::cast), ) { let mac_call_path0 = actual_macro_call.path().as_ref().map(|s| s.syntax().text()); let mac_call_path1 = @@ -201,6 +266,7 @@ fn expand( // none of our states have changed so stop the loop break 'expansion; } + ExpansionResult { original_file, speculative_file, offset, fake_ident_token, derive_ctx } }