macros: Add __internal_macro_expand feature for better RA macro expansion

This commit is contained in:
Jonas Platte 2024-09-06 20:26:22 +02:00
parent 79025dfca4
commit 0e8388abab
5 changed files with 171 additions and 4 deletions

View File

@ -14,7 +14,14 @@ rust-version = { workspace = true }
[lib] [lib]
proc-macro = true proc-macro = true
[features]
# Make the request and response macros expand internal derives they would
# usually emit in the `#[derive()]` list directly, such that Rust Analyzer's
# expand macro helper can render their output. Requires a nightly toolchain.
__internal_macro_expand = ["syn/visit-mut"]
[dependencies] [dependencies]
cfg-if = "1.0.0"
once_cell = "1.13.0" once_cell = "1.13.0"
proc-macro-crate = "3.1.0" proc-macro-crate = "3.1.0"
proc-macro2 = "1.0.24" proc-macro2 = "1.0.24"

View File

@ -1,3 +1,4 @@
use cfg_if::cfg_if;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::{quote, ToTokens}; use quote::{quote, ToTokens};
use syn::{ use syn::{
@ -26,13 +27,34 @@ pub fn expand_request(attr: RequestAttr, item: ItemStruct) -> TokenStream {
|DeriveRequestMeta::Error(ty)| quote! { #ty }, |DeriveRequestMeta::Error(ty)| quote! { #ty },
); );
cfg_if! {
if #[cfg(feature = "__internal_macro_expand")] {
use syn::parse_quote;
let mut derive_input = item.clone();
derive_input.attrs.push(parse_quote! { #[ruma_api(error = #error_ty)] });
crate::util::cfg_expand_struct(&mut derive_input);
let extra_derive = quote! { #ruma_macros::_FakeDeriveRumaApi };
let ruma_api_attribute = quote! {};
let request_impls =
expand_derive_request(derive_input).unwrap_or_else(syn::Error::into_compile_error);
} else {
let extra_derive = quote! { #ruma_macros::Request };
let ruma_api_attribute = quote! { #[ruma_api(error = #error_ty)] };
let request_impls = quote! {};
}
}
quote! { quote! {
#maybe_feature_error #maybe_feature_error
#[derive(Clone, Debug, #ruma_macros::Request, #ruma_common::serde::_FakeDeriveSerde)] #[derive(Clone, Debug, #ruma_common::serde::_FakeDeriveSerde, #extra_derive)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_api(error = #error_ty)] #ruma_api_attribute
#item #item
#request_impls
} }
} }

View File

@ -1,5 +1,6 @@
use std::ops::Not; use std::ops::Not;
use cfg_if::cfg_if;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
use quote::{quote, ToTokens}; use quote::{quote, ToTokens};
use syn::{ use syn::{
@ -41,13 +42,38 @@ pub fn expand_response(attr: ResponseAttr, item: ItemStruct) -> TokenStream {
}) })
.unwrap_or_else(|| quote! { OK }); .unwrap_or_else(|| quote! { OK });
cfg_if! {
if #[cfg(feature = "__internal_macro_expand")] {
use syn::parse_quote;
let mut derive_input = item.clone();
derive_input.attrs.push(parse_quote! {
#[ruma_api(error = #error_ty, status = #status_ident)]
});
crate::util::cfg_expand_struct(&mut derive_input);
let extra_derive = quote! { #ruma_macros::_FakeDeriveRumaApi };
let ruma_api_attribute = quote! {};
let response_impls =
expand_derive_response(derive_input).unwrap_or_else(syn::Error::into_compile_error);
} else {
let extra_derive = quote! { #ruma_macros::Response };
let ruma_api_attribute = quote! {
#[ruma_api(error = #error_ty, status = #status_ident)]
};
let response_impls = quote! {};
}
}
quote! { quote! {
#maybe_feature_error #maybe_feature_error
#[derive(Clone, Debug, #ruma_macros::Response, #ruma_common::serde::_FakeDeriveSerde)] #[derive(Clone, Debug, #ruma_common::serde::_FakeDeriveSerde, #extra_derive)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_api(error = #error_ty, status = #status_ident)] #ruma_api_attribute
#item #item
#response_impls
} }
} }

View File

@ -4,6 +4,7 @@
//! //!
//! See the documentation for the individual macros for usage details. //! See the documentation for the individual macros for usage details.
#![cfg_attr(feature = "__internal_macro_expand", feature(proc_macro_expand))]
#![warn(missing_docs)] #![warn(missing_docs)]
#![allow(unreachable_pub)] #![allow(unreachable_pub)]
// https://github.com/rust-lang/rust-clippy/issues/9029 // https://github.com/rust-lang/rust-clippy/issues/9029

View File

@ -91,3 +91,114 @@ impl ToTokens for PrivateField<'_> {
ty.to_tokens(tokens); ty.to_tokens(tokens);
} }
} }
#[cfg(feature = "__internal_macro_expand")]
pub fn cfg_expand_struct(item: &mut syn::ItemStruct) {
use std::mem;
use proc_macro2::TokenTree;
use syn::{visit_mut::VisitMut, Fields, LitBool, Meta};
fn eval_cfg(cfg_expr: TokenStream) -> Option<bool> {
let cfg_macro_call = quote! { ::core::cfg!(#cfg_expr) };
let expanded = match proc_macro::TokenStream::from(cfg_macro_call).expand_expr() {
Ok(t) => t,
Err(e) => {
eprintln!("Failed to expand cfg! {e}");
return None;
}
};
let lit: LitBool = syn::parse(expanded).expect("cfg! must expand to a boolean literal");
Some(lit.value())
}
fn tokentree_not_comma(tree: &TokenTree) -> bool {
match tree {
TokenTree::Punct(p) => p.as_char() != ',',
_ => true,
}
}
struct CfgAttrExpand;
impl VisitMut for CfgAttrExpand {
fn visit_attribute_mut(&mut self, attr: &mut syn::Attribute) {
if attr.meta.path().is_ident("cfg_attr") {
// Ignore invalid cfg attributes
let Meta::List(list) = &attr.meta else { return };
let mut token_iter = list.tokens.clone().into_iter();
// Take all the tokens until the first toplevel comma.
// That's the cfg-expression part of cfg_attr.
let cfg_expr: TokenStream =
token_iter.by_ref().take_while(tokentree_not_comma).collect();
let Some(cfg_value) = eval_cfg(cfg_expr) else { return };
if cfg_value {
// If we had the whole attribute list and could emit more
// than one attribute, we'd split the remaining arguments to
// cfg_attr by commas and turn them into regular attributes
//
// Because we can emit only one, do the first and error if
// there's any more after it.
let attr_tokens: TokenStream =
token_iter.by_ref().take_while(tokentree_not_comma).collect();
if attr_tokens.is_empty() {
// no-op cfg_attr??
return;
}
attr.meta = syn::parse2(attr_tokens)
.expect("syn must be able to parse cfg-attr arguments as syn::Meta");
let rest: TokenStream = token_iter.collect();
assert!(
rest.is_empty(),
"cfg_attr's with multiple arguments after the cfg expression are not \
currently supported by __internal_macro_expand."
);
}
}
}
}
CfgAttrExpand.visit_item_struct_mut(item);
let Fields::Named(fields) = &mut item.fields else {
panic!("only named fields are currently supported by __internal_macro_expand");
};
// Take out all the fields
'fields: for mut field in mem::take(&mut fields.named) {
// Take out all the attributes
for attr in mem::take(&mut field.attrs) {
// For non-cfg attrs, put them back
if !attr.meta.path().is_ident("cfg") {
field.attrs.push(attr);
continue;
}
// Also put back / ignore invalid cfg attributes
let Meta::List(list) = &attr.meta else {
field.attrs.push(attr);
continue;
};
// Also put back / ignore cfg attributes we can't eval
let Some(cfg_value) = eval_cfg(list.tokens.clone()) else {
field.attrs.push(attr);
continue;
};
// Finally, if the cfg is `false`, skip the part where it's put back
if !cfg_value {
continue 'fields;
}
}
// If `continue 'fields` above wasn't hit, we didn't find a cfg that
// evals to false, so put the field back
fields.named.push(field);
}
}