Merge pull request #19893 from Veykril/push-wzqsompmnlmx
Enhance renaming to include identifiers that are generated from the original symbol
This commit is contained in:
commit
23f79c10eb
9 changed files with 604 additions and 257 deletions
|
|
@ -21,6 +21,8 @@ smol_str.opt-level = 3
|
|||
text-size.opt-level = 3
|
||||
serde.opt-level = 3
|
||||
salsa.opt-level = 3
|
||||
dissimilar.opt-level = 3
|
||||
|
||||
# This speeds up `cargo xtask dist`.
|
||||
miniz_oxide.opt-level = 3
|
||||
|
||||
|
|
|
|||
|
|
@ -67,8 +67,14 @@ pub mod keys {
|
|||
pub const PROC_MACRO: Key<ast::Fn, ProcMacroId> = 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 DERIVE_MACRO_CALL: Key<ast::Attr, (AttrId, MacroCallId, Box<[Option<MacroCallId>]>)> =
|
||||
Key::new();
|
||||
pub const DERIVE_MACRO_CALL: Key<
|
||||
ast::Attr,
|
||||
(
|
||||
AttrId,
|
||||
/* derive() */ MacroCallId,
|
||||
/* actual derive macros */ Box<[Option<MacroCallId>]>,
|
||||
),
|
||||
> = Key::new();
|
||||
|
||||
/// XXX: AST Nodes and SyntaxNodes have identity equality semantics: nodes are
|
||||
/// equal if they point to exactly the same object.
|
||||
|
|
|
|||
|
|
@ -222,6 +222,21 @@ impl<DB: HirDatabase> Semantics<'_, DB> {
|
|||
self.imp.descend_node_at_offset(node, offset).filter_map(|mut it| it.find_map(N::cast))
|
||||
}
|
||||
|
||||
// FIXME: Rethink this API
|
||||
pub fn find_namelike_at_offset_with_descend<'slf>(
|
||||
&'slf self,
|
||||
node: &SyntaxNode,
|
||||
offset: TextSize,
|
||||
) -> impl Iterator<Item = ast::NameLike> + 'slf {
|
||||
node.token_at_offset(offset)
|
||||
.map(move |token| self.descend_into_macros_no_opaque(token))
|
||||
.map(|descendants| descendants.into_iter().filter_map(move |it| it.value.parent()))
|
||||
// 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.text_range().len().lt(&right.text_range().len()))
|
||||
.filter_map(ast::NameLike::cast)
|
||||
}
|
||||
|
||||
pub fn resolve_range_pat(&self, range_pat: &ast::RangePat) -> Option<Struct> {
|
||||
self.imp.resolve_range_pat(range_pat).map(Struct::from)
|
||||
}
|
||||
|
|
@ -535,7 +550,7 @@ impl<'db> SemanticsImpl<'db> {
|
|||
}
|
||||
|
||||
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)>> {
|
||||
|
|
@ -644,7 +659,7 @@ impl<'db> SemanticsImpl<'db> {
|
|||
|
||||
/// Checks if renaming `renamed` to `new_name` may introduce conflicts with other locals,
|
||||
/// and returns the conflicting locals.
|
||||
pub fn rename_conflicts(&self, to_be_renamed: &Local, new_name: &str) -> Vec<Local> {
|
||||
pub fn rename_conflicts(&self, to_be_renamed: &Local, new_name: &Name) -> Vec<Local> {
|
||||
let body = self.db.body(to_be_renamed.parent);
|
||||
let resolver = to_be_renamed.parent.resolver(self.db);
|
||||
let starting_expr =
|
||||
|
|
@ -653,7 +668,7 @@ impl<'db> SemanticsImpl<'db> {
|
|||
body: &body,
|
||||
conflicts: FxHashSet::default(),
|
||||
db: self.db,
|
||||
new_name: Symbol::intern(new_name),
|
||||
new_name: new_name.symbol().clone(),
|
||||
old_name: to_be_renamed.name(self.db).symbol().clone(),
|
||||
owner: to_be_renamed.parent,
|
||||
to_be_renamed: to_be_renamed.binding_id,
|
||||
|
|
@ -879,6 +894,7 @@ impl<'db> SemanticsImpl<'db> {
|
|||
// node is just the token, so descend the token
|
||||
self.descend_into_macros_impl(
|
||||
InFile::new(file.file_id, first),
|
||||
false,
|
||||
&mut |InFile { value, .. }, _ctx| {
|
||||
if let Some(node) = value
|
||||
.parent_ancestors()
|
||||
|
|
@ -893,14 +909,19 @@ impl<'db> SemanticsImpl<'db> {
|
|||
} else {
|
||||
// Descend first and last token, then zip them to look for the node they belong to
|
||||
let mut scratch: SmallVec<[_; 1]> = smallvec![];
|
||||
self.descend_into_macros_impl(InFile::new(file.file_id, first), &mut |token, _ctx| {
|
||||
scratch.push(token);
|
||||
CONTINUE_NO_BREAKS
|
||||
});
|
||||
self.descend_into_macros_impl(
|
||||
InFile::new(file.file_id, first),
|
||||
false,
|
||||
&mut |token, _ctx| {
|
||||
scratch.push(token);
|
||||
CONTINUE_NO_BREAKS
|
||||
},
|
||||
);
|
||||
|
||||
let mut scratch = scratch.into_iter();
|
||||
self.descend_into_macros_impl(
|
||||
InFile::new(file.file_id, last),
|
||||
false,
|
||||
&mut |InFile { value: last, file_id: last_fid }, _ctx| {
|
||||
if let Some(InFile { value: first, file_id: first_fid }) = scratch.next() {
|
||||
if first_fid == last_fid {
|
||||
|
|
@ -952,7 +973,7 @@ impl<'db> SemanticsImpl<'db> {
|
|||
ast::Item::Union(it) => it.into(),
|
||||
_ => return false,
|
||||
};
|
||||
ctx.has_derives(token.with_value(&adt))
|
||||
ctx.file_of_adt_has_derives(token.with_value(&adt))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -962,7 +983,7 @@ impl<'db> SemanticsImpl<'db> {
|
|||
token: SyntaxToken,
|
||||
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);
|
||||
CONTINUE_NO_BREAKS
|
||||
});
|
||||
|
|
@ -970,10 +991,14 @@ impl<'db> SemanticsImpl<'db> {
|
|||
|
||||
pub fn descend_into_macros(&self, token: SyntaxToken) -> SmallVec<[SyntaxToken; 1]> {
|
||||
let mut res = smallvec![];
|
||||
self.descend_into_macros_impl(self.wrap_token_infile(token.clone()), &mut |t, _ctx| {
|
||||
res.push(t.value);
|
||||
CONTINUE_NO_BREAKS
|
||||
});
|
||||
self.descend_into_macros_impl(
|
||||
self.wrap_token_infile(token.clone()),
|
||||
false,
|
||||
&mut |t, _ctx| {
|
||||
res.push(t.value);
|
||||
CONTINUE_NO_BREAKS
|
||||
},
|
||||
);
|
||||
if res.is_empty() {
|
||||
res.push(token);
|
||||
}
|
||||
|
|
@ -986,7 +1011,7 @@ impl<'db> SemanticsImpl<'db> {
|
|||
) -> SmallVec<[InFile<SyntaxToken>; 1]> {
|
||||
let mut res = smallvec![];
|
||||
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) {
|
||||
// Don't descend into opaque contexts
|
||||
res.push(t);
|
||||
|
|
@ -1004,7 +1029,7 @@ impl<'db> SemanticsImpl<'db> {
|
|||
token: InFile<SyntaxToken>,
|
||||
mut cb: impl FnMut(InFile<SyntaxToken>, SyntaxContext) -> ControlFlow<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
|
||||
|
|
@ -1077,41 +1102,41 @@ impl<'db> SemanticsImpl<'db> {
|
|||
fn descend_into_macros_impl<T>(
|
||||
&self,
|
||||
InFile { value: token, file_id }: InFile<SyntaxToken>,
|
||||
always_descend_into_derives: bool,
|
||||
f: &mut dyn FnMut(InFile<SyntaxToken>, SyntaxContext) -> ControlFlow<T>,
|
||||
) -> Option<T> {
|
||||
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
|
||||
let process_expansion_for_token = |stack: &mut Vec<_>, macro_file| {
|
||||
let InMacroFile { file_id, value: mapped_tokens } = self.with_ctx(|ctx| {
|
||||
Some(
|
||||
ctx.cache
|
||||
.get_or_insert_expansion(ctx.db, macro_file)
|
||||
.map_range_down(span)?
|
||||
.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(());
|
||||
// requeue the tokens we got from mapping our current token down
|
||||
stack.push((HirFileId::from(file_id), mapped_tokens));
|
||||
res
|
||||
};
|
||||
let process_expansion_for_token =
|
||||
|ctx: &mut SourceToDefCtx<'_, '_>, stack: &mut Vec<_>, macro_file| {
|
||||
let InMacroFile { file_id, value: mapped_tokens } = ctx
|
||||
.cache
|
||||
.get_or_insert_expansion(ctx.db, macro_file)
|
||||
.map_range_down(span)?
|
||||
.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(());
|
||||
// requeue the tokens we got from mapping our current token down
|
||||
stack.push((HirFileId::from(file_id), mapped_tokens));
|
||||
res
|
||||
};
|
||||
|
||||
// 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
|
||||
// the tokens themselves aren't that interesting as the span that is being used to map
|
||||
// things down never changes.
|
||||
let mut stack: Vec<(_, SmallVec<[_; 2]>)> = vec![];
|
||||
let include = file_id.file_id().and_then(|file_id| {
|
||||
self.s2d_cache.borrow_mut().get_or_insert_include_for(self.db, file_id)
|
||||
});
|
||||
let include = file_id
|
||||
.file_id()
|
||||
.and_then(|file_id| self.s2d_cache.borrow_mut().get_or_insert_include_for(db, file_id));
|
||||
match include {
|
||||
Some(include) => {
|
||||
// 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 => {
|
||||
stack.push((file_id, smallvec![(token, span.ctx)]));
|
||||
|
|
@ -1133,62 +1158,120 @@ impl<'db> SemanticsImpl<'db> {
|
|||
tokens.reverse();
|
||||
while let Some((token, ctx)) = tokens.pop() {
|
||||
let was_not_remapped = (|| {
|
||||
// First expand into attribute invocations
|
||||
let containing_attribute_macro_call = self.with_ctx(|ctx| {
|
||||
token.parent_ancestors().filter_map(ast::Item::cast).find_map(|item| {
|
||||
// Don't force populate the dyn cache for items that don't have an attribute anyways
|
||||
item.attrs().next()?;
|
||||
Some((ctx.item_to_macro_call(InFile::new(expansion, &item))?, item))
|
||||
})
|
||||
});
|
||||
if let Some((call_id, item)) = containing_attribute_macro_call {
|
||||
let attr_id = match self.db.lookup_intern_macro_call(call_id).kind {
|
||||
hir_expand::MacroCallKind::Attr { 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(),
|
||||
// First expand into attribute invocations, this is required to be handled
|
||||
// upfront as any other macro call within will not semantically resolve unless
|
||||
// also descended.
|
||||
let res = self.with_ctx(|ctx| {
|
||||
token
|
||||
.parent_ancestors()
|
||||
.filter_map(ast::Item::cast)
|
||||
// FIXME: This might work incorrectly when we have a derive, followed by
|
||||
// an attribute on an item, like:
|
||||
// ```
|
||||
// #[derive(Debug$0)]
|
||||
// #[my_attr]
|
||||
// struct MyStruct;
|
||||
// ```
|
||||
// here we should not consider the attribute at all, as our cursor
|
||||
// technically lies outside of its expansion
|
||||
.find_map(|item| {
|
||||
// Don't force populate the dyn cache for items that don't have an attribute anyways
|
||||
item.attrs().next()?;
|
||||
ctx.item_to_macro_call(InFile::new(expansion, &item))
|
||||
.zip(Some(item))
|
||||
})
|
||||
.unwrap_or_else(|| text_range.start());
|
||||
let text_range = TextRange::new(start, text_range.end());
|
||||
filter_duplicates(tokens, text_range);
|
||||
return process_expansion_for_token(&mut stack, call_id);
|
||||
.map(|(call_id, item)| {
|
||||
let attr_id = match db.lookup_intern_macro_call(call_id).kind {
|
||||
hir_expand::MacroCallKind::Attr {
|
||||
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
|
||||
// secondary attribute inputs
|
||||
let tt = token
|
||||
.parent_ancestors()
|
||||
.map_while(Either::<ast::TokenTree, ast::Meta>::cast)
|
||||
.last()?;
|
||||
|
||||
match tt {
|
||||
// function-like macro call
|
||||
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) {
|
||||
return None;
|
||||
}
|
||||
if tt.right_delimiter_token().map_or(false, |it| it == token) {
|
||||
return None;
|
||||
}
|
||||
let macro_call = tt.syntax().parent().and_then(ast::MacroCall::cast)?;
|
||||
let mcall = InFile::new(expansion, macro_call);
|
||||
let file_id = match m_cache.get(&mcall) {
|
||||
Some(&it) => it,
|
||||
|
|
@ -1201,13 +1284,16 @@ impl<'db> SemanticsImpl<'db> {
|
|||
let text_range = tt.syntax().text_range();
|
||||
filter_duplicates(tokens, text_range);
|
||||
|
||||
process_expansion_for_token(&mut stack, file_id).or(file_id
|
||||
.eager_arg(self.db)
|
||||
.and_then(|arg| {
|
||||
// also descend into eager expansions
|
||||
process_expansion_for_token(&mut stack, arg)
|
||||
}))
|
||||
self.with_ctx(|ctx| {
|
||||
process_expansion_for_token(ctx, &mut stack, file_id).or(file_id
|
||||
.eager_arg(db)
|
||||
.and_then(|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
|
||||
Either::Right(meta) => {
|
||||
// attribute we failed expansion for earlier, this might be a derive invocation
|
||||
|
|
@ -1216,31 +1302,33 @@ impl<'db> SemanticsImpl<'db> {
|
|||
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| {
|
||||
let res = 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(expansion, &adt),
|
||||
InFile::new(expansion, attr.clone()),
|
||||
)
|
||||
.map(|(_, call_id, _)| call_id)
|
||||
});
|
||||
let derive_call = ctx
|
||||
.attr_to_derive_macro_call(
|
||||
InFile::new(expansion, &adt),
|
||||
InFile::new(expansion, attr.clone()),
|
||||
)?
|
||||
.1;
|
||||
|
||||
match derive_call {
|
||||
Some(call_id) => {
|
||||
// resolved to a derive
|
||||
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, call_id,
|
||||
);
|
||||
}
|
||||
None => Some(adt),
|
||||
// resolved to a derive
|
||||
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())
|
||||
});
|
||||
Some(process_expansion_for_token(
|
||||
ctx,
|
||||
&mut stack,
|
||||
derive_call,
|
||||
))
|
||||
});
|
||||
if let Some(res) = res {
|
||||
return res;
|
||||
}
|
||||
Some(adt)
|
||||
}
|
||||
None => {
|
||||
// Otherwise this could be a derive helper on a variant or field
|
||||
|
|
@ -1254,12 +1342,9 @@ impl<'db> SemanticsImpl<'db> {
|
|||
)
|
||||
}
|
||||
}?;
|
||||
if !self.with_ctx(|ctx| ctx.has_derives(InFile::new(expansion, &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 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
|
||||
let resolver = &token
|
||||
.parent()
|
||||
|
|
@ -1267,7 +1352,7 @@ impl<'db> SemanticsImpl<'db> {
|
|||
self.analyze_impl(InFile::new(expansion, &parent), None, false)
|
||||
})?
|
||||
.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
|
||||
.def_map()
|
||||
.derive_helpers_in_scope(InFile::new(expansion, id))?;
|
||||
|
|
@ -1278,20 +1363,22 @@ impl<'db> SemanticsImpl<'db> {
|
|||
}
|
||||
|
||||
let mut res = None;
|
||||
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!
|
||||
// FIXME: We need to call `f` for all of them as well though!
|
||||
res = res.or(process_expansion_for_token(&mut stack, *derive));
|
||||
}
|
||||
res
|
||||
self.with_ctx(|ctx| {
|
||||
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!
|
||||
// FIXME: We need to call `f` for all of them as well though!
|
||||
res = res
|
||||
.or(process_expansion_for_token(ctx, &mut stack, *derive));
|
||||
}
|
||||
res
|
||||
})
|
||||
}
|
||||
}
|
||||
})()
|
||||
.is_none();
|
||||
|
||||
if was_not_remapped {
|
||||
if let ControlFlow::Break(b) = f(InFile::new(expansion, token), ctx) {
|
||||
return Some(b);
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ use span::FileId;
|
|||
use stdx::impl_from;
|
||||
use syntax::{
|
||||
AstNode, AstPtr, SyntaxNode,
|
||||
ast::{self, HasName},
|
||||
ast::{self, HasAttrs, HasName},
|
||||
};
|
||||
use tt::TextRange;
|
||||
|
||||
|
|
@ -411,10 +411,24 @@ impl SourceToDefCtx<'_, '_> {
|
|||
.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())
|
||||
}
|
||||
|
||||
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>(
|
||||
&mut self,
|
||||
src: InFile<&Ast>,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
//!
|
||||
//! The correct behavior in such cases is probably to show a dialog to the user.
|
||||
//! Our current behavior is ¯\_(ツ)_/¯.
|
||||
use std::fmt;
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
use crate::{
|
||||
source_change::ChangeAnnotation,
|
||||
|
|
@ -28,13 +28,12 @@ use crate::{
|
|||
};
|
||||
use base_db::AnchoredPathBuf;
|
||||
use either::Either;
|
||||
use hir::{EditionedFileId, FieldSource, FileRange, InFile, ModuleSource, Semantics};
|
||||
use hir::{FieldSource, FileRange, InFile, ModuleSource, Name, Semantics, sym};
|
||||
use span::{Edition, FileId, SyntaxContext};
|
||||
use stdx::{TupleExt, never};
|
||||
use syntax::{
|
||||
AstNode, SyntaxKind, T, TextRange,
|
||||
ast::{self, HasName},
|
||||
utils::is_raw_identifier,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
|
|
@ -70,26 +69,33 @@ macro_rules! _bail {
|
|||
}
|
||||
pub use _bail as bail;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum RenameDefinition {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
impl Definition {
|
||||
pub fn rename(
|
||||
&self,
|
||||
sema: &Semantics<'_, RootDatabase>,
|
||||
new_name: &str,
|
||||
rename_definition: RenameDefinition,
|
||||
) -> Result<SourceChange> {
|
||||
// We append `r#` if needed.
|
||||
let new_name = new_name.trim_start_matches("r#");
|
||||
|
||||
// self.krate() returns None if
|
||||
// self is a built-in attr, built-in type or tool module.
|
||||
// it is not allowed for these defs to be renamed.
|
||||
// cases where self.krate() is None is handled below.
|
||||
if let Some(krate) = self.krate(sema.db) {
|
||||
let edition = if let Some(krate) = self.krate(sema.db) {
|
||||
// Can we not rename non-local items?
|
||||
// Then bail if non-local
|
||||
if !krate.origin(sema.db).is_local() {
|
||||
bail!("Cannot rename a non-local definition")
|
||||
}
|
||||
}
|
||||
krate.edition(sema.db)
|
||||
} else {
|
||||
Edition::LATEST
|
||||
};
|
||||
|
||||
match *self {
|
||||
Definition::Module(module) => rename_mod(sema, module, new_name),
|
||||
|
|
@ -103,8 +109,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, rename_definition, edition)
|
||||
}
|
||||
def => rename_reference(sema, def, new_name, rename_definition, edition),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -237,10 +245,6 @@ fn rename_mod(
|
|||
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() {
|
||||
|
|
@ -248,6 +252,14 @@ fn rename_mod(
|
|||
}
|
||||
|
||||
let InFile { file_id, value: def_source } = module.definition_source(sema.db);
|
||||
let edition = file_id.edition(sema.db);
|
||||
let (new_name, kind) = IdentifierKind::classify(edition, new_name)?;
|
||||
if kind != IdentifierKind::Ident {
|
||||
bail!(
|
||||
"Invalid name `{0}`: cannot rename module to {0}",
|
||||
new_name.display(sema.db, edition)
|
||||
);
|
||||
}
|
||||
if let ModuleSource::SourceFile(..) = def_source {
|
||||
let anchor = file_id.original_file(sema.db).file_id(sema.db);
|
||||
|
||||
|
|
@ -256,7 +268,7 @@ fn rename_mod(
|
|||
|
||||
// Module exists in a named file
|
||||
if !is_mod_rs {
|
||||
let path = format!("{new_name}.rs");
|
||||
let path = format!("{}.rs", new_name.as_str());
|
||||
let dst = AnchoredPathBuf { anchor, path };
|
||||
source_change.push_file_system_edit(FileSystemEdit::MoveFile { src: anchor, dst })
|
||||
}
|
||||
|
|
@ -267,11 +279,11 @@ fn rename_mod(
|
|||
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.as_str()), format!("../{new_name}")))
|
||||
Some((format!("../{}", mod_name.as_str()), format!("../{}", new_name.as_str())))
|
||||
}
|
||||
// The anchor is on the same level as target dir
|
||||
(false, true, Some(mod_name)) => {
|
||||
Some((mod_name.as_str().to_owned(), new_name.to_owned()))
|
||||
Some((mod_name.as_str().to_owned(), new_name.as_str().to_owned()))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
|
@ -296,11 +308,7 @@ fn rename_mod(
|
|||
.original_file_range_opt(sema.db)
|
||||
.map(TupleExt::head)
|
||||
{
|
||||
let new_name = if is_raw_identifier(new_name, file_id.edition(sema.db)) {
|
||||
format!("r#{new_name}")
|
||||
} else {
|
||||
new_name.to_owned()
|
||||
};
|
||||
let new_name = new_name.display(sema.db, edition).to_string();
|
||||
source_change.insert_source_edit(
|
||||
file_id.file_id(sema.db),
|
||||
TextEdit::replace(file_range.range, new_name),
|
||||
|
|
@ -314,9 +322,10 @@ fn rename_mod(
|
|||
let def = Definition::Module(module);
|
||||
let usages = def.usages(sema).all();
|
||||
let ref_edits = usages.iter().map(|(file_id, references)| {
|
||||
let edition = file_id.edition(sema.db);
|
||||
(
|
||||
file_id.file_id(sema.db),
|
||||
source_edit_from_references(references, def, new_name, file_id.edition(sema.db)),
|
||||
source_edit_from_references(sema.db, references, def, &new_name, edition),
|
||||
)
|
||||
});
|
||||
source_change.extend(ref_edits);
|
||||
|
|
@ -328,8 +337,10 @@ fn rename_reference(
|
|||
sema: &Semantics<'_, RootDatabase>,
|
||||
def: Definition,
|
||||
new_name: &str,
|
||||
rename_definition: RenameDefinition,
|
||||
edition: Edition,
|
||||
) -> Result<SourceChange> {
|
||||
let ident_kind = IdentifierKind::classify(new_name)?;
|
||||
let (mut new_name, ident_kind) = IdentifierKind::classify(edition, new_name)?;
|
||||
|
||||
if matches!(
|
||||
def,
|
||||
|
|
@ -337,18 +348,34 @@ fn rename_reference(
|
|||
) {
|
||||
match ident_kind {
|
||||
IdentifierKind::Underscore => {
|
||||
bail!("Invalid name `{}`: not a lifetime identifier", new_name);
|
||||
bail!(
|
||||
"Invalid name `{}`: not a lifetime identifier",
|
||||
new_name.display(sema.db, edition)
|
||||
);
|
||||
}
|
||||
_ => cov_mark::hit!(rename_lifetime),
|
||||
IdentifierKind::Ident => {
|
||||
new_name = Name::new_lifetime(&format!("'{}", new_name.as_str()))
|
||||
}
|
||||
IdentifierKind::Lifetime => (),
|
||||
IdentifierKind::LowercaseSelf => bail!(
|
||||
"Invalid name `{}`: not a lifetime identifier",
|
||||
new_name.display(sema.db, edition)
|
||||
),
|
||||
}
|
||||
} else {
|
||||
match ident_kind {
|
||||
IdentifierKind::Lifetime => {
|
||||
cov_mark::hit!(rename_not_an_ident_ref);
|
||||
bail!("Invalid name `{}`: not an identifier", new_name);
|
||||
bail!("Invalid name `{}`: not an identifier", new_name.display(sema.db, edition));
|
||||
}
|
||||
IdentifierKind::Ident => cov_mark::hit!(rename_non_local),
|
||||
IdentifierKind::Underscore => (),
|
||||
IdentifierKind::LowercaseSelf => {
|
||||
bail!(
|
||||
"Invalid name `{}`: cannot rename to `self`",
|
||||
new_name.display(sema.db, edition)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -361,30 +388,29 @@ fn rename_reference(
|
|||
}
|
||||
let mut source_change = SourceChange::default();
|
||||
source_change.extend(usages.iter().map(|(file_id, references)| {
|
||||
let edition = file_id.edition(sema.db);
|
||||
(
|
||||
file_id.file_id(sema.db),
|
||||
source_edit_from_references(references, def, new_name, file_id.edition(sema.db)),
|
||||
source_edit_from_references(sema.db, references, def, &new_name, edition),
|
||||
)
|
||||
}));
|
||||
|
||||
// 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 {
|
||||
// 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);
|
||||
}
|
||||
Ok(source_change)
|
||||
}
|
||||
|
||||
pub fn source_edit_from_references(
|
||||
db: &RootDatabase,
|
||||
references: &[FileReference],
|
||||
def: Definition,
|
||||
new_name: &str,
|
||||
new_name: &Name,
|
||||
edition: Edition,
|
||||
) -> TextEdit {
|
||||
let new_name = if is_raw_identifier(new_name, edition) {
|
||||
format!("r#{new_name}")
|
||||
} else {
|
||||
new_name.to_owned()
|
||||
};
|
||||
let name_display = new_name.display(db, edition);
|
||||
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();
|
||||
|
|
@ -395,23 +421,15 @@ pub fn source_edit_from_references(
|
|||
// to make special rewrites like shorthand syntax and such, so just rename the node in
|
||||
// the macro input
|
||||
FileReferenceNode::NameRef(name_ref) if name_range == range => {
|
||||
source_edit_from_name_ref(&mut edit, name_ref, &new_name, def)
|
||||
source_edit_from_name_ref(&mut edit, name_ref, &name_display, def)
|
||||
}
|
||||
FileReferenceNode::Name(name) if name_range == range => {
|
||||
source_edit_from_name(&mut edit, name, &new_name)
|
||||
source_edit_from_name(&mut edit, name, &name_display)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !has_emitted_edit && !edited_ranges.contains(&range.start()) {
|
||||
let (range, new_name) = match name {
|
||||
FileReferenceNode::Lifetime(_) => (
|
||||
TextRange::new(range.start() + syntax::TextSize::from(1), range.end()),
|
||||
new_name.strip_prefix('\'').unwrap_or(&new_name).to_owned(),
|
||||
),
|
||||
_ => (range, new_name.to_owned()),
|
||||
};
|
||||
|
||||
edit.replace(range, new_name);
|
||||
edit.replace(range, name_display.to_string());
|
||||
edited_ranges.push(range.start());
|
||||
}
|
||||
}
|
||||
|
|
@ -419,7 +437,11 @@ pub fn source_edit_from_references(
|
|||
edit.finish()
|
||||
}
|
||||
|
||||
fn source_edit_from_name(edit: &mut TextEditBuilder, name: &ast::Name, new_name: &str) -> bool {
|
||||
fn source_edit_from_name(
|
||||
edit: &mut TextEditBuilder,
|
||||
name: &ast::Name,
|
||||
new_name: &dyn Display,
|
||||
) -> 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);
|
||||
|
|
@ -439,7 +461,7 @@ fn source_edit_from_name(edit: &mut TextEditBuilder, name: &ast::Name, new_name:
|
|||
fn source_edit_from_name_ref(
|
||||
edit: &mut TextEditBuilder,
|
||||
name_ref: &ast::NameRef,
|
||||
new_name: &str,
|
||||
new_name: &dyn Display,
|
||||
def: Definition,
|
||||
) -> bool {
|
||||
if name_ref.super_token().is_some() {
|
||||
|
|
@ -452,6 +474,7 @@ fn source_edit_from_name_ref(
|
|||
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)) => {
|
||||
let new_name = new_name.to_string();
|
||||
if field_name == name_ref {
|
||||
if init.text() == new_name {
|
||||
cov_mark::hit!(test_rename_field_put_init_shorthand);
|
||||
|
|
@ -507,6 +530,7 @@ fn source_edit_from_name_ref(
|
|||
{
|
||||
// field name is being renamed
|
||||
if let Some(name) = pat.name() {
|
||||
let new_name = new_name.to_string();
|
||||
if name.text() == new_name {
|
||||
cov_mark::hit!(test_rename_field_put_init_shorthand_pat);
|
||||
// Foo { field: ref mut local } -> Foo { ref mut field }
|
||||
|
|
@ -518,7 +542,7 @@ fn source_edit_from_name_ref(
|
|||
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_owned());
|
||||
edit.replace(name.syntax().text_range(), new_name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -532,16 +556,9 @@ fn source_edit_from_name_ref(
|
|||
fn source_edit_from_def(
|
||||
sema: &Semantics<'_, RootDatabase>,
|
||||
def: Definition,
|
||||
new_name: &str,
|
||||
new_name: &Name,
|
||||
source_change: &mut SourceChange,
|
||||
) -> Result<(FileId, TextEdit)> {
|
||||
let new_name_edition_aware = |new_name: &str, file_id: EditionedFileId| {
|
||||
if is_raw_identifier(new_name, file_id.edition(sema.db)) {
|
||||
format!("r#{new_name}")
|
||||
} else {
|
||||
new_name.to_owned()
|
||||
}
|
||||
};
|
||||
let mut edit = TextEdit::builder();
|
||||
if let Definition::Local(local) = def {
|
||||
let mut file_id = None;
|
||||
|
|
@ -573,7 +590,10 @@ fn source_edit_from_def(
|
|||
{
|
||||
Some(FileRange { file_id: file_id2, range }) => {
|
||||
file_id = Some(file_id2);
|
||||
edit.replace(range, new_name_edition_aware(new_name, file_id2));
|
||||
edit.replace(
|
||||
range,
|
||||
new_name.display(sema.db, file_id2.edition(sema.db)).to_string(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
None => {
|
||||
|
|
@ -587,7 +607,7 @@ fn source_edit_from_def(
|
|||
// special cases required for renaming fields/locals in Record patterns
|
||||
if let Some(pat_field) = pat.syntax().parent().and_then(ast::RecordPatField::cast) {
|
||||
if let Some(name_ref) = pat_field.name_ref() {
|
||||
if new_name == name_ref.text().as_str().trim_start_matches("r#")
|
||||
if new_name.as_str() == name_ref.text().as_str().trim_start_matches("r#")
|
||||
&& pat.at_token().is_none()
|
||||
{
|
||||
// Foo { field: ref mut local } -> Foo { ref mut field }
|
||||
|
|
@ -607,7 +627,9 @@ fn source_edit_from_def(
|
|||
// ^^^^^ replace this with `new_name`
|
||||
edit.replace(
|
||||
name_range,
|
||||
new_name_edition_aware(new_name, source.file_id),
|
||||
new_name
|
||||
.display(sema.db, source.file_id.edition(sema.db))
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -618,10 +640,16 @@ fn source_edit_from_def(
|
|||
pat.syntax().text_range().start(),
|
||||
format!("{}: ", pat_field.field_name().unwrap()),
|
||||
);
|
||||
edit.replace(name_range, new_name_edition_aware(new_name, source.file_id));
|
||||
edit.replace(
|
||||
name_range,
|
||||
new_name.display(sema.db, source.file_id.edition(sema.db)).to_string(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
edit.replace(name_range, new_name_edition_aware(new_name, source.file_id));
|
||||
edit.replace(
|
||||
name_range,
|
||||
new_name.display(sema.db, source.file_id.edition(sema.db)).to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -639,16 +667,13 @@ fn source_edit_from_def(
|
|||
.range_for_rename(sema)
|
||||
.ok_or_else(|| format_err!("No identifier available to rename"))?;
|
||||
let (range, new_name) = match def {
|
||||
Definition::GenericParam(hir::GenericParam::LifetimeParam(_)) | Definition::Label(_) => (
|
||||
TextRange::new(range.start() + syntax::TextSize::from(1), range.end()),
|
||||
new_name.strip_prefix('\'').unwrap_or(new_name).to_owned(),
|
||||
Definition::ExternCrateDecl(decl) if decl.alias(sema.db).is_none() => (
|
||||
TextRange::empty(range.end()),
|
||||
format!(" as {}", new_name.display(sema.db, file_id.edition(sema.db)),),
|
||||
),
|
||||
Definition::ExternCrateDecl(decl) if decl.alias(sema.db).is_none() => {
|
||||
(TextRange::empty(range.end()), format!(" as {new_name}"))
|
||||
}
|
||||
_ => (range, new_name.to_owned()),
|
||||
_ => (range, new_name.display(sema.db, file_id.edition(sema.db)).to_string()),
|
||||
};
|
||||
edit.replace(range, new_name_edition_aware(&new_name, file_id));
|
||||
edit.replace(range, new_name);
|
||||
Ok((file_id.file_id(sema.db), edit.finish()))
|
||||
}
|
||||
|
||||
|
|
@ -657,26 +682,27 @@ pub enum IdentifierKind {
|
|||
Ident,
|
||||
Lifetime,
|
||||
Underscore,
|
||||
LowercaseSelf,
|
||||
}
|
||||
|
||||
impl IdentifierKind {
|
||||
pub fn classify(new_name: &str) -> Result<IdentifierKind> {
|
||||
let new_name = new_name.trim_start_matches("r#");
|
||||
match parser::LexedStr::single_token(Edition::LATEST, new_name) {
|
||||
pub fn classify(edition: Edition, new_name: &str) -> Result<(Name, IdentifierKind)> {
|
||||
match parser::LexedStr::single_token(edition, new_name) {
|
||||
Some(res) => match res {
|
||||
(SyntaxKind::IDENT, _) => {
|
||||
if let Some(inner) = new_name.strip_prefix("r#") {
|
||||
if matches!(inner, "self" | "crate" | "super" | "Self") {
|
||||
bail!("Invalid name: `{}` cannot be a raw identifier", inner);
|
||||
}
|
||||
}
|
||||
Ok(IdentifierKind::Ident)
|
||||
(SyntaxKind::IDENT, _) => Ok((Name::new_root(new_name), IdentifierKind::Ident)),
|
||||
(T![_], _) => {
|
||||
Ok((Name::new_symbol_root(sym::underscore), IdentifierKind::Underscore))
|
||||
}
|
||||
(T![_], _) => Ok(IdentifierKind::Underscore),
|
||||
(SyntaxKind::LIFETIME_IDENT, _) if new_name != "'static" && new_name != "'_" => {
|
||||
Ok(IdentifierKind::Lifetime)
|
||||
Ok((Name::new_lifetime(new_name), IdentifierKind::Lifetime))
|
||||
}
|
||||
_ if is_raw_identifier(new_name, Edition::LATEST) => Ok(IdentifierKind::Ident),
|
||||
_ if SyntaxKind::from_keyword(new_name, edition).is_some() => match new_name {
|
||||
"self" => Ok((Name::new_root(new_name), IdentifierKind::LowercaseSelf)),
|
||||
"crate" | "super" | "Self" => {
|
||||
bail!("Invalid name `{}`: cannot rename to a keyword", new_name)
|
||||
}
|
||||
_ => Ok((Name::new_root(new_name), IdentifierKind::Ident)),
|
||||
},
|
||||
(_, Some(syntax_error)) => bail!("Invalid name `{}`: {}", new_name, syntax_error),
|
||||
(_, None) => bail!("Invalid name `{}`: not an identifier", new_name),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<Vec<Ass
|
|||
let label = format!("Rename to {}", d.suggested_text);
|
||||
let mut res = unresolved_fix("change_case", &label, frange.range);
|
||||
if ctx.resolve.should_resolve(&res.id) {
|
||||
let source_change = def.rename(&ctx.sema, &d.suggested_text);
|
||||
let source_change = def.rename(&ctx.sema, &d.suggested_text, RenameDefinition::Yes);
|
||||
res.source_change = Some(source_change.ok().unwrap_or_default());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@
|
|||
//! tests. This module also implements a couple of magic tricks, like renaming
|
||||
//! `self` and to `self` (to switch between associated function and method).
|
||||
|
||||
use hir::{AsAssocItem, InFile, Semantics};
|
||||
use hir::{AsAssocItem, InFile, Name, Semantics, sym};
|
||||
use ide_db::{
|
||||
FileId, FileRange, RootDatabase,
|
||||
defs::{Definition, NameClass, NameRefClass},
|
||||
rename::{IdentifierKind, bail, format_err, source_edit_from_references},
|
||||
rename::{IdentifierKind, RenameDefinition, bail, format_err, source_edit_from_references},
|
||||
source_change::SourceChangeBuilder,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
|
|
@ -33,8 +33,8 @@ pub(crate) fn prepare_rename(
|
|||
let source_file = sema.parse_guess_edition(position.file_id);
|
||||
let syntax = source_file.syntax();
|
||||
|
||||
let res = find_definitions(&sema, syntax, position)?
|
||||
.map(|(frange, kind, def)| {
|
||||
let res = find_definitions(&sema, syntax, position, &Name::new_symbol_root(sym::underscore))?
|
||||
.map(|(frange, kind, def, _, _)| {
|
||||
// ensure all ranges are valid
|
||||
|
||||
if def.range_for_rename(&sema).is_none() {
|
||||
|
|
@ -88,22 +88,28 @@ pub(crate) fn rename(
|
|||
let source_file = sema.parse(file_id);
|
||||
let syntax = source_file.syntax();
|
||||
|
||||
let defs = find_definitions(&sema, syntax, position)?;
|
||||
let alias_fallback = alias_fallback(syntax, position, new_name);
|
||||
let edition = file_id.edition(db);
|
||||
let (new_name, kind) = IdentifierKind::classify(edition, new_name)?;
|
||||
|
||||
let defs = find_definitions(&sema, syntax, position, &new_name)?;
|
||||
let alias_fallback =
|
||||
alias_fallback(syntax, position, &new_name.display(db, edition).to_string());
|
||||
|
||||
let ops: RenameResult<Vec<SourceChange>> = match alias_fallback {
|
||||
Some(_) => defs
|
||||
// FIXME: This can use the `ide_db::rename_reference` (or def.rename) method once we can
|
||||
// properly find "direct" usages/references.
|
||||
.map(|(.., def)| {
|
||||
match IdentifierKind::classify(new_name)? {
|
||||
.map(|(.., def, new_name, _)| {
|
||||
match kind {
|
||||
IdentifierKind::Ident => (),
|
||||
IdentifierKind::Lifetime => {
|
||||
bail!("Cannot alias reference to a lifetime identifier")
|
||||
}
|
||||
IdentifierKind::Underscore => bail!("Cannot alias reference to `_`"),
|
||||
IdentifierKind::LowercaseSelf => {
|
||||
bail!("Cannot rename alias reference to `self`")
|
||||
}
|
||||
};
|
||||
|
||||
let mut usages = def.usages(&sema).all();
|
||||
|
||||
// FIXME: hack - removes the usage that triggered this rename operation.
|
||||
|
|
@ -120,7 +126,7 @@ pub(crate) fn rename(
|
|||
source_change.extend(usages.references.get_mut(&file_id).iter().map(|refs| {
|
||||
(
|
||||
position.file_id,
|
||||
source_edit_from_references(refs, def, new_name, file_id.edition(db)),
|
||||
source_edit_from_references(db, refs, def, &new_name, edition),
|
||||
)
|
||||
}));
|
||||
|
||||
|
|
@ -128,18 +134,18 @@ pub(crate) fn rename(
|
|||
})
|
||||
.collect(),
|
||||
None => defs
|
||||
.map(|(.., def)| {
|
||||
.map(|(.., def, new_name, rename_def)| {
|
||||
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, kind);
|
||||
}
|
||||
if new_name == "self" {
|
||||
if kind == IdentifierKind::LowercaseSelf {
|
||||
cov_mark::hit!(rename_to_self);
|
||||
return rename_to_self(&sema, local);
|
||||
}
|
||||
}
|
||||
def.rename(&sema, new_name)
|
||||
def.rename(&sema, new_name.as_str(), rename_def)
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
|
@ -159,7 +165,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)
|
||||
}
|
||||
|
|
@ -200,22 +206,40 @@ fn find_definitions(
|
|||
sema: &Semantics<'_, RootDatabase>,
|
||||
syntax: &SyntaxNode,
|
||||
FilePosition { file_id, offset }: FilePosition,
|
||||
) -> RenameResult<impl Iterator<Item = (FileRange, SyntaxKind, Definition)>> {
|
||||
let token = syntax.token_at_offset(offset).find(|t| matches!(t.kind(), SyntaxKind::STRING));
|
||||
new_name: &Name,
|
||||
) -> RenameResult<impl Iterator<Item = (FileRange, SyntaxKind, Definition, Name, RenameDefinition)>>
|
||||
{
|
||||
let maybe_format_args =
|
||||
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))
|
||||
maybe_format_args.and_then(|token| sema.check_for_format_args_template(token, offset))
|
||||
{
|
||||
return Ok(vec![(
|
||||
FileRange { file_id, range },
|
||||
SyntaxKind::STRING,
|
||||
Definition::from(resolution),
|
||||
new_name.clone(),
|
||||
RenameDefinition::Yes,
|
||||
)]
|
||||
.into_iter());
|
||||
}
|
||||
|
||||
let original_ident = syntax
|
||||
.token_at_offset(offset)
|
||||
.max_by_key(|t| {
|
||||
t.kind().is_any_identifier() || matches!(t.kind(), SyntaxKind::LIFETIME_IDENT)
|
||||
})
|
||||
.map(|t| {
|
||||
if t.kind() == SyntaxKind::LIFETIME_IDENT {
|
||||
Name::new_lifetime(t.text())
|
||||
} else {
|
||||
Name::new_root(t.text())
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| format_err!("No references found at position"))?;
|
||||
let symbols =
|
||||
sema.find_nodes_at_offset_with_descend::<ast::NameLike>(syntax, offset).map(|name_like| {
|
||||
sema.find_namelike_at_offset_with_descend(syntax, offset).map(|name_like| {
|
||||
let kind = name_like.syntax().kind();
|
||||
let range = sema
|
||||
.original_range_opt(name_like.syntax())
|
||||
|
|
@ -284,23 +308,28 @@ fn find_definitions(
|
|||
.ok_or_else(|| format_err!("No references found at position"))
|
||||
}
|
||||
};
|
||||
res.map(|def| (range, kind, def))
|
||||
res.map(|def| {
|
||||
let n = def.name(sema.db)?;
|
||||
if n == original_ident {
|
||||
Some((range, kind, def, new_name.clone(), RenameDefinition::Yes))
|
||||
} else if let Some(suffix) = n.as_str().strip_prefix(original_ident.as_str()) {
|
||||
Some((range, kind, def, Name::new_root(&format!("{}{suffix}", new_name.as_str())), RenameDefinition::No))
|
||||
} else {
|
||||
n.as_str().strip_suffix(original_ident.as_str().trim_start_matches('\''))
|
||||
.map(|prefix| (range, kind, def, Name::new_root(&format!("{prefix}{}", new_name.as_str())), RenameDefinition::No))
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let res: RenameResult<Vec<_>> = symbols.collect();
|
||||
let res: RenameResult<Vec<_>> = symbols.filter_map(Result::transpose).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::<Vec<_>>()
|
||||
.into_iter())
|
||||
}
|
||||
// remove duplicates, comparing `Definition`s
|
||||
Ok(v.into_iter()
|
||||
.unique_by(|&(.., def, _, _)| def)
|
||||
.map(|(a, b, c, d, e)| (a.into_file_id(sema.db), b, c, d, e))
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter())
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
|
|
@ -370,7 +399,13 @@ fn rename_to_self(
|
|||
source_change.extend(usages.iter().map(|(file_id, references)| {
|
||||
(
|
||||
file_id.file_id(sema.db),
|
||||
source_edit_from_references(references, def, "self", file_id.edition(sema.db)),
|
||||
source_edit_from_references(
|
||||
sema.db,
|
||||
references,
|
||||
def,
|
||||
&Name::new_symbol_root(sym::self_),
|
||||
file_id.edition(sema.db),
|
||||
),
|
||||
)
|
||||
}));
|
||||
source_change.insert_source_edit(
|
||||
|
|
@ -384,23 +419,25 @@ fn rename_self_to_param(
|
|||
sema: &Semantics<'_, RootDatabase>,
|
||||
local: hir::Local,
|
||||
self_param: hir::SelfParam,
|
||||
new_name: &str,
|
||||
new_name: &Name,
|
||||
identifier_kind: IdentifierKind,
|
||||
) -> RenameResult<SourceChange> {
|
||||
if new_name == "self" {
|
||||
if identifier_kind == IdentifierKind::LowercaseSelf {
|
||||
// Let's do nothing rather than complain.
|
||||
cov_mark::hit!(rename_self_to_self);
|
||||
return Ok(SourceChange::default());
|
||||
}
|
||||
|
||||
let identifier_kind = IdentifierKind::classify(new_name)?;
|
||||
|
||||
let InFile { file_id, value: self_param } =
|
||||
sema.source(self_param).ok_or_else(|| format_err!("cannot find function source"))?;
|
||||
|
||||
let def = Definition::Local(local);
|
||||
let usages = def.usages(sema).all();
|
||||
let edit = text_edit_from_self_param(&self_param, new_name)
|
||||
.ok_or_else(|| format_err!("No target type found"))?;
|
||||
let edit = text_edit_from_self_param(
|
||||
&self_param,
|
||||
new_name.display(sema.db, file_id.edition(sema.db)).to_string(),
|
||||
)
|
||||
.ok_or_else(|| format_err!("No target type found"))?;
|
||||
if usages.len() > 1 && identifier_kind == IdentifierKind::Underscore {
|
||||
bail!("Cannot rename reference to `_` as it is being referenced multiple times");
|
||||
}
|
||||
|
|
@ -409,13 +446,19 @@ fn rename_self_to_param(
|
|||
source_change.extend(usages.iter().map(|(file_id, references)| {
|
||||
(
|
||||
file_id.file_id(sema.db),
|
||||
source_edit_from_references(references, def, new_name, file_id.edition(sema.db)),
|
||||
source_edit_from_references(
|
||||
sema.db,
|
||||
references,
|
||||
def,
|
||||
new_name,
|
||||
file_id.edition(sema.db),
|
||||
),
|
||||
)
|
||||
}));
|
||||
Ok(source_change)
|
||||
}
|
||||
|
||||
fn text_edit_from_self_param(self_param: &ast::SelfParam, new_name: &str) -> Option<TextEdit> {
|
||||
fn text_edit_from_self_param(self_param: &ast::SelfParam, new_name: String) -> Option<TextEdit> {
|
||||
fn target_type_name(impl_def: &ast::Impl) -> Option<String> {
|
||||
if let Some(ast::Type::PathType(p)) = impl_def.self_ty() {
|
||||
return Some(p.path()?.segment()?.name_ref()?.text().to_string());
|
||||
|
|
@ -427,7 +470,7 @@ fn text_edit_from_self_param(self_param: &ast::SelfParam, new_name: &str) -> Opt
|
|||
Some(impl_def) => {
|
||||
let type_name = target_type_name(&impl_def)?;
|
||||
|
||||
let mut replacement_text = String::from(new_name);
|
||||
let mut replacement_text = new_name;
|
||||
replacement_text.push_str(": ");
|
||||
match (self_param.amp_token(), self_param.mut_token()) {
|
||||
(Some(_), None) => replacement_text.push('&'),
|
||||
|
|
@ -440,7 +483,7 @@ fn text_edit_from_self_param(self_param: &ast::SelfParam, new_name: &str) -> Opt
|
|||
}
|
||||
None => {
|
||||
cov_mark::hit!(rename_self_outside_of_methods);
|
||||
let mut replacement_text = String::from(new_name);
|
||||
let mut replacement_text = new_name;
|
||||
replacement_text.push_str(": _");
|
||||
Some(TextEdit::replace(self_param.syntax().text_range(), replacement_text))
|
||||
}
|
||||
|
|
@ -710,7 +753,7 @@ impl Foo {
|
|||
check(
|
||||
"super",
|
||||
r#"fn main() { let i$0 = 1; }"#,
|
||||
"error: Invalid name `super`: not an identifier",
|
||||
"error: Invalid name `super`: cannot rename to a keyword",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -759,7 +802,11 @@ impl Foo {
|
|||
|
||||
#[test]
|
||||
fn test_rename_mod_invalid_raw_ident() {
|
||||
check("r#self", r#"mod foo$0 {}"#, "error: Invalid name `self`: not an identifier");
|
||||
check(
|
||||
"r#self",
|
||||
r#"mod foo$0 {}"#,
|
||||
"error: Invalid name `self`: cannot rename module to self",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -2359,7 +2406,6 @@ fn foo(foo: Foo) {
|
|||
|
||||
#[test]
|
||||
fn test_rename_lifetimes() {
|
||||
cov_mark::check!(rename_lifetime);
|
||||
check(
|
||||
"'yeeee",
|
||||
r#"
|
||||
|
|
@ -2536,7 +2582,7 @@ fn baz() {
|
|||
x.0$0 = 5;
|
||||
}
|
||||
"#,
|
||||
"error: No identifier available to rename",
|
||||
"error: No references found at position",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -2566,7 +2612,7 @@ impl Foo {
|
|||
}
|
||||
}
|
||||
"#,
|
||||
"error: Cannot rename `Self`",
|
||||
"error: No references found at position",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -3259,6 +3305,102 @@ trait Trait<T> {
|
|||
trait Trait<U> {
|
||||
fn foo() -> impl use<U> 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]
|
||||
// FIXME
|
||||
#[should_panic]
|
||||
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]
|
||||
// FIXME
|
||||
#[should_panic]
|
||||
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);
|
||||
fn other_place() { Quux::Bar$0; }
|
||||
"#,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,57 @@ 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<TopSubtree, ProcMacroExpansionError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue