macros: Add __internal_macro_expand feature for better RA macro expansion
This commit is contained in:
parent
79025dfca4
commit
0e8388abab
@ -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"
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user