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