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 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 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 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 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 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 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 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 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 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 args.base_path
202 } else {
203 format!("{}/{}.md", args.base_path, doc_file_name)
205 };
206
207 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 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#[cfg(test)]
226mod tests {
227 use super::*;
228 use rust_format::{Formatter, RustFmt};
229
230 fn format_and_print(tokens: proc_macro2::TokenStream) -> String {
231 let fmt_str = RustFmt::default()
232 .format_tokens(tokens)
233 .unwrap_or_else(|e| panic!("Format error: {}", e));
234 println!("Generated code: {}", fmt_str);
235 fmt_str
236 }
237
238 #[test]
239 fn test_basic_doc_injection() {
240 let args = quote!(path = "../docs");
241 let item = quote! {
242 fn test_function(x: u32) -> u32 {
243 x + 1
244 }
245 };
246
247 let result = syncdoc_impl(args, item);
248 assert!(result.is_ok());
249
250 let output = result.unwrap();
251 let output_str = format_and_print(output);
252
253 assert!(output_str.contains("include_str!"));
254 assert!(output_str.contains("../docs/test_function.md"));
255 assert!(output_str.contains("fn test_function"));
256 }
257
258 #[test]
259 fn test_custom_name() {
260 let args = quote!(path = "../docs", name = "custom");
261 let item = quote! {
262 fn test_function() {}
263 };
264
265 let result = syncdoc_impl(args, item);
266 assert!(result.is_ok());
267
268 let output = result.unwrap();
269 let output_str = format_and_print(output);
270
271 assert!(output_str.contains("../docs/custom.md"));
272 }
273
274 #[test]
275 fn test_async_function_doc() {
276 let args = quote!(path = "../docs");
277 let item = quote! {
278 async fn test_async() {
279 println!("async test");
280 }
281 };
282
283 let result = syncdoc_impl(args, item);
284 assert!(result.is_ok());
285
286 let output = result.unwrap();
287 let output_str = format_and_print(output);
288
289 assert!(output_str.contains("async fn test_async"));
290 assert!(output_str.contains("include_str!"));
291 }
292
293 #[test]
294 fn test_unsafe_function_doc() {
295 let args = quote!(path = "../docs");
296 let item = quote! {
297 unsafe fn test_unsafe() {
298 println!("unsafe test");
299 }
300 };
301
302 let result = syncdoc_impl(args, item);
303 assert!(result.is_ok());
304
305 let output = result.unwrap();
306 let output_str = format_and_print(output);
307
308 assert!(output_str.contains("unsafe fn test_unsafe"));
309 assert!(output_str.contains("include_str!"));
310 }
311
312 #[test]
313 fn test_pub_async_function_doc() {
314 let args = quote!(path = "../docs");
315 let item = quote! {
316 pub async fn test_pub_async() {
317 println!("pub async test");
318 }
319 };
320
321 let result = syncdoc_impl(args, item);
322 assert!(result.is_ok());
323
324 let output = result.unwrap();
325 let output_str = format_and_print(output);
326
327 assert!(output_str.contains("pub async fn test_pub_async"));
328 assert!(output_str.contains("include_str!"));
329 }
330
331 #[test]
332 fn test_direct_file_path() {
333 let args = quote!(path = "../docs/special.md");
334 let item = quote! {
335 fn test_function() {}
336 };
337
338 let result = syncdoc_impl(args, item);
339 assert!(result.is_ok());
340
341 let output = result.unwrap();
342 let output_str = format_and_print(output);
343
344 assert!(output_str.contains("../docs/special.md"));
345 assert!(!output_str.contains("test_function.md"));
346 }
347}