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 args.cfg_attr.is_none() {
173                    let call_site = proc_macro2::Span::call_site();
174                    if let Some(source_path) = call_site.local_file() {
175                        let source_file = source_path.to_string_lossy().to_string();
176                        if let Ok(cfg) = crate::config::get_cfg_attr(&source_file) {
177                            args.cfg_attr = cfg;
178                        }
179                    }
180                }
181            }
182
183            Ok(args)
184        }
185        Err(e) => Err(format!("Failed to parse syncdoc args: {}", e)),
186    }
187}
188
189fn parse_simple_function(input: &mut TokenIter) -> core::result::Result<SimpleFunction, String> {
190    match input.parse::<FnSig>() {
191        Ok(parsed) => {
192            // Handle attributes
193            let attrs = if let Some(attr_list) = parsed.attributes {
194                attr_list
195                    .0
196                    .into_iter()
197                    .map(|attr| {
198                        let mut tokens = TokenStream::new();
199                        unsynn::ToTokens::to_tokens(&attr, &mut tokens);
200                        tokens
201                    })
202                    .collect()
203            } else {
204                Vec::new()
205            };
206
207            // Handle visibility
208            let vis = parsed.visibility.map(|v| {
209                let mut tokens = TokenStream::new();
210                quote::ToTokens::to_tokens(&v, &mut tokens);
211                tokens
212            });
213
214            // Handle const keyword
215            let const_kw = parsed.const_kw.map(|k| {
216                let mut tokens = TokenStream::new();
217                unsynn::ToTokens::to_tokens(&k, &mut tokens);
218                tokens
219            });
220
221            // Handle async keyword
222            let async_kw = parsed.async_kw.map(|k| {
223                let mut tokens = TokenStream::new();
224                unsynn::ToTokens::to_tokens(&k, &mut tokens);
225                tokens
226            });
227
228            // Handle unsafe keyword
229            let unsafe_kw = parsed.unsafe_kw.map(|k| {
230                let mut tokens = TokenStream::new();
231                unsynn::ToTokens::to_tokens(&k, &mut tokens);
232                tokens
233            });
234
235            // Handle extern keyword
236            let extern_kw = parsed.extern_kw.map(|k| {
237                let mut tokens = TokenStream::new();
238                unsynn::ToTokens::to_tokens(&k, &mut tokens);
239                tokens
240            });
241
242            let fn_name = parsed.name;
243
244            let generics = parsed.generics.map(|g| {
245                let mut tokens = TokenStream::new();
246                unsynn::ToTokens::to_tokens(&g, &mut tokens);
247                tokens
248            });
249
250            let mut params = TokenStream::new();
251            unsynn::ToTokens::to_tokens(&parsed.params, &mut params);
252
253            let ret_type = parsed.return_type.map(|rt| {
254                let mut tokens = TokenStream::new();
255                unsynn::ToTokens::to_tokens(&rt, &mut tokens);
256                tokens
257            });
258
259            let where_clause = parsed.where_clause.map(|wc| {
260                let mut tokens = TokenStream::new();
261                unsynn::ToTokens::to_tokens(&wc, &mut tokens);
262                tokens
263            });
264
265            let mut body = TokenStream::new();
266            unsynn::ToTokens::to_tokens(&parsed.body, &mut body);
267
268            Ok(SimpleFunction {
269                attrs,
270                vis,
271                const_kw,
272                async_kw,
273                unsafe_kw,
274                extern_kw,
275                fn_name,
276                generics,
277                params,
278                ret_type,
279                where_clause,
280                body,
281            })
282        }
283        Err(e) => Err(format!("Failed to parse function: {}", e)),
284    }
285}
286
287fn generate_documented_function(args: SyncDocArgs, func: SimpleFunction) -> TokenStream {
288    let SimpleFunction {
289        attrs,
290        vis,
291        const_kw,
292        async_kw,
293        unsafe_kw,
294        extern_kw,
295        fn_name,
296        generics,
297        params,
298        ret_type,
299        where_clause,
300        body,
301    } = func;
302
303    // Construct the doc path
304    let doc_file_name = args.name.unwrap_or_else(|| fn_name.to_string());
305    let doc_path = if args.base_path.ends_with(".md") {
306        // Direct file path provided
307        args.base_path
308    } else {
309        // Directory path provided, append function name
310        format!("{}/{}.md", args.base_path, doc_file_name)
311    };
312
313    // Make path relative to call site (doc_path already includes module path from omnibus.rs)
314    let call_site = proc_macro2::Span::call_site();
315    let local_file = call_site.local_file().expect("Could not find local file");
316    let rel_doc_path = make_manifest_relative_path(&doc_path, &local_file);
317
318    // Generate tokens for all the modifiers
319    let vis_tokens = vis.unwrap_or_default();
320    let const_tokens = const_kw.unwrap_or_default();
321    let async_tokens = async_kw.unwrap_or_default();
322    let unsafe_tokens = unsafe_kw.unwrap_or_default();
323    let extern_tokens = extern_kw.unwrap_or_default();
324    let generics_tokens = generics.unwrap_or_default();
325    let ret_tokens = ret_type.unwrap_or_default();
326    let where_tokens = where_clause.unwrap_or_default();
327
328    // Generate the documented function
329    let doc_attr = if let Some(cfg_value) = args.cfg_attr {
330        let cfg_ident = proc_macro2::Ident::new(&cfg_value, proc_macro2::Span::call_site());
331        quote! { #[cfg_attr(#cfg_ident, doc = include_str!(#rel_doc_path))] }
332    } else {
333        quote! { #[doc = include_str!(#rel_doc_path)] }
334    };
335
336    quote! {
337        #(#attrs)*
338        #doc_attr
339        #vis_tokens #const_tokens #async_tokens #unsafe_tokens #extern_tokens fn #fn_name #generics_tokens #params #ret_tokens #where_tokens #body
340    }
341}
342
343/// Injects a doc attribute without parsing the item structure
344pub fn inject_doc_attr(
345    doc_path: String,
346    cfg_attr: Option<String>,
347    item: TokenStream,
348) -> TokenStream {
349    // Get the call site's file path if there might be config we could use there
350    let call_site = proc_macro2::Span::call_site();
351    let local_file = call_site.local_file().expect("Could not find local file");
352    let rel_doc_path = make_manifest_relative_path(&doc_path, &local_file);
353    if let Some(cfg_value) = cfg_attr {
354        let cfg_ident = proc_macro2::Ident::new(&cfg_value, proc_macro2::Span::call_site());
355        quote! {
356            #[cfg_attr(#cfg_ident, doc = include_str!(#rel_doc_path))]
357            #item
358        }
359    } else {
360        quote! {
361            #[doc = include_str!(#rel_doc_path)]
362            #item
363        }
364    }
365}