Skip to content

Commit 4717bdf

Browse files
committed
Auto merge of #17328 - Veykril:derive-helper-completions, r=Veykril
feat: Enable completions within derive helper attributes ![Code_zG5qInoQ6B](https://github.com/rust-lang/rust-analyzer/assets/3757771/db30b98d-4981-45e3-83a5-7ff23fbd3f66)
2 parents 60d2f73 + f122a6f commit 4717bdf

File tree

2 files changed

+209
-98
lines changed

2 files changed

+209
-98
lines changed

src/tools/rust-analyzer/crates/hir/src/semantics.rs

+133-88
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,27 @@ impl<'db> SemanticsImpl<'db> {
380380
self.with_ctx(|ctx| ctx.has_derives(adt))
381381
}
382382

383+
pub fn derive_helper(&self, attr: &ast::Attr) -> Option<Vec<(Macro, MacroFileId)>> {
384+
let adt = attr.syntax().ancestors().find_map(ast::Item::cast).and_then(|it| match it {
385+
ast::Item::Struct(it) => Some(ast::Adt::Struct(it)),
386+
ast::Item::Enum(it) => Some(ast::Adt::Enum(it)),
387+
ast::Item::Union(it) => Some(ast::Adt::Union(it)),
388+
_ => None,
389+
})?;
390+
let attr_name = attr.path().and_then(|it| it.as_single_name_ref())?.as_name();
391+
let sa = self.analyze_no_infer(adt.syntax())?;
392+
let id = self.db.ast_id_map(sa.file_id).ast_id(&adt);
393+
let res: Vec<_> = sa
394+
.resolver
395+
.def_map()
396+
.derive_helpers_in_scope(InFile::new(sa.file_id, id))?
397+
.iter()
398+
.filter(|&(name, _, _)| *name == attr_name)
399+
.map(|&(_, macro_, call)| (macro_.into(), call.as_macro_file()))
400+
.collect();
401+
res.is_empty().not().then_some(res)
402+
}
403+
383404
pub fn is_attr_macro_call(&self, item: &ast::Item) -> bool {
384405
let file_id = self.find_file(item.syntax()).file_id;
385406
let src = InFile::new(file_id, item.clone());
@@ -409,6 +430,20 @@ impl<'db> SemanticsImpl<'db> {
409430
)
410431
}
411432

433+
pub fn speculative_expand_raw(
434+
&self,
435+
macro_file: MacroFileId,
436+
speculative_args: &SyntaxNode,
437+
token_to_map: SyntaxToken,
438+
) -> Option<(SyntaxNode, SyntaxToken)> {
439+
hir_expand::db::expand_speculative(
440+
self.db.upcast(),
441+
macro_file.macro_call_id,
442+
speculative_args,
443+
token_to_map,
444+
)
445+
}
446+
412447
/// Expand the macro call with a different item as the input, mapping the `token_to_map` down into the
413448
/// expansion. `token_to_map` should be a token from the `speculative args` node.
414449
pub fn speculative_expand_attr_macro(
@@ -826,99 +861,109 @@ impl<'db> SemanticsImpl<'db> {
826861

827862
// Then check for token trees, that means we are either in a function-like macro or
828863
// secondary attribute inputs
829-
let tt = token.parent_ancestors().map_while(ast::TokenTree::cast).last()?;
830-
let parent = tt.syntax().parent()?;
831-
832-
if tt.left_delimiter_token().map_or(false, |it| it == token) {
833-
return None;
834-
}
835-
if tt.right_delimiter_token().map_or(false, |it| it == token) {
836-
return None;
837-
}
838-
839-
if let Some(macro_call) = ast::MacroCall::cast(parent.clone()) {
840-
let mcall: hir_expand::files::InFileWrapper<HirFileId, ast::MacroCall> =
841-
InFile::new(file_id, macro_call);
842-
let file_id = match mcache.get(&mcall) {
843-
Some(&it) => it,
844-
None => {
845-
let it = sa.expand(self.db, mcall.as_ref())?;
846-
mcache.insert(mcall, it);
847-
it
864+
let tt = token
865+
.parent_ancestors()
866+
.map_while(Either::<ast::TokenTree, ast::Meta>::cast)
867+
.last()?;
868+
match tt {
869+
Either::Left(tt) => {
870+
if tt.left_delimiter_token().map_or(false, |it| it == token) {
871+
return None;
848872
}
849-
};
850-
let text_range = tt.syntax().text_range();
851-
// remove any other token in this macro input, all their mappings are the
852-
// same as this one
853-
tokens.retain(|t| !text_range.contains_range(t.text_range()));
854-
855-
process_expansion_for_token(&mut stack, file_id).or(file_id
856-
.eager_arg(self.db.upcast())
857-
.and_then(|arg| {
858-
// also descend into eager expansions
859-
process_expansion_for_token(&mut stack, arg.as_macro_file())
860-
}))
861-
} else if let Some(meta) = ast::Meta::cast(parent) {
862-
// attribute we failed expansion for earlier, this might be a derive invocation
863-
// or derive helper attribute
864-
let attr = meta.parent_attr()?;
865-
let adt = if let Some(adt) = attr.syntax().parent().and_then(ast::Adt::cast)
866-
{
867-
// this might be a derive on an ADT
868-
let derive_call = self.with_ctx(|ctx| {
869-
// so try downmapping the token into the pseudo derive expansion
870-
// see [hir_expand::builtin_attr_macro] for how the pseudo derive expansion works
871-
ctx.attr_to_derive_macro_call(
872-
InFile::new(file_id, &adt),
873-
InFile::new(file_id, attr.clone()),
874-
)
875-
.map(|(_, call_id, _)| call_id)
876-
});
877-
878-
match derive_call {
879-
Some(call_id) => {
880-
// resolved to a derive
881-
let file_id = call_id.as_macro_file();
882-
let text_range = attr.syntax().text_range();
883-
// remove any other token in this macro input, all their mappings are the
884-
// same as this
885-
tokens.retain(|t| !text_range.contains_range(t.text_range()));
886-
return process_expansion_for_token(&mut stack, file_id);
887-
}
888-
None => Some(adt),
873+
if tt.right_delimiter_token().map_or(false, |it| it == token) {
874+
return None;
889875
}
890-
} else {
891-
// Otherwise this could be a derive helper on a variant or field
892-
attr.syntax().ancestors().find_map(ast::Item::cast).and_then(|it| {
893-
match it {
894-
ast::Item::Struct(it) => Some(ast::Adt::Struct(it)),
895-
ast::Item::Enum(it) => Some(ast::Adt::Enum(it)),
896-
ast::Item::Union(it) => Some(ast::Adt::Union(it)),
897-
_ => None,
876+
let macro_call = tt.syntax().parent().and_then(ast::MacroCall::cast)?;
877+
let mcall: hir_expand::files::InFileWrapper<HirFileId, ast::MacroCall> =
878+
InFile::new(file_id, macro_call);
879+
let file_id = match mcache.get(&mcall) {
880+
Some(&it) => it,
881+
None => {
882+
let it = sa.expand(self.db, mcall.as_ref())?;
883+
mcache.insert(mcall, it);
884+
it
898885
}
899-
})
900-
}?;
901-
if !self.with_ctx(|ctx| ctx.has_derives(InFile::new(file_id, &adt))) {
902-
return None;
886+
};
887+
let text_range = tt.syntax().text_range();
888+
// remove any other token in this macro input, all their mappings are the
889+
// same as this one
890+
tokens.retain(|t| !text_range.contains_range(t.text_range()));
891+
892+
process_expansion_for_token(&mut stack, file_id).or(file_id
893+
.eager_arg(self.db.upcast())
894+
.and_then(|arg| {
895+
// also descend into eager expansions
896+
process_expansion_for_token(&mut stack, arg.as_macro_file())
897+
}))
903898
}
904-
// Not an attribute, nor a derive, so it's either a builtin or a derive helper
905-
// Try to resolve to a derive helper and downmap
906-
let attr_name =
907-
attr.path().and_then(|it| it.as_single_name_ref())?.as_name();
908-
let id = self.db.ast_id_map(file_id).ast_id(&adt);
909-
let helpers = def_map.derive_helpers_in_scope(InFile::new(file_id, id))?;
910-
let mut res = None;
911-
for (.., derive) in
912-
helpers.iter().filter(|(helper, ..)| *helper == attr_name)
913-
{
914-
res = res.or(process_expansion_for_token(
915-
&mut stack,
916-
derive.as_macro_file(),
917-
));
899+
Either::Right(meta) => {
900+
// attribute we failed expansion for earlier, this might be a derive invocation
901+
// or derive helper attribute
902+
let attr = meta.parent_attr()?;
903+
let adt = match attr.syntax().parent().and_then(ast::Adt::cast) {
904+
Some(adt) => {
905+
// this might be a derive on an ADT
906+
let derive_call = self.with_ctx(|ctx| {
907+
// so try downmapping the token into the pseudo derive expansion
908+
// see [hir_expand::builtin_attr_macro] for how the pseudo derive expansion works
909+
ctx.attr_to_derive_macro_call(
910+
InFile::new(file_id, &adt),
911+
InFile::new(file_id, attr.clone()),
912+
)
913+
.map(|(_, call_id, _)| call_id)
914+
});
915+
916+
match derive_call {
917+
Some(call_id) => {
918+
// resolved to a derive
919+
let file_id = call_id.as_macro_file();
920+
let text_range = attr.syntax().text_range();
921+
// remove any other token in this macro input, all their mappings are the
922+
// same as this
923+
tokens.retain(|t| {
924+
!text_range.contains_range(t.text_range())
925+
});
926+
return process_expansion_for_token(
927+
&mut stack, file_id,
928+
);
929+
}
930+
None => Some(adt),
931+
}
932+
}
933+
None => {
934+
// Otherwise this could be a derive helper on a variant or field
935+
attr.syntax().ancestors().find_map(ast::Item::cast).and_then(
936+
|it| match it {
937+
ast::Item::Struct(it) => Some(ast::Adt::Struct(it)),
938+
ast::Item::Enum(it) => Some(ast::Adt::Enum(it)),
939+
ast::Item::Union(it) => Some(ast::Adt::Union(it)),
940+
_ => None,
941+
},
942+
)
943+
}
944+
}?;
945+
if !self.with_ctx(|ctx| ctx.has_derives(InFile::new(file_id, &adt))) {
946+
return None;
947+
}
948+
let attr_name =
949+
attr.path().and_then(|it| it.as_single_name_ref())?.as_name();
950+
// Not an attribute, nor a derive, so it's either a builtin or a derive helper
951+
// Try to resolve to a derive helper and downmap
952+
let id = self.db.ast_id_map(file_id).ast_id(&adt);
953+
let helpers =
954+
def_map.derive_helpers_in_scope(InFile::new(file_id, id))?;
955+
956+
let mut res = None;
957+
for (.., derive) in
958+
helpers.iter().filter(|(helper, ..)| *helper == attr_name)
959+
{
960+
res = res.or(process_expansion_for_token(
961+
&mut stack,
962+
derive.as_macro_file(),
963+
));
964+
}
965+
res
918966
}
919-
res
920-
} else {
921-
None
922967
}
923968
})()
924969
.is_none();

src/tools/rust-analyzer/crates/ide-completion/src/context/analysis.rs

+76-10
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ use std::iter;
33

44
use hir::{Semantics, Type, TypeInfo, Variant};
55
use ide_db::{active_parameter::ActiveParameter, RootDatabase};
6+
use itertools::Either;
67
use syntax::{
7-
algo::{find_node_at_offset, non_trivia_sibling},
8+
algo::{ancestors_at_offset, find_node_at_offset, non_trivia_sibling},
89
ast::{self, AttrKind, HasArgList, HasGenericParams, HasLoopBody, HasName, NameOrNameRef},
910
match_ast, AstNode, AstToken, Direction, NodeOrToken, SyntaxElement, SyntaxKind, SyntaxNode,
1011
SyntaxToken, TextRange, TextSize, T,
@@ -119,20 +120,45 @@ fn expand(
119120
}
120121

121122
// No attributes have been expanded, so look for macro_call! token trees or derive token trees
122-
let orig_tt = match find_node_at_offset::<ast::TokenTree>(&original_file, offset) {
123+
let orig_tt = match ancestors_at_offset(&original_file, offset)
124+
.map_while(Either::<ast::TokenTree, ast::Meta>::cast)
125+
.last()
126+
{
123127
Some(it) => it,
124128
None => break 'expansion,
125129
};
126-
let spec_tt = match find_node_at_offset::<ast::TokenTree>(&speculative_file, offset) {
130+
let spec_tt = match ancestors_at_offset(&speculative_file, offset)
131+
.map_while(Either::<ast::TokenTree, ast::Meta>::cast)
132+
.last()
133+
{
127134
Some(it) => it,
128135
None => break 'expansion,
129136
};
130137

131-
// Expand pseudo-derive expansion
132-
if let (Some(orig_attr), Some(spec_attr)) = (
133-
orig_tt.syntax().parent().and_then(ast::Meta::cast).and_then(|it| it.parent_attr()),
134-
spec_tt.syntax().parent().and_then(ast::Meta::cast).and_then(|it| it.parent_attr()),
135-
) {
138+
let (tts, attrs) = match (orig_tt, spec_tt) {
139+
(Either::Left(orig_tt), Either::Left(spec_tt)) => {
140+
let attrs = orig_tt
141+
.syntax()
142+
.parent()
143+
.and_then(ast::Meta::cast)
144+
.and_then(|it| it.parent_attr())
145+
.zip(
146+
spec_tt
147+
.syntax()
148+
.parent()
149+
.and_then(ast::Meta::cast)
150+
.and_then(|it| it.parent_attr()),
151+
);
152+
(Some((orig_tt, spec_tt)), attrs)
153+
}
154+
(Either::Right(orig_path), Either::Right(spec_path)) => {
155+
(None, orig_path.parent_attr().zip(spec_path.parent_attr()))
156+
}
157+
_ => break 'expansion,
158+
};
159+
160+
// Expand pseudo-derive expansion aka `derive(Debug$0)`
161+
if let Some((orig_attr, spec_attr)) = attrs {
136162
if let (Some(actual_expansion), Some((fake_expansion, fake_mapped_token))) = (
137163
sema.expand_derive_as_pseudo_attr_macro(&orig_attr),
138164
sema.speculative_expand_derive_as_pseudo_attr_macro(
@@ -147,15 +173,54 @@ fn expand(
147173
fake_mapped_token.text_range().start(),
148174
orig_attr,
149175
));
176+
break 'expansion;
177+
}
178+
179+
if let Some(spec_adt) =
180+
spec_attr.syntax().ancestors().find_map(ast::Item::cast).and_then(|it| match it {
181+
ast::Item::Struct(it) => Some(ast::Adt::Struct(it)),
182+
ast::Item::Enum(it) => Some(ast::Adt::Enum(it)),
183+
ast::Item::Union(it) => Some(ast::Adt::Union(it)),
184+
_ => None,
185+
})
186+
{
187+
// might be the path of derive helper or a token tree inside of one
188+
if let Some(helpers) = sema.derive_helper(&orig_attr) {
189+
for (_mac, file) in helpers {
190+
if let Some((fake_expansion, fake_mapped_token)) = sema
191+
.speculative_expand_raw(
192+
file,
193+
spec_adt.syntax(),
194+
fake_ident_token.clone(),
195+
)
196+
{
197+
// we are inside a derive helper token tree, treat this as being inside
198+
// the derive expansion
199+
let actual_expansion = sema.parse_or_expand(file.into());
200+
let new_offset = fake_mapped_token.text_range().start();
201+
if new_offset + relative_offset > actual_expansion.text_range().end() {
202+
// offset outside of bounds from the original expansion,
203+
// stop here to prevent problems from happening
204+
break 'expansion;
205+
}
206+
original_file = actual_expansion;
207+
speculative_file = fake_expansion;
208+
fake_ident_token = fake_mapped_token;
209+
offset = new_offset;
210+
continue 'expansion;
211+
}
212+
}
213+
}
150214
}
151215
// at this point we won't have any more successful expansions, so stop
152216
break 'expansion;
153217
}
154218

155219
// Expand fn-like macro calls
220+
let Some((orig_tt, spec_tt)) = tts else { break 'expansion };
156221
if let (Some(actual_macro_call), Some(macro_call_with_fake_ident)) = (
157-
orig_tt.syntax().ancestors().find_map(ast::MacroCall::cast),
158-
spec_tt.syntax().ancestors().find_map(ast::MacroCall::cast),
222+
orig_tt.syntax().parent().and_then(ast::MacroCall::cast),
223+
spec_tt.syntax().parent().and_then(ast::MacroCall::cast),
159224
) {
160225
let mac_call_path0 = actual_macro_call.path().as_ref().map(|s| s.syntax().text());
161226
let mac_call_path1 =
@@ -201,6 +266,7 @@ fn expand(
201266
// none of our states have changed so stop the loop
202267
break 'expansion;
203268
}
269+
204270
ExpansionResult { original_file, speculative_file, offset, fake_ident_token, derive_ctx }
205271
}
206272

0 commit comments

Comments
 (0)