diff --git a/crates/ruma-macros/Cargo.toml b/crates/ruma-macros/Cargo.toml index 27c4d51d..6c62f4a6 100644 --- a/crates/ruma-macros/Cargo.toml +++ b/crates/ruma-macros/Cargo.toml @@ -14,7 +14,14 @@ rust-version = { workspace = true } [lib] 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] +cfg-if = "1.0.0" once_cell = "1.13.0" proc-macro-crate = "3.1.0" proc-macro2 = "1.0.24" diff --git a/crates/ruma-macros/src/api/request.rs b/crates/ruma-macros/src/api/request.rs index 0d4bc0c0..386f3ecd 100644 --- a/crates/ruma-macros/src/api/request.rs +++ b/crates/ruma-macros/src/api/request.rs @@ -1,3 +1,4 @@ +use cfg_if::cfg_if; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use syn::{ @@ -26,13 +27,34 @@ pub fn expand_request(attr: RequestAttr, item: ItemStruct) -> TokenStream { |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! { #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)] - #[ruma_api(error = #error_ty)] + #ruma_api_attribute #item + + #request_impls } } diff --git a/crates/ruma-macros/src/api/response.rs b/crates/ruma-macros/src/api/response.rs index ae7c906f..667d5483 100644 --- a/crates/ruma-macros/src/api/response.rs +++ b/crates/ruma-macros/src/api/response.rs @@ -1,5 +1,6 @@ use std::ops::Not; +use cfg_if::cfg_if; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use syn::{ @@ -41,13 +42,38 @@ pub fn expand_response(attr: ResponseAttr, item: ItemStruct) -> TokenStream { }) .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! { #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)] - #[ruma_api(error = #error_ty, status = #status_ident)] + #ruma_api_attribute #item + + #response_impls } } diff --git a/crates/ruma-macros/src/lib.rs b/crates/ruma-macros/src/lib.rs index dda4a84a..74278dc4 100644 --- a/crates/ruma-macros/src/lib.rs +++ b/crates/ruma-macros/src/lib.rs @@ -4,6 +4,7 @@ //! //! See the documentation for the individual macros for usage details. +#![cfg_attr(feature = "__internal_macro_expand", feature(proc_macro_expand))] #![warn(missing_docs)] #![allow(unreachable_pub)] // https://github.com/rust-lang/rust-clippy/issues/9029 diff --git a/crates/ruma-macros/src/util.rs b/crates/ruma-macros/src/util.rs index 8c3af72a..9dd7e510 100644 --- a/crates/ruma-macros/src/util.rs +++ b/crates/ruma-macros/src/util.rs @@ -91,3 +91,114 @@ impl ToTokens for PrivateField<'_> { 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 { + 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); + } +}