syncdoc_core/
doc_injector.rs1use 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 let syncdoc_args = match parse_syncdoc_args(&mut args.to_token_iter()) {
12 Ok(args) => args,
13 Err(e) => {
14 return Ok(quote! {
16 compile_error!(#e);
17 #item
18 });
19 }
20 };
21
22 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 args.base_path.is_empty() {
81 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 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 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 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 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 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 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 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 args.base_path
217 } else {
218 format!("{}/{}.md", args.base_path, doc_file_name)
220 };
221
222 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 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
240pub 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}