1use 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 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 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 args.base_path.is_empty() {
84 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 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 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 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 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 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 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 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 args.base_path
221 } else {
222 format!("{}/{}.md", args.base_path, doc_file_name)
224 };
225
226 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 let doc_attr = if cfg!(feature = "cfg-attr-doc") {
238 quote! { #[cfg_attr(doc, doc = include_str!(#doc_path))] }
240 } else {
241 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
252pub fn inject_doc_attr(doc_path: String, item: TokenStream) -> TokenStream {
254 if cfg!(feature = "cfg-attr-doc") {
255 quote! {
257 #[cfg_attr(doc, doc = include_str!(#doc_path))]
258 #item
259 }
260 } else {
261 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}