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