derive_inspector_reflection.rs

  1//! Implements `#[derive_inspector_reflection]` macro to provide runtime access to trait methods
  2//! that have the shape `fn method(self) -> Self`. This code was generated using Zed Agent with Claude Opus 4.
  3
  4use heck::ToSnakeCase as _;
  5use proc_macro::TokenStream;
  6use proc_macro2::{Span, TokenStream as TokenStream2};
  7use quote::quote;
  8use syn::{
  9    Attribute, Expr, FnArg, Ident, Item, ItemTrait, Lit, Meta, Path, ReturnType, TraitItem, Type,
 10    parse_macro_input, parse_quote,
 11    visit_mut::{self, VisitMut},
 12};
 13
 14pub fn derive_inspector_reflection(_args: TokenStream, input: TokenStream) -> TokenStream {
 15    let mut item = parse_macro_input!(input as Item);
 16
 17    // First, expand any macros in the trait
 18    match &mut item {
 19        Item::Trait(trait_item) => {
 20            let mut expander = MacroExpander;
 21            expander.visit_item_trait_mut(trait_item);
 22        }
 23        _ => {
 24            return syn::Error::new_spanned(
 25                quote!(#item),
 26                "#[derive_inspector_reflection] can only be applied to traits",
 27            )
 28            .to_compile_error()
 29            .into();
 30        }
 31    }
 32
 33    // Now process the expanded trait
 34    match item {
 35        Item::Trait(trait_item) => generate_reflected_trait(trait_item),
 36        _ => unreachable!(),
 37    }
 38}
 39
 40fn generate_reflected_trait(trait_item: ItemTrait) -> TokenStream {
 41    let trait_name = &trait_item.ident;
 42    let vis = &trait_item.vis;
 43
 44    // Determine if we're being called from within the gpui crate
 45    let call_site = Span::call_site();
 46    let inspector_reflection_path = if is_called_from_gpui_crate(call_site) {
 47        quote! { crate::inspector_reflection }
 48    } else {
 49        quote! { ::gpui::inspector_reflection }
 50    };
 51
 52    // Collect method information for methods of form fn name(self) -> Self or fn name(mut self) -> Self
 53    let mut method_infos = Vec::new();
 54
 55    for item in &trait_item.items {
 56        if let TraitItem::Fn(method) = item {
 57            let method_name = &method.sig.ident;
 58
 59            // Check if method has self or mut self receiver
 60            let has_valid_self_receiver = method
 61                .sig
 62                .inputs
 63                .iter()
 64                .any(|arg| matches!(arg, FnArg::Receiver(r) if r.reference.is_none()));
 65
 66            // Check if method returns Self
 67            let returns_self = match &method.sig.output {
 68                ReturnType::Type(_, ty) => {
 69                    matches!(**ty, Type::Path(ref path) if path.path.is_ident("Self"))
 70                }
 71                ReturnType::Default => false,
 72            };
 73
 74            // Check if method has exactly one parameter (self or mut self)
 75            let param_count = method.sig.inputs.len();
 76
 77            // Include methods of form fn name(self) -> Self or fn name(mut self) -> Self
 78            // This includes methods with default implementations
 79            if has_valid_self_receiver && returns_self && param_count == 1 {
 80                // Extract documentation and cfg attributes
 81                let doc = extract_doc_comment(&method.attrs);
 82                let cfg_attrs = extract_cfg_attributes(&method.attrs);
 83                method_infos.push((method_name.clone(), doc, cfg_attrs));
 84            }
 85        }
 86    }
 87
 88    // Generate the reflection module name
 89    let reflection_mod_name = Ident::new(
 90        &format!("{}_reflection", trait_name.to_string().to_snake_case()),
 91        trait_name.span(),
 92    );
 93
 94    // Generate wrapper functions for each method
 95    // These wrappers use type erasure to allow runtime invocation
 96    let wrapper_functions = method_infos.iter().map(|(method_name, _doc, cfg_attrs)| {
 97        let wrapper_name = Ident::new(
 98            &format!("__wrapper_{}", method_name),
 99            method_name.span(),
100        );
101        quote! {
102            #(#cfg_attrs)*
103            fn #wrapper_name<T: #trait_name + 'static>(value: Box<dyn std::any::Any>) -> Box<dyn std::any::Any> {
104                if let Ok(concrete) = value.downcast::<T>() {
105                    Box::new(concrete.#method_name())
106                } else {
107                    panic!("Type mismatch in reflection wrapper");
108                }
109            }
110        }
111    });
112
113    // Generate method info entries
114    let method_info_entries = method_infos.iter().map(|(method_name, doc, cfg_attrs)| {
115        let method_name_str = method_name.to_string();
116        let wrapper_name = Ident::new(&format!("__wrapper_{}", method_name), method_name.span());
117        let doc_expr = match doc {
118            Some(doc_str) => quote! { Some(#doc_str) },
119            None => quote! { None },
120        };
121        quote! {
122            #(#cfg_attrs)*
123            #inspector_reflection_path::FunctionReflection {
124                name: #method_name_str,
125                function: #wrapper_name::<T>,
126                documentation: #doc_expr,
127                _type: ::std::marker::PhantomData,
128            }
129        }
130    });
131
132    // Generate the complete output
133    let output = quote! {
134        #trait_item
135
136        /// Implements function reflection
137        #vis mod #reflection_mod_name {
138            use super::*;
139
140            #(#wrapper_functions)*
141
142            /// Get all reflectable methods for a concrete type implementing the trait
143            pub fn methods<T: #trait_name + 'static>() -> Vec<#inspector_reflection_path::FunctionReflection<T>> {
144                vec![
145                    #(#method_info_entries),*
146                ]
147            }
148
149            /// Find a method by name for a concrete type implementing the trait
150            pub fn find_method<T: #trait_name + 'static>(name: &str) -> Option<#inspector_reflection_path::FunctionReflection<T>> {
151                methods::<T>().into_iter().find(|m| m.name == name)
152            }
153        }
154    };
155
156    TokenStream::from(output)
157}
158
159fn extract_doc_comment(attrs: &[Attribute]) -> Option<String> {
160    let mut doc_lines = Vec::new();
161
162    for attr in attrs {
163        if attr.path().is_ident("doc") {
164            if let Meta::NameValue(meta) = &attr.meta {
165                if let Expr::Lit(expr_lit) = &meta.value {
166                    if let Lit::Str(lit_str) = &expr_lit.lit {
167                        let line = lit_str.value();
168                        let line = line.strip_prefix(' ').unwrap_or(&line);
169                        doc_lines.push(line.to_string());
170                    }
171                }
172            }
173        }
174    }
175
176    if doc_lines.is_empty() {
177        None
178    } else {
179        Some(doc_lines.join("\n"))
180    }
181}
182
183fn extract_cfg_attributes(attrs: &[Attribute]) -> Vec<Attribute> {
184    attrs
185        .iter()
186        .filter(|attr| attr.path().is_ident("cfg"))
187        .cloned()
188        .collect()
189}
190
191fn is_called_from_gpui_crate(_span: Span) -> bool {
192    // Check if we're being called from within the gpui crate by examining the call site
193    // This is a heuristic approach - we check if the current crate name is "gpui"
194    std::env::var("CARGO_PKG_NAME").map_or(false, |name| name == "gpui")
195}
196
197struct MacroExpander;
198
199impl VisitMut for MacroExpander {
200    fn visit_item_trait_mut(&mut self, trait_item: &mut ItemTrait) {
201        let mut expanded_items = Vec::new();
202        let mut items_to_keep = Vec::new();
203
204        for item in trait_item.items.drain(..) {
205            match item {
206                TraitItem::Macro(macro_item) => {
207                    // Try to expand known macros
208                    if let Some(expanded) = try_expand_macro(&macro_item) {
209                        expanded_items.extend(expanded);
210                    } else {
211                        // Keep unknown macros as-is
212                        items_to_keep.push(TraitItem::Macro(macro_item));
213                    }
214                }
215                other => {
216                    items_to_keep.push(other);
217                }
218            }
219        }
220
221        // Rebuild the items list with expanded content first, then original items
222        trait_item.items = expanded_items;
223        trait_item.items.extend(items_to_keep);
224
225        // Continue visiting
226        visit_mut::visit_item_trait_mut(self, trait_item);
227    }
228}
229
230fn try_expand_macro(macro_item: &syn::TraitItemMacro) -> Option<Vec<TraitItem>> {
231    let path = &macro_item.mac.path;
232
233    // Check if this is one of our known style macros
234    let macro_name = path_to_string(path);
235
236    // Handle the known macros by calling their implementations
237    match macro_name.as_str() {
238        "gpui_macros::style_helpers" | "style_helpers" => {
239            let tokens = macro_item.mac.tokens.clone();
240            let expanded = crate::styles::style_helpers(TokenStream::from(tokens));
241            parse_expanded_items(expanded)
242        }
243        "gpui_macros::visibility_style_methods" | "visibility_style_methods" => {
244            let tokens = macro_item.mac.tokens.clone();
245            let expanded = crate::styles::visibility_style_methods(TokenStream::from(tokens));
246            parse_expanded_items(expanded)
247        }
248        "gpui_macros::margin_style_methods" | "margin_style_methods" => {
249            let tokens = macro_item.mac.tokens.clone();
250            let expanded = crate::styles::margin_style_methods(TokenStream::from(tokens));
251            parse_expanded_items(expanded)
252        }
253        "gpui_macros::padding_style_methods" | "padding_style_methods" => {
254            let tokens = macro_item.mac.tokens.clone();
255            let expanded = crate::styles::padding_style_methods(TokenStream::from(tokens));
256            parse_expanded_items(expanded)
257        }
258        "gpui_macros::position_style_methods" | "position_style_methods" => {
259            let tokens = macro_item.mac.tokens.clone();
260            let expanded = crate::styles::position_style_methods(TokenStream::from(tokens));
261            parse_expanded_items(expanded)
262        }
263        "gpui_macros::overflow_style_methods" | "overflow_style_methods" => {
264            let tokens = macro_item.mac.tokens.clone();
265            let expanded = crate::styles::overflow_style_methods(TokenStream::from(tokens));
266            parse_expanded_items(expanded)
267        }
268        "gpui_macros::cursor_style_methods" | "cursor_style_methods" => {
269            let tokens = macro_item.mac.tokens.clone();
270            let expanded = crate::styles::cursor_style_methods(TokenStream::from(tokens));
271            parse_expanded_items(expanded)
272        }
273        "gpui_macros::border_style_methods" | "border_style_methods" => {
274            let tokens = macro_item.mac.tokens.clone();
275            let expanded = crate::styles::border_style_methods(TokenStream::from(tokens));
276            parse_expanded_items(expanded)
277        }
278        "gpui_macros::box_shadow_style_methods" | "box_shadow_style_methods" => {
279            let tokens = macro_item.mac.tokens.clone();
280            let expanded = crate::styles::box_shadow_style_methods(TokenStream::from(tokens));
281            parse_expanded_items(expanded)
282        }
283        _ => None,
284    }
285}
286
287fn path_to_string(path: &Path) -> String {
288    path.segments
289        .iter()
290        .map(|seg| seg.ident.to_string())
291        .collect::<Vec<_>>()
292        .join("::")
293}
294
295fn parse_expanded_items(expanded: TokenStream) -> Option<Vec<TraitItem>> {
296    let tokens = TokenStream2::from(expanded);
297
298    // Try to parse the expanded tokens as trait items
299    // We need to wrap them in a dummy trait to parse properly
300    let dummy_trait: ItemTrait = parse_quote! {
301        trait Dummy {
302            #tokens
303        }
304    };
305
306    Some(dummy_trait.items)
307}