syncdoc_core/
doc_injector.rs

1//! Procedural macro attributes for automatically injecting documentation from files.
2
3use proc_macro2::TokenStream;
4use quote::quote;
5use unsynn::*;
6
7use crate::parse::{FnSig, SyncDocArg, SyncDocInner};
8use crate::path_utils::make_manifest_relative_path;
9
10pub fn syncdoc_impl(
11    args: TokenStream,
12    item: TokenStream,
13) -> core::result::Result<TokenStream, TokenStream> {
14    // Parse the syncdoc arguments
15    let syncdoc_args = match parse_syncdoc_args(&mut args.to_token_iter()) {
16        Ok(args) => args,
17        Err(e) => {
18            // Return both error and original item to preserve valid syntax
19            return Ok(quote! {
20                compile_error!(#e);
21                #item
22            });
23        }
24    };
25
26    // Parse the function
27    let mut item_iter = item.to_token_iter();
28    let func = match parse_simple_function(&mut item_iter) {
29        Ok(func) => func,
30        Err(e) => {
31            return Ok(quote! {
32                compile_error!(#e);
33                #item
34            });
35        }
36    };
37
38    Ok(generate_documented_function(syncdoc_args, func))
39}
40
41/// Implementation for the module_doc!() macro
42///
43/// Generates an include_str!() call with the automatically resolved path
44/// to the module's markdown documentation file.
45pub fn module_doc_impl(args: TokenStream) -> core::result::Result<TokenStream, TokenStream> {
46    let call_site = proc_macro2::Span::call_site();
47    let source_file = call_site
48        .local_file()
49        .ok_or_else(|| {
50            let error = "Could not determine source file location";
51            quote! { compile_error!(#error) }
52        })?
53        .to_string_lossy()
54        .to_string();
55
56    // Parse the arguments to get base_path if provided
57    let base_path = if args.is_empty() {
58        // No args provided, get from config
59        crate::config::get_docs_path(&source_file).map_err(|e| {
60            let error = format!("Failed to get docs path from config: {}", e);
61            quote! { compile_error!(#error) }
62        })?
63    } else {
64        // Parse args to extract path
65        let mut args_iter = args.into_token_iter();
66        match parse_syncdoc_args(&mut args_iter) {
67            Ok(parsed_args) => parsed_args.base_path,
68            Err(e) => {
69                let error = format!("Failed to parse module_doc args: {}", e);
70                return Err(quote! { compile_error!(#error) });
71            }
72        }
73    };
74
75    // Extract module path and construct full doc path
76    let module_path = crate::path_utils::extract_module_path(&source_file);
77    let doc_path = if module_path.is_empty() {
78        // For lib.rs or main.rs, use the file stem
79        let file_stem = std::path::Path::new(&source_file)
80            .file_stem()
81            .and_then(|s| s.to_str())
82            .unwrap_or("module");
83        format!("{}/{}.md", base_path, file_stem)
84    } else {
85        format!("{}/{}.md", base_path, module_path)
86    };
87
88    // Make path relative to call site
89    let local_file = call_site.local_file().ok_or_else(|| {
90        let error = "Could not find local file";
91        quote! { compile_error!(#error) }
92    })?;
93    let rel_doc_path = make_manifest_relative_path(&doc_path, &local_file);
94
95    // Generate include_str!() call
96    Ok(quote! {
97        include_str!(#rel_doc_path)
98    })
99}
100
101#[derive(Debug)]
102struct SyncDocArgs {
103    base_path: String,
104    name: Option<String>,
105    cfg_attr: Option<String>,
106}
107
108struct SimpleFunction {
109    attrs: Vec<TokenStream>,
110    vis: Option<TokenStream>,
111    const_kw: Option<TokenStream>,
112    async_kw: Option<TokenStream>,
113    unsafe_kw: Option<TokenStream>,
114    extern_kw: Option<TokenStream>,
115    fn_name: proc_macro2::Ident,
116    generics: Option<TokenStream>,
117    params: TokenStream,
118    ret_type: Option<TokenStream>,
119    where_clause: Option<TokenStream>,
120    body: TokenStream,
121}
122
123fn parse_syncdoc_args(input: &mut TokenIter) -> core::result::Result<SyncDocArgs, String> {
124    match input.parse::<SyncDocInner>() {
125        Ok(parsed) => {
126            let mut args = SyncDocArgs {
127                base_path: String::new(),
128                name: None,
129                cfg_attr: None,
130            };
131
132            if let Some(arg_list) = parsed.args {
133                for arg in arg_list.0 {
134                    match arg.value {
135                        SyncDocArg::Path(path_arg) => {
136                            args.base_path = path_arg.value.as_str().to_string();
137                        }
138                        SyncDocArg::Name(name_arg) => {
139                            args.name = Some(name_arg.value.as_str().to_string());
140                        }
141                        SyncDocArg::CfgAttr(cfg_attr_arg) => {
142                            args.cfg_attr = Some(cfg_attr_arg.value.as_str().to_string());
143                        }
144                    }
145                }
146            }
147
148            if args.base_path.is_empty() || args.cfg_attr.is_none() {
149                // If macro path and TOML docs-path both unset, we don't know where to find the docs
150                if args.base_path.is_empty() {
151                    // Get the call site's file path if there might be config we could use there
152                    let call_site = proc_macro2::Span::call_site();
153                    let source_file = call_site
154                        .local_file()
155                        .ok_or("Could not determine source file location")?
156                        .to_string_lossy()
157                        .to_string();
158
159                    let base_path = crate::config::get_docs_path(&source_file)
160                        .map_err(|e| format!("Failed to get docs path from config: {}", e))?;
161
162                    // Extract module path and prepend to base_path
163                    let module_path = crate::path_utils::extract_module_path(&source_file);
164                    args.base_path = if module_path.is_empty() {
165                        base_path
166                    } else {
167                        format!("{}/{}", base_path, module_path)
168                    };
169                }
170
171                // We don't error on unconfigured cfg_attr, it's optional
172                if let Ok(cfg) = crate::config::get_cfg_attr() {
173                    args.cfg_attr = cfg;
174                }
175            }
176
177            Ok(args)
178        }
179        Err(e) => Err(format!("Failed to parse syncdoc args: {}", e)),
180    }
181}
182
183fn parse_simple_function(input: &mut TokenIter) -> core::result::Result<SimpleFunction, String> {
184    match input.parse::<FnSig>() {
185        Ok(parsed) => {
186            // Handle attributes
187            let attrs = if let Some(attr_list) = parsed.attributes {
188                attr_list
189                    .0
190                    .into_iter()
191                    .map(|attr| {
192                        let mut tokens = TokenStream::new();
193                        unsynn::ToTokens::to_tokens(&attr, &mut tokens);
194                        tokens
195                    })
196                    .collect()
197            } else {
198                Vec::new()
199            };
200
201            // Handle visibility
202            let vis = parsed.visibility.map(|v| {
203                let mut tokens = TokenStream::new();
204                quote::ToTokens::to_tokens(&v, &mut tokens);
205                tokens
206            });
207
208            // Handle const keyword
209            let const_kw = parsed.const_kw.map(|k| {
210                let mut tokens = TokenStream::new();
211                unsynn::ToTokens::to_tokens(&k, &mut tokens);
212                tokens
213            });
214
215            // Handle async keyword
216            let async_kw = parsed.async_kw.map(|k| {
217                let mut tokens = TokenStream::new();
218                unsynn::ToTokens::to_tokens(&k, &mut tokens);
219                tokens
220            });
221
222            // Handle unsafe keyword
223            let unsafe_kw = parsed.unsafe_kw.map(|k| {
224                let mut tokens = TokenStream::new();
225                unsynn::ToTokens::to_tokens(&k, &mut tokens);
226                tokens
227            });
228
229            // Handle extern keyword
230            let extern_kw = parsed.extern_kw.map(|k| {
231                let mut tokens = TokenStream::new();
232                unsynn::ToTokens::to_tokens(&k, &mut tokens);
233                tokens
234            });
235
236            let fn_name = parsed.name;
237
238            let generics = parsed.generics.map(|g| {
239                let mut tokens = TokenStream::new();
240                unsynn::ToTokens::to_tokens(&g, &mut tokens);
241                tokens
242            });
243
244            let mut params = TokenStream::new();
245            unsynn::ToTokens::to_tokens(&parsed.params, &mut params);
246
247            let ret_type = parsed.return_type.map(|rt| {
248                let mut tokens = TokenStream::new();
249                unsynn::ToTokens::to_tokens(&rt, &mut tokens);
250                tokens
251            });
252
253            let where_clause = parsed.where_clause.map(|wc| {
254                let mut tokens = TokenStream::new();
255                unsynn::ToTokens::to_tokens(&wc, &mut tokens);
256                tokens
257            });
258
259            let mut body = TokenStream::new();
260            unsynn::ToTokens::to_tokens(&parsed.body, &mut body);
261
262            Ok(SimpleFunction {
263                attrs,
264                vis,
265                const_kw,
266                async_kw,
267                unsafe_kw,
268                extern_kw,
269                fn_name,
270                generics,
271                params,
272                ret_type,
273                where_clause,
274                body,
275            })
276        }
277        Err(e) => Err(format!("Failed to parse function: {}", e)),
278    }
279}
280
281fn generate_documented_function(args: SyncDocArgs, func: SimpleFunction) -> TokenStream {
282    let SimpleFunction {
283        attrs,
284        vis,
285        const_kw,
286        async_kw,
287        unsafe_kw,
288        extern_kw,
289        fn_name,
290        generics,
291        params,
292        ret_type,
293        where_clause,
294        body,
295    } = func;
296
297    // Construct the doc path
298    let doc_file_name = args.name.unwrap_or_else(|| fn_name.to_string());
299    let doc_path = if args.base_path.ends_with(".md") {
300        // Direct file path provided
301        args.base_path
302    } else {
303        // Directory path provided, append function name
304        format!("{}/{}.md", args.base_path, doc_file_name)
305    };
306
307    // Make path relative to call site (doc_path already includes module path from omnibus.rs)
308    let call_site = proc_macro2::Span::call_site();
309    let local_file = call_site.local_file().expect("Could not find local file");
310    let rel_doc_path = make_manifest_relative_path(&doc_path, &local_file);
311
312    // Generate tokens for all the modifiers
313    let vis_tokens = vis.unwrap_or_default();
314    let const_tokens = const_kw.unwrap_or_default();
315    let async_tokens = async_kw.unwrap_or_default();
316    let unsafe_tokens = unsafe_kw.unwrap_or_default();
317    let extern_tokens = extern_kw.unwrap_or_default();
318    let generics_tokens = generics.unwrap_or_default();
319    let ret_tokens = ret_type.unwrap_or_default();
320    let where_tokens = where_clause.unwrap_or_default();
321
322    // Generate the documented function
323    let doc_attr = if let Some(cfg_value) = args.cfg_attr {
324        let cfg_ident = proc_macro2::Ident::new(&cfg_value, proc_macro2::Span::call_site());
325        quote! { #[cfg_attr(#cfg_ident, doc = include_str!(#rel_doc_path))] }
326    } else {
327        quote! { #[doc = include_str!(#rel_doc_path)] }
328    };
329
330    quote! {
331        #(#attrs)*
332        #doc_attr
333        #vis_tokens #const_tokens #async_tokens #unsafe_tokens #extern_tokens fn #fn_name #generics_tokens #params #ret_tokens #where_tokens #body
334    }
335}
336
337/// Injects a doc attribute without parsing the item structure
338pub fn inject_doc_attr(
339    doc_path: String,
340    cfg_attr: Option<String>,
341    item: TokenStream,
342) -> TokenStream {
343    // Get the call site's file path if there might be config we could use there
344    let call_site = proc_macro2::Span::call_site();
345    let local_file = call_site.local_file().expect("Could not find local file");
346    let rel_doc_path = make_manifest_relative_path(&doc_path, &local_file);
347    if let Some(cfg_value) = cfg_attr {
348        let cfg_ident = proc_macro2::Ident::new(&cfg_value, proc_macro2::Span::call_site());
349        quote! {
350            #[cfg_attr(#cfg_ident, doc = include_str!(#rel_doc_path))]
351            #item
352        }
353    } else {
354        quote! {
355            #[doc = include_str!(#rel_doc_path)]
356            #item
357        }
358    }
359}