feat: Support macro calls in eager macros for IDE features

This commit is contained in:
Lukas Wirth 2024-03-14 12:02:23 +01:00
parent 9767156a29
commit d2f8eae2ec
22 changed files with 420 additions and 226 deletions

View file

@ -201,10 +201,12 @@ impl Attr {
span_map: SpanMapRef<'_>,
id: AttrId,
) -> Option<Attr> {
let path = Interned::new(ModPath::from_src(db, ast.path()?, &mut |range| {
let path = ast.path()?;
let range = path.syntax().text_range();
let path = Interned::new(ModPath::from_src(db, path, &mut |range| {
span_map.span_for_range(range).ctx
})?);
let span = span_map.span_for_range(ast.syntax().text_range());
let span = span_map.span_for_range(range);
let input = if let Some(ast::Expr::Literal(lit)) = ast.expr() {
let value = match lit.kind() {
ast::LiteralKind::String(string) => string.value()?.into(),

View file

@ -19,14 +19,14 @@ use crate::{
};
macro_rules! register_builtin {
( LAZY: $(($name:ident, $kind: ident) => $expand:ident),* , EAGER: $(($e_name:ident, $e_kind: ident) => $e_expand:ident),* ) => {
( $LAZY:ident: $(($name:ident, $kind: ident) => $expand:ident),* , $EAGER:ident: $(($e_name:ident, $e_kind: ident) => $e_expand:ident),* ) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BuiltinFnLikeExpander {
pub enum $LAZY {
$($kind),*
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EagerExpander {
pub enum $EAGER {
$($e_kind),*
}
@ -84,6 +84,17 @@ impl EagerExpander {
pub fn is_include(&self) -> bool {
matches!(self, EagerExpander::Include)
}
pub fn is_include_like(&self) -> bool {
matches!(
self,
EagerExpander::Include | EagerExpander::IncludeStr | EagerExpander::IncludeBytes
)
}
pub fn is_env_or_option_env(&self) -> bool {
matches!(self, EagerExpander::Env | EagerExpander::OptionEnv)
}
}
pub fn find_builtin_macro(
@ -93,7 +104,7 @@ pub fn find_builtin_macro(
}
register_builtin! {
LAZY:
BuiltinFnLikeExpander:
(column, Column) => line_expand,
(file, File) => file_expand,
(line, Line) => line_expand,
@ -114,7 +125,7 @@ register_builtin! {
(format_args_nl, FormatArgsNl) => format_args_nl_expand,
(quote, Quote) => quote_expand,
EAGER:
EagerExpander:
(compile_error, CompileError) => compile_error_expand,
(concat, Concat) => concat_expand,
(concat_idents, ConcatIdents) => concat_idents_expand,
@ -426,22 +437,25 @@ fn use_panic_2021(db: &dyn ExpandDatabase, span: Span) -> bool {
}
}
fn unquote_str(lit: &tt::Literal) -> Option<String> {
fn unquote_str(lit: &tt::Literal) -> Option<(String, Span)> {
let span = lit.span;
let lit = ast::make::tokens::literal(&lit.to_string());
let token = ast::String::cast(lit)?;
token.value().map(|it| it.into_owned())
token.value().map(|it| (it.into_owned(), span))
}
fn unquote_char(lit: &tt::Literal) -> Option<char> {
fn unquote_char(lit: &tt::Literal) -> Option<(char, Span)> {
let span = lit.span;
let lit = ast::make::tokens::literal(&lit.to_string());
let token = ast::Char::cast(lit)?;
token.value()
token.value().zip(Some(span))
}
fn unquote_byte_string(lit: &tt::Literal) -> Option<Vec<u8>> {
fn unquote_byte_string(lit: &tt::Literal) -> Option<(Vec<u8>, Span)> {
let span = lit.span;
let lit = ast::make::tokens::literal(&lit.to_string());
let token = ast::ByteString::cast(lit)?;
token.value().map(|it| it.into_owned())
token.value().map(|it| (it.into_owned(), span))
}
fn compile_error_expand(
@ -452,7 +466,7 @@ fn compile_error_expand(
) -> ExpandResult<tt::Subtree> {
let err = match &*tt.token_trees {
[tt::TokenTree::Leaf(tt::Leaf::Literal(it))] => match unquote_str(it) {
Some(unquoted) => ExpandError::other(unquoted.into_boxed_str()),
Some((unquoted, _)) => ExpandError::other(unquoted.into_boxed_str()),
None => ExpandError::other("`compile_error!` argument must be a string"),
},
_ => ExpandError::other("`compile_error!` argument must be a string"),
@ -465,10 +479,16 @@ fn concat_expand(
_db: &dyn ExpandDatabase,
_arg_id: MacroCallId,
tt: &tt::Subtree,
span: Span,
_: Span,
) -> ExpandResult<tt::Subtree> {
let mut err = None;
let mut text = String::new();
let mut span: Option<Span> = None;
let mut record_span = |s: Span| match &mut span {
Some(span) if span.anchor == s.anchor => span.range = span.range.cover(s.range),
Some(_) => (),
None => span = Some(s),
};
for (i, mut t) in tt.token_trees.iter().enumerate() {
// FIXME: hack on top of a hack: `$e:expr` captures get surrounded in parentheses
// to ensure the right parsing order, so skip the parentheses here. Ideally we'd
@ -486,11 +506,14 @@ fn concat_expand(
// concat works with string and char literals, so remove any quotes.
// It also works with integer, float and boolean literals, so just use the rest
// as-is.
if let Some(c) = unquote_char(it) {
if let Some((c, span)) = unquote_char(it) {
text.push(c);
record_span(span);
} else {
let component = unquote_str(it).unwrap_or_else(|| it.text.to_string());
let (component, span) =
unquote_str(it).unwrap_or_else(|| (it.text.to_string(), it.span));
text.push_str(&component);
record_span(span);
}
}
// handle boolean literals
@ -498,6 +521,7 @@ fn concat_expand(
if i % 2 == 0 && (id.text == "true" || id.text == "false") =>
{
text.push_str(id.text.as_str());
record_span(id.span);
}
tt::TokenTree::Leaf(tt::Leaf::Punct(punct)) if i % 2 == 1 && punct.char == ',' => (),
_ => {
@ -505,6 +529,7 @@ fn concat_expand(
}
}
}
let span = span.unwrap_or(tt.delimiter.open);
ExpandResult { value: quote!(span =>#text), err }
}
@ -512,18 +537,25 @@ fn concat_bytes_expand(
_db: &dyn ExpandDatabase,
_arg_id: MacroCallId,
tt: &tt::Subtree,
span: Span,
call_site: Span,
) -> ExpandResult<tt::Subtree> {
let mut bytes = Vec::new();
let mut err = None;
let mut span: Option<Span> = None;
let mut record_span = |s: Span| match &mut span {
Some(span) if span.anchor == s.anchor => span.range = span.range.cover(s.range),
Some(_) => (),
None => span = Some(s),
};
for (i, t) in tt.token_trees.iter().enumerate() {
match t {
tt::TokenTree::Leaf(tt::Leaf::Literal(lit)) => {
let token = ast::make::tokens::literal(&lit.to_string());
record_span(lit.span);
match token.kind() {
syntax::SyntaxKind::BYTE => bytes.push(token.text().to_owned()),
syntax::SyntaxKind::BYTE_STRING => {
let components = unquote_byte_string(lit).unwrap_or_default();
let components = unquote_byte_string(lit).map_or(vec![], |(it, _)| it);
components.into_iter().for_each(|it| bytes.push(it.to_string()));
}
_ => {
@ -534,7 +566,7 @@ fn concat_bytes_expand(
}
tt::TokenTree::Leaf(tt::Leaf::Punct(punct)) if i % 2 == 1 && punct.char == ',' => (),
tt::TokenTree::Subtree(tree) if tree.delimiter.kind == tt::DelimiterKind::Bracket => {
if let Err(e) = concat_bytes_expand_subtree(tree, &mut bytes) {
if let Err(e) = concat_bytes_expand_subtree(tree, &mut bytes, &mut record_span) {
err.get_or_insert(e);
break;
}
@ -546,17 +578,24 @@ fn concat_bytes_expand(
}
}
let value = tt::Subtree {
delimiter: tt::Delimiter { open: span, close: span, kind: tt::DelimiterKind::Bracket },
delimiter: tt::Delimiter {
open: call_site,
close: call_site,
kind: tt::DelimiterKind::Bracket,
},
token_trees: {
Itertools::intersperse_with(
bytes.into_iter().map(|it| {
tt::TokenTree::Leaf(tt::Leaf::Literal(tt::Literal { text: it.into(), span }))
tt::TokenTree::Leaf(tt::Leaf::Literal(tt::Literal {
text: it.into(),
span: span.unwrap_or(call_site),
}))
}),
|| {
tt::TokenTree::Leaf(tt::Leaf::Punct(tt::Punct {
char: ',',
spacing: tt::Spacing::Alone,
span,
span: call_site,
}))
},
)
@ -569,13 +608,15 @@ fn concat_bytes_expand(
fn concat_bytes_expand_subtree(
tree: &tt::Subtree,
bytes: &mut Vec<String>,
mut record_span: impl FnMut(Span),
) -> Result<(), ExpandError> {
for (ti, tt) in tree.token_trees.iter().enumerate() {
match tt {
tt::TokenTree::Leaf(tt::Leaf::Literal(lit)) => {
let lit = ast::make::tokens::literal(&lit.to_string());
tt::TokenTree::Leaf(tt::Leaf::Literal(it)) => {
let lit = ast::make::tokens::literal(&it.to_string());
match lit.kind() {
syntax::SyntaxKind::BYTE | syntax::SyntaxKind::INT_NUMBER => {
record_span(it.span);
bytes.push(lit.text().to_owned())
}
_ => {
@ -635,7 +676,7 @@ fn relative_file(
}
}
fn parse_string(tt: &tt::Subtree) -> Result<String, ExpandError> {
fn parse_string(tt: &tt::Subtree) -> Result<(String, Span), ExpandError> {
tt.token_trees
.first()
.and_then(|tt| match tt {
@ -675,7 +716,7 @@ pub fn include_input_to_file_id(
arg_id: MacroCallId,
arg: &tt::Subtree,
) -> Result<FileId, ExpandError> {
relative_file(db, arg_id, &parse_string(arg)?, false)
relative_file(db, arg_id, &parse_string(arg)?.0, false)
}
fn include_bytes_expand(
@ -701,7 +742,7 @@ fn include_str_expand(
tt: &tt::Subtree,
span: Span,
) -> ExpandResult<tt::Subtree> {
let path = match parse_string(tt) {
let (path, span) = match parse_string(tt) {
Ok(it) => it,
Err(e) => {
return ExpandResult::new(tt::Subtree::empty(DelimSpan { open: span, close: span }), e)
@ -736,7 +777,7 @@ fn env_expand(
tt: &tt::Subtree,
span: Span,
) -> ExpandResult<tt::Subtree> {
let key = match parse_string(tt) {
let (key, span) = match parse_string(tt) {
Ok(it) => it,
Err(e) => {
return ExpandResult::new(tt::Subtree::empty(DelimSpan { open: span, close: span }), e)
@ -766,18 +807,24 @@ fn option_env_expand(
db: &dyn ExpandDatabase,
arg_id: MacroCallId,
tt: &tt::Subtree,
span: Span,
call_site: Span,
) -> ExpandResult<tt::Subtree> {
let key = match parse_string(tt) {
let (key, span) = match parse_string(tt) {
Ok(it) => it,
Err(e) => {
return ExpandResult::new(tt::Subtree::empty(DelimSpan { open: span, close: span }), e)
return ExpandResult::new(
tt::Subtree::empty(DelimSpan { open: call_site, close: call_site }),
e,
)
}
};
let dollar_crate = dollar_crate(span);
let dollar_crate = dollar_crate(call_site);
let expanded = match get_env_inner(db, arg_id, &key) {
None => quote! {span => #dollar_crate::option::Option::None::<&str> },
Some(s) => quote! {span => #dollar_crate::option::Option::Some(#s) },
None => quote! {call_site => #dollar_crate::option::Option::None::<&str> },
Some(s) => {
let s = quote! (span => #s);
quote! {call_site => #dollar_crate::option::Option::Some(#s) }
}
};
ExpandResult::ok(expanded)

View file

@ -323,6 +323,9 @@ impl HirFileIdExt for HirFileId {
}
pub trait MacroFileIdExt {
fn is_env_or_option_env(&self, db: &dyn ExpandDatabase) -> bool;
fn is_include_like_macro(&self, db: &dyn ExpandDatabase) -> bool;
fn eager_arg(&self, db: &dyn ExpandDatabase) -> Option<MacroCallId>;
fn expansion_level(self, db: &dyn ExpandDatabase) -> u32;
/// If this is a macro call, returns the syntax node of the call.
fn call_node(self, db: &dyn ExpandDatabase) -> InFile<SyntaxNode>;
@ -389,18 +392,34 @@ impl MacroFileIdExt for MacroFileId {
db.lookup_intern_macro_call(self.macro_call_id).def.is_include()
}
fn is_include_like_macro(&self, db: &dyn ExpandDatabase) -> bool {
db.lookup_intern_macro_call(self.macro_call_id).def.is_include_like()
}
fn is_env_or_option_env(&self, db: &dyn ExpandDatabase) -> bool {
db.lookup_intern_macro_call(self.macro_call_id).def.is_env_or_option_env()
}
fn is_eager(&self, db: &dyn ExpandDatabase) -> bool {
let loc: MacroCallLoc = db.lookup_intern_macro_call(self.macro_call_id);
let loc = db.lookup_intern_macro_call(self.macro_call_id);
matches!(loc.def.kind, MacroDefKind::BuiltInEager(..))
}
fn eager_arg(&self, db: &dyn ExpandDatabase) -> Option<MacroCallId> {
let loc = db.lookup_intern_macro_call(self.macro_call_id);
match &loc.kind {
MacroCallKind::FnLike { eager, .. } => eager.as_ref().map(|it| it.arg_id),
_ => None,
}
}
fn is_attr_macro(&self, db: &dyn ExpandDatabase) -> bool {
let loc: MacroCallLoc = db.lookup_intern_macro_call(self.macro_call_id);
let loc = db.lookup_intern_macro_call(self.macro_call_id);
matches!(loc.kind, MacroCallKind::Attr { .. })
}
fn is_derive_attr_pseudo_expansion(&self, db: &dyn ExpandDatabase) -> bool {
let loc: MacroCallLoc = db.lookup_intern_macro_call(self.macro_call_id);
let loc = db.lookup_intern_macro_call(self.macro_call_id);
loc.def.is_attribute_derive()
}
}
@ -478,6 +497,14 @@ impl MacroDefId {
pub fn is_include(&self) -> bool {
matches!(self.kind, MacroDefKind::BuiltInEager(expander, ..) if expander.is_include())
}
pub fn is_include_like(&self) -> bool {
matches!(self.kind, MacroDefKind::BuiltInEager(expander, ..) if expander.is_include_like())
}
pub fn is_env_or_option_env(&self) -> bool {
matches!(self.kind, MacroDefKind::BuiltInEager(expander, ..) if expander.is_env_or_option_env())
}
}
impl MacroCallLoc {
@ -659,7 +686,7 @@ impl MacroCallKind {
/// ExpansionInfo mainly describes how to map text range between src and expanded macro
// FIXME: can be expensive to create, we should check the use sites and maybe replace them with
// simpler function calls if the map is only used once
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExpansionInfo {
pub expanded: InMacroFile<SyntaxNode>,
/// The argument TokenTree or item for attributes
@ -689,6 +716,22 @@ impl ExpansionInfo {
/// Maps the passed in file range down into a macro expansion if it is the input to a macro call.
///
/// Note this does a linear search through the entire backing vector of the spanmap.
pub fn map_range_down_exact(
&self,
span: Span,
) -> Option<InMacroFile<impl Iterator<Item = SyntaxToken> + '_>> {
let tokens = self
.exp_map
.ranges_with_span_exact(span)
.flat_map(move |range| self.expanded.value.covering_element(range).into_token());
Some(InMacroFile::new(self.expanded.file_id, tokens))
}
/// Maps the passed in file range down into a macro expansion if it is the input to a macro call.
/// Unlike [`map_range_down_exact`], this will consider spans that contain the given span.
///
/// Note this does a linear search through the entire backing vector of the spanmap.
pub fn map_range_down(
&self,
span: Span,
@ -745,7 +788,7 @@ impl ExpansionInfo {
InFile::new(
self.arg.file_id,
arg_map
.ranges_with_span(span)
.ranges_with_span_exact(span)
.filter(|range| range.intersect(arg_range).is_some())
.collect(),
)

View file

@ -266,10 +266,10 @@ mod tests {
let quoted = quote!(DUMMY =>#a);
assert_eq!(quoted.to_string(), "hello");
let t = format!("{quoted:?}");
let t = format!("{quoted:#?}");
expect![[r#"
SUBTREE $$ SpanData { range: 0..0, anchor: SpanAnchor(FileId(937550), 0), ctx: SyntaxContextId(0) } SpanData { range: 0..0, anchor: SpanAnchor(FileId(937550), 0), ctx: SyntaxContextId(0) }
IDENT hello SpanData { range: 0..0, anchor: SpanAnchor(FileId(937550), 0), ctx: SyntaxContextId(0) }"#]].assert_eq(&t);
SUBTREE $$ 937550:0@0..0#0 937550:0@0..0#0
IDENT hello 937550:0@0..0#0"#]].assert_eq(&t);
}
#[test]

View file

@ -1,4 +1,5 @@
//! Span maps for real files and macro expansions.
use span::{FileId, HirFileId, HirFileIdRepr, MacroFileId, Span, SyntaxContextId};
use syntax::{AstNode, TextRange};
use triomphe::Arc;