Support derive-macros for rename prefix magic

This commit is contained in:
Lukas Wirth 2025-06-02 15:22:03 +02:00
parent 96c4455114
commit 9a786d0008
4 changed files with 211 additions and 117 deletions

View File

@ -21,6 +21,8 @@ smol_str.opt-level = 3
text-size.opt-level = 3 text-size.opt-level = 3
serde.opt-level = 3 serde.opt-level = 3
salsa.opt-level = 3 salsa.opt-level = 3
dissimilar.opt-level = 3
# This speeds up `cargo xtask dist`. # This speeds up `cargo xtask dist`.
miniz_oxide.opt-level = 3 miniz_oxide.opt-level = 3

View File

@ -67,8 +67,14 @@ pub mod keys {
pub const PROC_MACRO: Key<ast::Fn, ProcMacroId> = Key::new(); pub const PROC_MACRO: Key<ast::Fn, ProcMacroId> = Key::new();
pub const MACRO_CALL: Key<ast::MacroCall, MacroCallId> = Key::new(); pub const MACRO_CALL: Key<ast::MacroCall, MacroCallId> = Key::new();
pub const ATTR_MACRO_CALL: Key<ast::Item, MacroCallId> = Key::new(); pub const ATTR_MACRO_CALL: Key<ast::Item, MacroCallId> = Key::new();
pub const DERIVE_MACRO_CALL: Key<ast::Attr, (AttrId, MacroCallId, Box<[Option<MacroCallId>]>)> = pub const DERIVE_MACRO_CALL: Key<
Key::new(); ast::Attr,
(
AttrId,
/* derive() */ MacroCallId,
/* actual derive macros */ Box<[Option<MacroCallId>]>,
),
> = Key::new();
/// XXX: AST Nodes and SyntaxNodes have identity equality semantics: nodes are /// XXX: AST Nodes and SyntaxNodes have identity equality semantics: nodes are
/// equal if they point to exactly the same object. /// equal if they point to exactly the same object.

View File

@ -550,7 +550,7 @@ impl<'db> SemanticsImpl<'db> {
} }
pub fn is_derive_annotated(&self, adt: InFile<&ast::Adt>) -> bool { pub fn is_derive_annotated(&self, adt: InFile<&ast::Adt>) -> bool {
self.with_ctx(|ctx| ctx.has_derives(adt)) self.with_ctx(|ctx| ctx.file_of_adt_has_derives(adt))
} }
pub fn derive_helpers_in_scope(&self, adt: &ast::Adt) -> Option<Vec<(Symbol, Symbol)>> { pub fn derive_helpers_in_scope(&self, adt: &ast::Adt) -> Option<Vec<(Symbol, Symbol)>> {
@ -894,6 +894,7 @@ impl<'db> SemanticsImpl<'db> {
// node is just the token, so descend the token // node is just the token, so descend the token
self.descend_into_macros_impl( self.descend_into_macros_impl(
InFile::new(file.file_id, first), InFile::new(file.file_id, first),
false,
&mut |InFile { value, .. }, _ctx| { &mut |InFile { value, .. }, _ctx| {
if let Some(node) = value if let Some(node) = value
.parent_ancestors() .parent_ancestors()
@ -908,14 +909,19 @@ impl<'db> SemanticsImpl<'db> {
} else { } else {
// Descend first and last token, then zip them to look for the node they belong to // Descend first and last token, then zip them to look for the node they belong to
let mut scratch: SmallVec<[_; 1]> = smallvec![]; let mut scratch: SmallVec<[_; 1]> = smallvec![];
self.descend_into_macros_impl(InFile::new(file.file_id, first), &mut |token, _ctx| { self.descend_into_macros_impl(
scratch.push(token); InFile::new(file.file_id, first),
CONTINUE_NO_BREAKS false,
}); &mut |token, _ctx| {
scratch.push(token);
CONTINUE_NO_BREAKS
},
);
let mut scratch = scratch.into_iter(); let mut scratch = scratch.into_iter();
self.descend_into_macros_impl( self.descend_into_macros_impl(
InFile::new(file.file_id, last), InFile::new(file.file_id, last),
false,
&mut |InFile { value: last, file_id: last_fid }, _ctx| { &mut |InFile { value: last, file_id: last_fid }, _ctx| {
if let Some(InFile { value: first, file_id: first_fid }) = scratch.next() { if let Some(InFile { value: first, file_id: first_fid }) = scratch.next() {
if first_fid == last_fid { if first_fid == last_fid {
@ -967,7 +973,7 @@ impl<'db> SemanticsImpl<'db> {
ast::Item::Union(it) => it.into(), ast::Item::Union(it) => it.into(),
_ => return false, _ => return false,
}; };
ctx.has_derives(token.with_value(&adt)) ctx.file_of_adt_has_derives(token.with_value(&adt))
}) })
}) })
} }
@ -977,7 +983,7 @@ impl<'db> SemanticsImpl<'db> {
token: SyntaxToken, token: SyntaxToken,
mut cb: impl FnMut(InFile<SyntaxToken>, SyntaxContext), mut cb: impl FnMut(InFile<SyntaxToken>, SyntaxContext),
) { ) {
self.descend_into_macros_impl(self.wrap_token_infile(token), &mut |t, ctx| { self.descend_into_macros_impl(self.wrap_token_infile(token), false, &mut |t, ctx| {
cb(t, ctx); cb(t, ctx);
CONTINUE_NO_BREAKS CONTINUE_NO_BREAKS
}); });
@ -985,10 +991,14 @@ impl<'db> SemanticsImpl<'db> {
pub fn descend_into_macros(&self, token: SyntaxToken) -> SmallVec<[SyntaxToken; 1]> { pub fn descend_into_macros(&self, token: SyntaxToken) -> SmallVec<[SyntaxToken; 1]> {
let mut res = smallvec![]; let mut res = smallvec![];
self.descend_into_macros_impl(self.wrap_token_infile(token.clone()), &mut |t, _ctx| { self.descend_into_macros_impl(
res.push(t.value); self.wrap_token_infile(token.clone()),
CONTINUE_NO_BREAKS false,
}); &mut |t, _ctx| {
res.push(t.value);
CONTINUE_NO_BREAKS
},
);
if res.is_empty() { if res.is_empty() {
res.push(token); res.push(token);
} }
@ -1001,7 +1011,7 @@ impl<'db> SemanticsImpl<'db> {
) -> SmallVec<[InFile<SyntaxToken>; 1]> { ) -> SmallVec<[InFile<SyntaxToken>; 1]> {
let mut res = smallvec![]; let mut res = smallvec![];
let token = self.wrap_token_infile(token); let token = self.wrap_token_infile(token);
self.descend_into_macros_impl(token.clone(), &mut |t, ctx| { self.descend_into_macros_impl(token.clone(), true, &mut |t, ctx| {
if !ctx.is_opaque(self.db) { if !ctx.is_opaque(self.db) {
// Don't descend into opaque contexts // Don't descend into opaque contexts
res.push(t); res.push(t);
@ -1019,7 +1029,7 @@ impl<'db> SemanticsImpl<'db> {
token: InFile<SyntaxToken>, token: InFile<SyntaxToken>,
mut cb: impl FnMut(InFile<SyntaxToken>, SyntaxContext) -> ControlFlow<T>, mut cb: impl FnMut(InFile<SyntaxToken>, SyntaxContext) -> ControlFlow<T>,
) -> Option<T> { ) -> Option<T> {
self.descend_into_macros_impl(token, &mut cb) self.descend_into_macros_impl(token, false, &mut cb)
} }
/// Descends the token into expansions, returning the tokens that matches the input /// Descends the token into expansions, returning the tokens that matches the input
@ -1092,41 +1102,41 @@ impl<'db> SemanticsImpl<'db> {
fn descend_into_macros_impl<T>( fn descend_into_macros_impl<T>(
&self, &self,
InFile { value: token, file_id }: InFile<SyntaxToken>, InFile { value: token, file_id }: InFile<SyntaxToken>,
always_descend_into_derives: bool,
f: &mut dyn FnMut(InFile<SyntaxToken>, SyntaxContext) -> ControlFlow<T>, f: &mut dyn FnMut(InFile<SyntaxToken>, SyntaxContext) -> ControlFlow<T>,
) -> Option<T> { ) -> Option<T> {
let _p = tracing::info_span!("descend_into_macros_impl").entered(); let _p = tracing::info_span!("descend_into_macros_impl").entered();
let span = self.db.span_map(file_id).span_for_range(token.text_range()); let db = self.db;
let span = db.span_map(file_id).span_for_range(token.text_range());
// Process the expansion of a call, pushing all tokens with our span in the expansion back onto our stack // Process the expansion of a call, pushing all tokens with our span in the expansion back onto our stack
let process_expansion_for_token = |stack: &mut Vec<_>, macro_file| { let process_expansion_for_token =
let InMacroFile { file_id, value: mapped_tokens } = self.with_ctx(|ctx| { |ctx: &mut SourceToDefCtx<'_, '_>, stack: &mut Vec<_>, macro_file| {
Some( let InMacroFile { file_id, value: mapped_tokens } = ctx
ctx.cache .cache
.get_or_insert_expansion(ctx.db, macro_file) .get_or_insert_expansion(ctx.db, macro_file)
.map_range_down(span)? .map_range_down(span)?
.map(SmallVec::<[_; 2]>::from_iter), .map(SmallVec::<[_; 2]>::from_iter);
) // we have found a mapping for the token if the vec is non-empty
})?; let res = mapped_tokens.is_empty().not().then_some(());
// we have found a mapping for the token if the vec is non-empty // requeue the tokens we got from mapping our current token down
let res = mapped_tokens.is_empty().not().then_some(()); stack.push((HirFileId::from(file_id), mapped_tokens));
// requeue the tokens we got from mapping our current token down res
stack.push((HirFileId::from(file_id), mapped_tokens)); };
res
};
// A stack of tokens to process, along with the file they came from // A stack of tokens to process, along with the file they came from
// These are tracked to know which macro calls we still have to look into // These are tracked to know which macro calls we still have to look into
// the tokens themselves aren't that interesting as the span that is being used to map // the tokens themselves aren't that interesting as the span that is being used to map
// things down never changes. // things down never changes.
let mut stack: Vec<(_, SmallVec<[_; 2]>)> = vec![]; let mut stack: Vec<(_, SmallVec<[_; 2]>)> = vec![];
let include = file_id.file_id().and_then(|file_id| { let include = file_id
self.s2d_cache.borrow_mut().get_or_insert_include_for(self.db, file_id) .file_id()
}); .and_then(|file_id| self.s2d_cache.borrow_mut().get_or_insert_include_for(db, file_id));
match include { match include {
Some(include) => { Some(include) => {
// include! inputs are always from real files, so they only need to be handled once upfront // include! inputs are always from real files, so they only need to be handled once upfront
process_expansion_for_token(&mut stack, include)?; self.with_ctx(|ctx| process_expansion_for_token(ctx, &mut stack, include))?;
} }
None => { None => {
stack.push((file_id, smallvec![(token, span.ctx)])); stack.push((file_id, smallvec![(token, span.ctx)]));
@ -1148,62 +1158,120 @@ impl<'db> SemanticsImpl<'db> {
tokens.reverse(); tokens.reverse();
while let Some((token, ctx)) = tokens.pop() { while let Some((token, ctx)) = tokens.pop() {
let was_not_remapped = (|| { let was_not_remapped = (|| {
// First expand into attribute invocations // First expand into attribute invocations, this is required to be handled
let containing_attribute_macro_call = self.with_ctx(|ctx| { // upfront as any other macro call within will not semantically resolve unless
token.parent_ancestors().filter_map(ast::Item::cast).find_map(|item| { // also descended.
// Don't force populate the dyn cache for items that don't have an attribute anyways let res = self.with_ctx(|ctx| {
item.attrs().next()?; token
Some((ctx.item_to_macro_call(InFile::new(expansion, &item))?, item)) .parent_ancestors()
}) .filter_map(ast::Item::cast)
}); // FIXME: This might work incorrectly when we have a derive, followed by
if let Some((call_id, item)) = containing_attribute_macro_call { // an attribute on an item, like:
let attr_id = match self.db.lookup_intern_macro_call(call_id).kind { // ```
hir_expand::MacroCallKind::Attr { invoc_attr_index, .. } => { // #[derive(Debug$0)]
invoc_attr_index.ast_index() // #[my_attr]
} // struct MyStruct;
_ => 0, // ```
}; // here we should not consider the attribute at all, as our cursor
// FIXME: here, the attribute's text range is used to strip away all // technically lies outside of its expansion
// entries from the start of the attribute "list" up the invoking .find_map(|item| {
// attribute. But in // Don't force populate the dyn cache for items that don't have an attribute anyways
// ``` item.attrs().next()?;
// mod foo { ctx.item_to_macro_call(InFile::new(expansion, &item))
// #![inner] .zip(Some(item))
// }
// ```
// we don't wanna strip away stuff in the `mod foo {` range, that is
// here if the id corresponds to an inner attribute we got strip all
// text ranges of the outer ones, and then all of the inner ones up
// to the invoking attribute so that the inbetween is ignored.
let text_range = item.syntax().text_range();
let start = collect_attrs(&item)
.nth(attr_id)
.map(|attr| match attr.1 {
Either::Left(it) => it.syntax().text_range().start(),
Either::Right(it) => it.syntax().text_range().start(),
}) })
.unwrap_or_else(|| text_range.start()); .map(|(call_id, item)| {
let text_range = TextRange::new(start, text_range.end()); let attr_id = match db.lookup_intern_macro_call(call_id).kind {
filter_duplicates(tokens, text_range); hir_expand::MacroCallKind::Attr {
return process_expansion_for_token(&mut stack, call_id); invoc_attr_index, ..
} => invoc_attr_index.ast_index(),
_ => 0,
};
// FIXME: here, the attribute's text range is used to strip away all
// entries from the start of the attribute "list" up the invoking
// attribute. But in
// ```
// mod foo {
// #![inner]
// }
// ```
// we don't wanna strip away stuff in the `mod foo {` range, that is
// here if the id corresponds to an inner attribute we got strip all
// text ranges of the outer ones, and then all of the inner ones up
// to the invoking attribute so that the inbetween is ignored.
let text_range = item.syntax().text_range();
let start = collect_attrs(&item)
.nth(attr_id)
.map(|attr| match attr.1 {
Either::Left(it) => it.syntax().text_range().start(),
Either::Right(it) => it.syntax().text_range().start(),
})
.unwrap_or_else(|| text_range.start());
let text_range = TextRange::new(start, text_range.end());
filter_duplicates(tokens, text_range);
process_expansion_for_token(ctx, &mut stack, call_id)
})
});
if let Some(res) = res {
return res;
} }
if always_descend_into_derives {
let res = self.with_ctx(|ctx| {
let (derives, adt) = token
.parent_ancestors()
.filter_map(ast::Adt::cast)
.find_map(|adt| {
Some((
ctx.derive_macro_calls(InFile::new(expansion, &adt))?
.map(|(a, b, c)| (a, b, c.to_owned()))
.collect::<SmallVec<[_; 2]>>(),
adt,
))
})?;
let mut res = None;
for (_, derive_attr, derives) in derives {
// as there may be multiple derives registering the same helper
// name, we gotta make sure to call this for all of them!
// FIXME: We need to call `f` for all of them as well though!
res = res.or(process_expansion_for_token(
ctx,
&mut stack,
derive_attr,
));
for derive in derives.into_iter().flatten() {
res = res
.or(process_expansion_for_token(ctx, &mut stack, derive));
}
}
// remove all tokens that are within the derives expansion
filter_duplicates(tokens, adt.syntax().text_range());
Some(res)
});
// if we found derives, we can early exit. There is no way we can be in any
// macro call at this point given we are not in a token tree
if let Some(res) = res {
return res;
}
}
// Then check for token trees, that means we are either in a function-like macro or // Then check for token trees, that means we are either in a function-like macro or
// secondary attribute inputs // secondary attribute inputs
let tt = token let tt = token
.parent_ancestors() .parent_ancestors()
.map_while(Either::<ast::TokenTree, ast::Meta>::cast) .map_while(Either::<ast::TokenTree, ast::Meta>::cast)
.last()?; .last()?;
match tt { match tt {
// function-like macro call // function-like macro call
Either::Left(tt) => { Either::Left(tt) => {
let macro_call = tt.syntax().parent().and_then(ast::MacroCall::cast)?;
if tt.left_delimiter_token().map_or(false, |it| it == token) { if tt.left_delimiter_token().map_or(false, |it| it == token) {
return None; return None;
} }
if tt.right_delimiter_token().map_or(false, |it| it == token) { if tt.right_delimiter_token().map_or(false, |it| it == token) {
return None; return None;
} }
let macro_call = tt.syntax().parent().and_then(ast::MacroCall::cast)?;
let mcall = InFile::new(expansion, macro_call); let mcall = InFile::new(expansion, macro_call);
let file_id = match m_cache.get(&mcall) { let file_id = match m_cache.get(&mcall) {
Some(&it) => it, Some(&it) => it,
@ -1216,13 +1284,16 @@ impl<'db> SemanticsImpl<'db> {
let text_range = tt.syntax().text_range(); let text_range = tt.syntax().text_range();
filter_duplicates(tokens, text_range); filter_duplicates(tokens, text_range);
process_expansion_for_token(&mut stack, file_id).or(file_id self.with_ctx(|ctx| {
.eager_arg(self.db) process_expansion_for_token(ctx, &mut stack, file_id).or(file_id
.and_then(|arg| { .eager_arg(db)
// also descend into eager expansions .and_then(|arg| {
process_expansion_for_token(&mut stack, arg) // also descend into eager expansions
})) process_expansion_for_token(ctx, &mut stack, arg)
}))
})
} }
Either::Right(_) if always_descend_into_derives => None,
// derive or derive helper // derive or derive helper
Either::Right(meta) => { Either::Right(meta) => {
// attribute we failed expansion for earlier, this might be a derive invocation // attribute we failed expansion for earlier, this might be a derive invocation
@ -1231,31 +1302,33 @@ impl<'db> SemanticsImpl<'db> {
let adt = match attr.syntax().parent().and_then(ast::Adt::cast) { let adt = match attr.syntax().parent().and_then(ast::Adt::cast) {
Some(adt) => { Some(adt) => {
// this might be a derive on an ADT // this might be a derive on an ADT
let derive_call = self.with_ctx(|ctx| { let res = self.with_ctx(|ctx| {
// so try downmapping the token into the pseudo derive expansion // so try downmapping the token into the pseudo derive expansion
// see [hir_expand::builtin_attr_macro] for how the pseudo derive expansion works // see [hir_expand::builtin_attr_macro] for how the pseudo derive expansion works
ctx.attr_to_derive_macro_call( let derive_call = ctx
InFile::new(expansion, &adt), .attr_to_derive_macro_call(
InFile::new(expansion, attr.clone()), InFile::new(expansion, &adt),
) InFile::new(expansion, attr.clone()),
.map(|(_, call_id, _)| call_id) )?
}); .1;
match derive_call { // resolved to a derive
Some(call_id) => { let text_range = attr.syntax().text_range();
// resolved to a derive // remove any other token in this macro input, all their mappings are the
let text_range = attr.syntax().text_range(); // same as this
// remove any other token in this macro input, all their mappings are the tokens.retain(|(t, _)| {
// same as this !text_range.contains_range(t.text_range())
tokens.retain(|(t, _)| { });
!text_range.contains_range(t.text_range()) Some(process_expansion_for_token(
}); ctx,
return process_expansion_for_token( &mut stack,
&mut stack, call_id, derive_call,
); ))
} });
None => Some(adt), if let Some(res) = res {
return res;
} }
Some(adt)
} }
None => { None => {
// Otherwise this could be a derive helper on a variant or field // Otherwise this could be a derive helper on a variant or field
@ -1269,12 +1342,9 @@ impl<'db> SemanticsImpl<'db> {
) )
} }
}?; }?;
if !self.with_ctx(|ctx| ctx.has_derives(InFile::new(expansion, &adt))) {
return None;
}
let attr_name = let attr_name =
attr.path().and_then(|it| it.as_single_name_ref())?.as_name(); attr.path().and_then(|it| it.as_single_name_ref())?.as_name();
// Not an attribute, nor a derive, so it's either an intert attribute or a derive helper // Not an attribute, nor a derive, so it's either an inert attribute or a derive helper
// Try to resolve to a derive helper and downmap // Try to resolve to a derive helper and downmap
let resolver = &token let resolver = &token
.parent() .parent()
@ -1282,7 +1352,7 @@ impl<'db> SemanticsImpl<'db> {
self.analyze_impl(InFile::new(expansion, &parent), None, false) self.analyze_impl(InFile::new(expansion, &parent), None, false)
})? })?
.resolver; .resolver;
let id = self.db.ast_id_map(expansion).ast_id(&adt); let id = db.ast_id_map(expansion).ast_id(&adt);
let helpers = resolver let helpers = resolver
.def_map() .def_map()
.derive_helpers_in_scope(InFile::new(expansion, id))?; .derive_helpers_in_scope(InFile::new(expansion, id))?;
@ -1293,20 +1363,22 @@ impl<'db> SemanticsImpl<'db> {
} }
let mut res = None; let mut res = None;
for (.., derive) in self.with_ctx(|ctx| {
helpers.iter().filter(|(helper, ..)| *helper == attr_name) for (.., derive) in
{ helpers.iter().filter(|(helper, ..)| *helper == attr_name)
// as there may be multiple derives registering the same helper {
// name, we gotta make sure to call this for all of them! // as there may be multiple derives registering the same helper
// FIXME: We need to call `f` for all of them as well though! // name, we gotta make sure to call this for all of them!
res = res.or(process_expansion_for_token(&mut stack, *derive)); // FIXME: We need to call `f` for all of them as well though!
} res = res
res .or(process_expansion_for_token(ctx, &mut stack, *derive));
}
res
})
} }
} }
})() })()
.is_none(); .is_none();
if was_not_remapped { if was_not_remapped {
if let ControlFlow::Break(b) = f(InFile::new(expansion, token), ctx) { if let ControlFlow::Break(b) = f(InFile::new(expansion, token), ctx) {
return Some(b); return Some(b);

View File

@ -108,7 +108,7 @@ use span::FileId;
use stdx::impl_from; use stdx::impl_from;
use syntax::{ use syntax::{
AstNode, AstPtr, SyntaxNode, AstNode, AstPtr, SyntaxNode,
ast::{self, HasName}, ast::{self, HasAttrs, HasName},
}; };
use tt::TextRange; use tt::TextRange;
@ -411,10 +411,24 @@ impl SourceToDefCtx<'_, '_> {
.map(|&(attr_id, call_id, ref ids)| (attr_id, call_id, &**ids)) .map(|&(attr_id, call_id, ref ids)| (attr_id, call_id, &**ids))
} }
pub(super) fn has_derives(&mut self, adt: InFile<&ast::Adt>) -> bool { pub(super) fn file_of_adt_has_derives(&mut self, adt: InFile<&ast::Adt>) -> bool {
self.dyn_map(adt).as_ref().is_some_and(|map| !map[keys::DERIVE_MACRO_CALL].is_empty()) self.dyn_map(adt).as_ref().is_some_and(|map| !map[keys::DERIVE_MACRO_CALL].is_empty())
} }
pub(super) fn derive_macro_calls<'slf>(
&'slf mut self,
adt: InFile<&ast::Adt>,
) -> Option<impl Iterator<Item = (AttrId, MacroCallId, &'slf [Option<MacroCallId>])> + use<'slf>>
{
self.dyn_map(adt).as_ref().map(|&map| {
let dyn_map = &map[keys::DERIVE_MACRO_CALL];
adt.value
.attrs()
.filter_map(move |attr| dyn_map.get(&AstPtr::new(&attr)))
.map(|&(attr_id, call_id, ref ids)| (attr_id, call_id, &**ids))
})
}
fn to_def<Ast: AstNode + 'static, ID: Copy + 'static>( fn to_def<Ast: AstNode + 'static, ID: Copy + 'static>(
&mut self, &mut self,
src: InFile<&Ast>, src: InFile<&Ast>,