1use crate::discover::ParsedFile;
4use crate::extract::extract_doc_content;
5use std::collections::HashMap;
6use std::fs;
7pub(crate) use std::path::{Path, PathBuf};
8use syncdoc_core::parse::{
9 EnumSig, EnumVariantData, ImplBlockSig, ModuleItem, ModuleSig, StructSig, TraitSig,
10};
11
12pub(crate) mod expected;
13pub use expected::find_expected_doc_paths;
14
15#[derive(Debug, Clone, PartialEq)]
17pub struct DocExtraction {
18 pub markdown_path: PathBuf,
20 pub content: String,
22 pub source_location: String,
24}
25
26impl DocExtraction {
27 pub fn new(markdown_path: PathBuf, mut content: String, source_location: String) -> Self {
29 if !content.ends_with('\n') {
30 content.push('\n');
31 }
32 Self {
33 markdown_path,
34 content,
35 source_location,
36 }
37 }
38}
39
40#[derive(Debug, Default)]
42pub struct WriteReport {
43 pub files_written: usize,
44 pub files_skipped: usize,
45 pub errors: Vec<String>,
46}
47
48pub fn extract_all_docs(parsed: &ParsedFile, docs_root: &str) -> Vec<DocExtraction> {
53 let mut extractions = Vec::new();
54
55 let module_path = syncdoc_core::path_utils::extract_module_path(&parsed.path.to_string_lossy());
57
58 if let Some(inner_doc) = crate::extract::extract_inner_doc_content(&parsed.content.inner_attrs)
60 {
61 let file_stem = parsed
63 .path
64 .file_stem()
65 .and_then(|s| s.to_str())
66 .unwrap_or("module");
67
68 let path = if module_path.is_empty() {
69 format!("{}/{}.md", docs_root, file_stem)
70 } else {
71 format!("{}/{}.md", docs_root, module_path)
72 };
73
74 extractions.push(DocExtraction::new(
75 PathBuf::from(path),
76 inner_doc,
77 format!("{}:1", parsed.path.display()),
78 ));
79 }
80
81 let mut context = Vec::new();
83 if !module_path.is_empty() {
84 context.push(module_path);
85 }
86
87 for item_delimited in &parsed.content.items.0 {
88 let item = &item_delimited.value;
89 extractions.extend(extract_item_docs(
90 item,
91 context.clone(),
92 docs_root,
93 &parsed.path,
94 ));
95 }
96
97 extractions
98}
99
100pub(crate) fn extract_item_docs(
102 item: &ModuleItem,
103 context: Vec<String>,
104 base_path: &str,
105 source_file: &Path,
106) -> Vec<DocExtraction> {
107 let mut extractions = Vec::new();
108
109 match item {
110 ModuleItem::TraitMethod(method_sig) => {
111 if let Some(content) = extract_doc_content(&method_sig.attributes) {
112 let path = build_path(base_path, &context, &method_sig.name.to_string());
113 let location = format!(
114 "{}:{}",
115 source_file.display(),
116 method_sig.name.span().start().line
117 );
118 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
119 }
120 }
121
122 ModuleItem::Function(func_sig) => {
123 if let Some(content) = extract_doc_content(&func_sig.attributes) {
124 let path = build_path(base_path, &context, &func_sig.name.to_string());
125 let location = format!(
126 "{}:{}",
127 source_file.display(),
128 func_sig.name.span().start().line
129 );
130 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
131 }
132 }
133
134 ModuleItem::ImplBlock(impl_block) => {
135 extractions.extend(extract_impl_docs(
136 impl_block,
137 context,
138 base_path,
139 source_file,
140 ));
141 }
142
143 ModuleItem::Module(module) => {
144 extractions.extend(extract_module_docs(module, context, base_path, source_file));
145 }
146
147 ModuleItem::Trait(trait_def) => {
148 extractions.extend(extract_trait_docs(
149 trait_def,
150 context,
151 base_path,
152 source_file,
153 ));
154 }
155
156 ModuleItem::Enum(enum_sig) => {
157 extractions.extend(extract_enum_docs(enum_sig, context, base_path, source_file));
158 }
159
160 ModuleItem::Struct(struct_sig) => {
161 extractions.extend(extract_struct_docs(
162 struct_sig,
163 context,
164 base_path,
165 source_file,
166 ));
167 }
168
169 ModuleItem::TypeAlias(type_alias) => {
170 if let Some(content) = extract_doc_content(&type_alias.attributes) {
171 let path = build_path(base_path, &context, &type_alias.name.to_string());
172 let location = format!(
173 "{}:{}",
174 source_file.display(),
175 type_alias.name.span().start().line
176 );
177 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
178 }
179 }
180
181 ModuleItem::Const(const_sig) => {
182 if let Some(content) = extract_doc_content(&const_sig.attributes) {
183 let path = build_path(base_path, &context, &const_sig.name.to_string());
184 let location = format!(
185 "{}:{}",
186 source_file.display(),
187 const_sig.name.span().start().line
188 );
189 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
190 }
191 }
192
193 ModuleItem::Static(static_sig) => {
194 if let Some(content) = extract_doc_content(&static_sig.attributes) {
195 let path = build_path(base_path, &context, &static_sig.name.to_string());
196 let location = format!(
197 "{}:{}",
198 source_file.display(),
199 static_sig.name.span().start().line
200 );
201 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
202 }
203 }
204
205 ModuleItem::Other(_) => {}
207 }
208
209 extractions
210}
211
212pub(crate) fn extract_impl_docs(
214 impl_block: &ImplBlockSig,
215 context: Vec<String>,
216 base_path: &str,
217 source_file: &Path,
218) -> Vec<DocExtraction> {
219 let mut extractions = Vec::new();
220
221 let impl_context = if let Some(for_trait) = &impl_block.for_trait {
225 let trait_name = if let Some(first) = impl_block.target_type.0.first() {
228 if let proc_macro2::TokenTree::Ident(ident) = &first.value.second {
229 ident.to_string()
230 } else {
231 "Unknown".to_string()
232 }
233 } else {
234 "Unknown".to_string()
235 };
236
237 let type_name = if let Some(first) = for_trait.second.0.first() {
239 if let proc_macro2::TokenTree::Ident(ident) = &first.value.second {
240 ident.to_string()
241 } else {
242 "Unknown".to_string()
243 }
244 } else {
245 "Unknown".to_string()
246 };
247
248 vec![type_name, trait_name]
250 } else {
251 let type_name = if let Some(first) = impl_block.target_type.0.first() {
253 if let proc_macro2::TokenTree::Ident(ident) = &first.value.second {
254 ident.to_string()
255 } else {
256 "Unknown".to_string()
257 }
258 } else {
259 "Unknown".to_string()
260 };
261 vec![type_name]
262 };
263
264 let mut new_context = context;
265 new_context.extend(impl_context);
266
267 let module_content = &impl_block.items.content;
269 for item_delimited in &module_content.items.0 {
270 extractions.extend(extract_item_docs(
271 &item_delimited.value,
272 new_context.clone(),
273 base_path,
274 source_file,
275 ));
276 }
277
278 extractions
279}
280
281pub(crate) fn extract_module_docs(
283 module: &ModuleSig,
284 context: Vec<String>,
285 base_path: &str,
286 source_file: &Path,
287) -> Vec<DocExtraction> {
288 let mut extractions = Vec::new();
289
290 if let Some(content) = extract_doc_content(&module.attributes) {
292 let path = build_path(base_path, &context, &module.name.to_string());
293 let location = format!(
294 "{}:{}",
295 source_file.display(),
296 module.name.span().start().line
297 );
298 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
299 }
300
301 let mut new_context = context;
303 new_context.push(module.name.to_string());
304
305 let module_content = &module.items.content;
307 for item_delimited in &module_content.items.0 {
308 extractions.extend(extract_item_docs(
309 &item_delimited.value,
310 new_context.clone(),
311 base_path,
312 source_file,
313 ));
314 }
315
316 extractions
317}
318
319pub(crate) fn extract_trait_docs(
321 trait_def: &TraitSig,
322 context: Vec<String>,
323 base_path: &str,
324 source_file: &Path,
325) -> Vec<DocExtraction> {
326 let mut extractions = Vec::new();
327
328 if let Some(content) = extract_doc_content(&trait_def.attributes) {
330 let path = build_path(base_path, &context, &trait_def.name.to_string());
331 let location = format!(
332 "{}:{}",
333 source_file.display(),
334 trait_def.name.span().start().line
335 );
336 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
337 }
338
339 let mut new_context = context;
341 new_context.push(trait_def.name.to_string());
342
343 let module_content = &trait_def.items.content;
345 for item_delimited in &module_content.items.0 {
346 extractions.extend(extract_item_docs(
347 &item_delimited.value,
348 new_context.clone(),
349 base_path,
350 source_file,
351 ));
352 }
353
354 extractions
355}
356
357pub(crate) fn extract_enum_docs(
359 enum_sig: &EnumSig,
360 context: Vec<String>,
361 base_path: &str,
362 source_file: &Path,
363) -> Vec<DocExtraction> {
364 let mut extractions = Vec::new();
365 let enum_name = enum_sig.name.to_string();
366
367 if let Some(content) = extract_doc_content(&enum_sig.attributes) {
369 let path = build_path(base_path, &context, &enum_name);
370 let location = format!(
371 "{}:{}",
372 source_file.display(),
373 enum_sig.name.span().start().line
374 );
375 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
376 }
377
378 if let Some(variants_cdv) = enum_sig.variants.content.as_ref() {
380 for variant_delimited in &variants_cdv.0 {
381 let variant = &variant_delimited.value;
382 if let Some(content) = extract_doc_content(&variant.attributes) {
383 let path = build_path(
384 base_path,
385 &context,
386 &format!("{}/{}", enum_name, variant.name),
387 );
388 extractions.push(DocExtraction::new(
389 PathBuf::from(path),
390 content,
391 format!(
392 "{}:{}",
393 source_file.display(),
394 variant.name.span().start().line
395 ),
396 ));
397 }
398
399 if let Some(EnumVariantData::Struct(fields_containing)) = &variant.data {
401 if let Some(fields_cdv) = fields_containing.content.as_ref() {
402 for field_delimited in &fields_cdv.0 {
403 let field = &field_delimited.value;
404 if let Some(content) = extract_doc_content(&field.attributes) {
405 let path = build_path(
406 base_path,
407 &context,
408 &format!("{}/{}/{}", enum_name, variant.name, field.name),
409 );
410 extractions.push(DocExtraction::new(
411 PathBuf::from(path),
412 content,
413 format!(
414 "{}:{}",
415 source_file.display(),
416 field.name.span().start().line
417 ),
418 ));
419 }
420 }
421 }
422 }
423 }
424 }
425
426 extractions
427}
428
429pub(crate) fn extract_struct_docs(
431 struct_sig: &StructSig,
432 context: Vec<String>,
433 base_path: &str,
434 source_file: &Path,
435) -> Vec<DocExtraction> {
436 let mut extractions = Vec::new();
437 let struct_name = struct_sig.name.to_string();
438
439 if let Some(content) = extract_doc_content(&struct_sig.attributes) {
441 let path = build_path(base_path, &context, &struct_name);
442 let location = format!(
443 "{}:{}",
444 source_file.display(),
445 struct_sig.name.span().start().line
446 );
447 extractions.push(DocExtraction::new(PathBuf::from(path), content, location));
448 }
449
450 if let syncdoc_core::parse::StructBody::Named(fields_containing) = &struct_sig.body {
452 if let Some(fields_cdv) = fields_containing.content.as_ref() {
453 for field_delimited in &fields_cdv.0 {
454 let field = &field_delimited.value;
455 if let Some(content) = extract_doc_content(&field.attributes) {
456 let path = build_path(
457 base_path,
458 &context,
459 &format!("{}/{}", struct_name, field.name),
460 );
461 extractions.push(DocExtraction::new(
462 PathBuf::from(path),
463 content,
464 format!(
465 "{}:{}",
466 source_file.display(),
467 field.name.span().start().line
468 ),
469 ));
470 }
471 }
472 }
473 }
474
475 extractions
476}
477
478pub fn write_extractions(
483 extractions: &[DocExtraction],
484 dry_run: bool,
485) -> std::io::Result<WriteReport> {
486 let mut report = WriteReport::default();
487
488 let mut dirs: HashMap<PathBuf, Vec<&DocExtraction>> = HashMap::new();
490 for extraction in extractions {
491 if let Some(parent) = extraction.markdown_path.parent() {
492 dirs.entry(parent.to_path_buf())
493 .or_default()
494 .push(extraction);
495 }
496 }
497
498 for dir in dirs.keys() {
500 if !dry_run {
501 if let Err(e) = fs::create_dir_all(dir) {
502 report.errors.push(format!(
503 "Failed to create directory {}: {}",
504 dir.display(),
505 e
506 ));
507 continue;
508 }
509 }
510 }
511
512 for extraction in extractions {
514 if dry_run {
515 println!("Would write: {}", extraction.markdown_path.display());
516 report.files_written += 1;
517 } else {
518 match fs::write(&extraction.markdown_path, &extraction.content) {
519 Ok(_) => {
520 report.files_written += 1;
521 }
522 Err(e) => {
523 report.errors.push(format!(
524 "Failed to write {}: {}",
525 extraction.markdown_path.display(),
526 e
527 ));
528 report.files_skipped += 1;
529 }
530 }
531 }
532 }
533
534 Ok(report)
535}
536
537pub(crate) fn build_path(base_path: &str, context: &[String], item_name: &str) -> String {
540 let mut parts = vec![base_path.to_string()];
541 parts.extend(context.iter().cloned());
542 parts.push(format!("{}.md", item_name));
543 parts.join("/")
544}