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
225pub 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}