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