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::{DocStubArg, DocStubInner, FnSig};
8
9pub fn syncdoc_impl(
10    args: TokenStream,
11    item: TokenStream,
12) -> core::result::Result<TokenStream, TokenStream> {
13    // Parse the syncdoc arguments
14    let syncdoc_args = match parse_syncdoc_args(&mut args.to_token_iter()) {
15        Ok(args) => args,
16        Err(e) => {
17            // Return both error and original item to preserve valid syntax
18            return Ok(quote! {
19                compile_error!(#e);
20                #item
21            });
22        }
23    };
24
25    // Parse the function
26    let mut item_iter = item.to_token_iter();
27    let func = match parse_simple_function(&mut item_iter) {
28        Ok(func) => func,
29        Err(e) => {
30            return Ok(quote! {
31                compile_error!(#e);
32                #item
33            });
34        }
35    };
36
37    Ok(generate_documented_function(syncdoc_args, func))
38}
39
40#[derive(Debug)]
41struct DocStubArgs {
42    base_path: String,
43    name: Option<String>,
44}
45
46struct SimpleFunction {
47    attrs: Vec<TokenStream>,
48    vis: Option<TokenStream>,
49    const_kw: Option<TokenStream>,
50    async_kw: Option<TokenStream>,
51    unsafe_kw: Option<TokenStream>,
52    extern_kw: Option<TokenStream>,
53    fn_name: proc_macro2::Ident,
54    generics: Option<TokenStream>,
55    params: TokenStream,
56    ret_type: Option<TokenStream>,
57    where_clause: Option<TokenStream>,
58    body: TokenStream,
59}
60
61fn parse_syncdoc_args(input: &mut TokenIter) -> core::result::Result<DocStubArgs, String> {
62    match input.parse::<DocStubInner>() {
63        Ok(parsed) => {
64            let mut args = DocStubArgs {
65                base_path: String::new(),
66                name: None,
67            };
68
69            if let Some(arg_list) = parsed.args {
70                for arg in arg_list.0 {
71                    match arg.value {
72                        DocStubArg::Path(path_arg) => {
73                            args.base_path = path_arg.value.as_str().to_string();
74                        }
75                        DocStubArg::Name(name_arg) => {
76                            args.name = Some(name_arg.value.as_str().to_string());
77                        }
78                    }
79                }
80            }
81
82            // If no path provided, try to get from config
83            if args.base_path.is_empty() {
84                // Get the call site's file path
85                let call_site = proc_macro2::Span::call_site();
86                let source_file = call_site
87                    .local_file()
88                    .ok_or("Could not determine source file location")?
89                    .to_string_lossy()
90                    .to_string();
91
92                args.base_path = crate::config::get_docs_path(&source_file)
93                    .map_err(|e| format!("Failed to get docs path from config: {}", e))?;
94            }
95
96            Ok(args)
97        }
98        Err(e) => Err(format!("Failed to parse syncdoc args: {}", e)),
99    }
100}
101
102fn parse_simple_function(input: &mut TokenIter) -> core::result::Result<SimpleFunction, String> {
103    match input.parse::<FnSig>() {
104        Ok(parsed) => {
105            // Handle attributes
106            let attrs = if let Some(attr_list) = parsed.attributes {
107                attr_list
108                    .0
109                    .into_iter()
110                    .map(|attr| {
111                        let mut tokens = TokenStream::new();
112                        unsynn::ToTokens::to_tokens(&attr, &mut tokens);
113                        tokens
114                    })
115                    .collect()
116            } else {
117                Vec::new()
118            };
119
120            // Handle visibility
121            let vis = parsed.visibility.map(|v| {
122                let mut tokens = TokenStream::new();
123                quote::ToTokens::to_tokens(&v, &mut tokens);
124                tokens
125            });
126
127            // Handle const keyword
128            let const_kw = parsed.const_kw.map(|k| {
129                let mut tokens = TokenStream::new();
130                unsynn::ToTokens::to_tokens(&k, &mut tokens);
131                tokens
132            });
133
134            // Handle async keyword
135            let async_kw = parsed.async_kw.map(|k| {
136                let mut tokens = TokenStream::new();
137                unsynn::ToTokens::to_tokens(&k, &mut tokens);
138                tokens
139            });
140
141            // Handle unsafe keyword
142            let unsafe_kw = parsed.unsafe_kw.map(|k| {
143                let mut tokens = TokenStream::new();
144                unsynn::ToTokens::to_tokens(&k, &mut tokens);
145                tokens
146            });
147
148            // Handle extern keyword
149            let extern_kw = parsed.extern_kw.map(|k| {
150                let mut tokens = TokenStream::new();
151                unsynn::ToTokens::to_tokens(&k, &mut tokens);
152                tokens
153            });
154
155            let fn_name = parsed.name;
156
157            let generics = parsed.generics.map(|g| {
158                let mut tokens = TokenStream::new();
159                unsynn::ToTokens::to_tokens(&g, &mut tokens);
160                tokens
161            });
162
163            let mut params = TokenStream::new();
164            unsynn::ToTokens::to_tokens(&parsed.params, &mut params);
165
166            let ret_type = parsed.return_type.map(|rt| {
167                let mut tokens = TokenStream::new();
168                unsynn::ToTokens::to_tokens(&rt, &mut tokens);
169                tokens
170            });
171
172            let where_clause = parsed.where_clause.map(|wc| {
173                let mut tokens = TokenStream::new();
174                unsynn::ToTokens::to_tokens(&wc, &mut tokens);
175                tokens
176            });
177
178            let mut body = TokenStream::new();
179            unsynn::ToTokens::to_tokens(&parsed.body, &mut body);
180
181            Ok(SimpleFunction {
182                attrs,
183                vis,
184                const_kw,
185                async_kw,
186                unsafe_kw,
187                extern_kw,
188                fn_name,
189                generics,
190                params,
191                ret_type,
192                where_clause,
193                body,
194            })
195        }
196        Err(e) => Err(format!("Failed to parse function: {}", e)),
197    }
198}
199
200fn generate_documented_function(args: DocStubArgs, func: SimpleFunction) -> TokenStream {
201    let SimpleFunction {
202        attrs,
203        vis,
204        const_kw,
205        async_kw,
206        unsafe_kw,
207        extern_kw,
208        fn_name,
209        generics,
210        params,
211        ret_type,
212        where_clause,
213        body,
214    } = func;
215
216    // Construct the doc path
217    let doc_file_name = args.name.unwrap_or_else(|| fn_name.to_string());
218    let doc_path = if args.base_path.ends_with(".md") {
219        // Direct file path provided
220        args.base_path
221    } else {
222        // Directory path provided, append function name
223        format!("{}/{}.md", args.base_path, doc_file_name)
224    };
225
226    // Generate tokens for all the modifiers
227    let vis_tokens = vis.unwrap_or_default();
228    let const_tokens = const_kw.unwrap_or_default();
229    let async_tokens = async_kw.unwrap_or_default();
230    let unsafe_tokens = unsafe_kw.unwrap_or_default();
231    let extern_tokens = extern_kw.unwrap_or_default();
232    let generics_tokens = generics.unwrap_or_default();
233    let ret_tokens = ret_type.unwrap_or_default();
234    let where_tokens = where_clause.unwrap_or_default();
235
236    // Generate the documented function
237    let doc_attr = if cfg!(feature = "cfg-attr-doc") {
238        // Feature-gated doc attribute
239        quote! { #[cfg_attr(doc, doc = include_str!(#doc_path))] }
240    } else {
241        // Regular doc attribute
242        quote! { #[doc = include_str!(#doc_path)] }
243    };
244
245    quote! {
246        #(#attrs)*
247        #doc_attr
248        #vis_tokens #const_tokens #async_tokens #unsafe_tokens #extern_tokens fn #fn_name #generics_tokens #params #ret_tokens #where_tokens #body
249    }
250}
251
252/// Injects a doc attribute without parsing the item structure
253pub fn inject_doc_attr(doc_path: String, item: TokenStream) -> TokenStream {
254    if cfg!(feature = "cfg-attr-doc") {
255        // Feature-gated doc attribute
256        quote! {
257            #[cfg_attr(doc, doc = include_str!(#doc_path))]
258            #item
259        }
260    } else {
261        // Regular doc attribute
262        quote! {
263            #[doc = include_str!(#doc_path)]
264            #item
265        }
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272    use rust_format::{Formatter, RustFmt};
273
274    fn format_and_print(tokens: proc_macro2::TokenStream) -> String {
275        let fmt_str = RustFmt::default()
276            .format_tokens(tokens)
277            .unwrap_or_else(|e| panic!("Format error: {}", e));
278        println!("Generated code: {}", fmt_str);
279        fmt_str
280    }
281
282    #[test]
283    fn test_basic_doc_injection() {
284        let args = quote!(path = "../docs");
285        let item = quote! {
286            fn test_function(x: u32) -> u32 {
287                x + 1
288            }
289        };
290
291        let result = syncdoc_impl(args, item);
292        assert!(result.is_ok());
293
294        let output = result.unwrap();
295        let output_str = format_and_print(output);
296
297        assert!(output_str.replace(" ", "").contains("include_str!"));
298        assert!(output_str.contains("../docs/test_function.md"));
299        assert!(output_str.contains("fn test_function"));
300    }
301
302    #[test]
303    fn test_custom_name() {
304        let args = quote!(path = "../docs", name = "custom");
305        let item = quote! {
306            fn test_function() {}
307        };
308
309        let result = syncdoc_impl(args, item);
310        assert!(result.is_ok());
311
312        let output = result.unwrap();
313        let output_str = format_and_print(output);
314
315        assert!(output_str.contains("../docs/custom.md"));
316    }
317
318    #[test]
319    fn test_async_function_doc() {
320        let args = quote!(path = "../docs");
321        let item = quote! {
322            async fn test_async() {
323                println!("async test");
324            }
325        };
326
327        let result = syncdoc_impl(args, item);
328        assert!(result.is_ok());
329
330        let output = result.unwrap();
331        let output_str = format_and_print(output);
332
333        assert!(output_str.contains("async fn test_async"));
334        assert!(output_str.replace(" ", "").contains("include_str!"));
335    }
336
337    #[test]
338    fn test_unsafe_function_doc() {
339        let args = quote!(path = "../docs");
340        let item = quote! {
341            unsafe fn test_unsafe() {
342                println!("unsafe test");
343            }
344        };
345
346        let result = syncdoc_impl(args, item);
347        assert!(result.is_ok());
348
349        let output = result.unwrap();
350        let output_str = format_and_print(output);
351
352        assert!(output_str.contains("unsafe fn test_unsafe"));
353        assert!(output_str.replace(" ", "").contains("include_str!"));
354    }
355
356    #[test]
357    fn test_pub_async_function_doc() {
358        let args = quote!(path = "../docs");
359        let item = quote! {
360            pub async fn test_pub_async() {
361                println!("pub async test");
362            }
363        };
364
365        let result = syncdoc_impl(args, item);
366        assert!(result.is_ok());
367
368        let output = result.unwrap();
369        let output_str = format_and_print(output);
370
371        assert!(output_str.contains("pub async fn test_pub_async"));
372        assert!(output_str.replace(" ", "").contains("include_str!"));
373    }
374
375    #[test]
376    fn test_direct_file_path() {
377        let args = quote!(path = "../docs/special.md");
378        let item = quote! {
379            fn test_function() {}
380        };
381
382        let result = syncdoc_impl(args, item);
383        assert!(result.is_ok());
384
385        let output = result.unwrap();
386        let output_str = format_and_print(output);
387
388        assert!(output_str.contains("../docs/special.md"));
389        assert!(!output_str.contains("test_function.md"));
390    }
391}