syncdoc_migrate/write/
expected.rs

1//! Expected documentation paths based on code structure
2//!
3//! This module discovers what documentation files should exist based on the
4//! structure of Rust source code, regardless of whether documentation comments
5//! are present. It's used to identify missing documentation files that should
6//! be created.
7
8use crate::write::DocExtraction;
9use std::path::{Path, PathBuf};
10use syncdoc_core::parse::{
11    EnumSig, EnumVariantData, ImplBlockSig, ModuleItem, ModuleSig, StructSig, TraitSig,
12};
13
14use super::ParsedFile;
15
16/// Finds all documentation paths that are expected based on code structure
17///
18/// Returns a vector of `DocExtraction` structs with empty content, representing
19/// the markdown files that should exist for the given source file's structure.
20pub fn find_expected_doc_paths(parsed: &ParsedFile, docs_root: &str) -> Vec<DocExtraction> {
21    let mut extractions = Vec::new();
22    let module_path = syncdoc_core::path_utils::extract_module_path(&parsed.path.to_string_lossy());
23
24    // Module-level documentation path
25    let file_stem = parsed
26        .path
27        .file_stem()
28        .and_then(|s| s.to_str())
29        .unwrap_or("module");
30
31    let path = if module_path.is_empty() {
32        format!("{}/{}.md", docs_root, file_stem)
33    } else {
34        format!("{}/{}.md", docs_root, module_path)
35    };
36
37    extractions.push(DocExtraction::new(
38        PathBuf::from(path),
39        String::new(),
40        format!("{}:1", parsed.path.display()),
41    ));
42
43    let mut context = Vec::new();
44    if !module_path.is_empty() {
45        context.push(module_path);
46    }
47
48    // Find all item documentation paths
49    for item_delimited in &parsed.content.items.0 {
50        extractions.extend(find_item_paths(
51            &item_delimited.value,
52            context.clone(),
53            docs_root,
54            &parsed.path,
55        ));
56    }
57
58    extractions
59}
60
61/// Recursively finds documentation paths for a single item
62fn find_item_paths(
63    item: &ModuleItem,
64    context: Vec<String>,
65    base_path: &str,
66    source_file: &Path,
67) -> Vec<DocExtraction> {
68    let mut extractions = Vec::new();
69
70    match item {
71        ModuleItem::Function(func_sig) => {
72            let path = build_path(base_path, &context, &func_sig.name.to_string());
73            let location = format!(
74                "{}:{}",
75                source_file.display(),
76                func_sig.name.span().start().line
77            );
78            extractions.push(DocExtraction::new(
79                PathBuf::from(path),
80                String::new(),
81                location,
82            ));
83        }
84
85        ModuleItem::ImplBlock(impl_block) => {
86            extractions.extend(find_impl_paths(impl_block, context, base_path, source_file));
87        }
88
89        ModuleItem::Module(module) => {
90            extractions.extend(find_module_paths(module, context, base_path, source_file));
91        }
92
93        ModuleItem::Trait(trait_def) => {
94            extractions.extend(find_trait_paths(trait_def, context, base_path, source_file));
95        }
96
97        ModuleItem::Enum(enum_sig) => {
98            extractions.extend(find_enum_paths(enum_sig, context, base_path, source_file));
99        }
100
101        ModuleItem::Struct(struct_sig) => {
102            extractions.extend(find_struct_paths(
103                struct_sig,
104                context,
105                base_path,
106                source_file,
107            ));
108        }
109
110        ModuleItem::TypeAlias(type_alias) => {
111            let path = build_path(base_path, &context, &type_alias.name.to_string());
112            let location = format!(
113                "{}:{}",
114                source_file.display(),
115                type_alias.name.span().start().line
116            );
117            extractions.push(DocExtraction::new(
118                PathBuf::from(path),
119                String::new(),
120                location,
121            ));
122        }
123
124        ModuleItem::Const(const_sig) => {
125            let path = build_path(base_path, &context, &const_sig.name.to_string());
126            let location = format!(
127                "{}:{}",
128                source_file.display(),
129                const_sig.name.span().start().line
130            );
131            extractions.push(DocExtraction::new(
132                PathBuf::from(path),
133                String::new(),
134                location,
135            ));
136        }
137
138        ModuleItem::Static(static_sig) => {
139            let path = build_path(base_path, &context, &static_sig.name.to_string());
140            let location = format!(
141                "{}:{}",
142                source_file.display(),
143                static_sig.name.span().start().line
144            );
145            extractions.push(DocExtraction::new(
146                PathBuf::from(path),
147                String::new(),
148                location,
149            ));
150        }
151
152        ModuleItem::Other(_) => {}
153    }
154
155    extractions
156}
157
158fn find_impl_paths(
159    impl_block: &ImplBlockSig,
160    context: Vec<String>,
161    base_path: &str,
162    source_file: &Path,
163) -> Vec<DocExtraction> {
164    let mut extractions = Vec::new();
165
166    // Determine the context path for the impl block
167    // If this is `impl Trait for Type`, context is [Type, Trait]
168    // If this is `impl Type`, context is [Type]
169    let impl_context = if let Some(for_trait) = &impl_block.for_trait {
170        // This is `impl Trait for Type`
171        // target_type contains the TRAIT name (before "for")
172        let trait_name = if let Some(first) = impl_block.target_type.0.first() {
173            if let proc_macro2::TokenTree::Ident(ident) = &first.value.second {
174                ident.to_string()
175            } else {
176                "Unknown".to_string()
177            }
178        } else {
179            "Unknown".to_string()
180        };
181
182        // for_trait.second contains the TYPE name (after "for")
183        let type_name = if let Some(first) = for_trait.second.0.first() {
184            if let proc_macro2::TokenTree::Ident(ident) = &first.value.second {
185                ident.to_string()
186            } else {
187                "Unknown".to_string()
188            }
189        } else {
190            "Unknown".to_string()
191        };
192
193        // Context is Type/Trait
194        vec![type_name, trait_name]
195    } else {
196        // This is `impl Type`, extract Type from target_type
197        let type_name = if let Some(first) = impl_block.target_type.0.first() {
198            if let proc_macro2::TokenTree::Ident(ident) = &first.value.second {
199                ident.to_string()
200            } else {
201                "Unknown".to_string()
202            }
203        } else {
204            "Unknown".to_string()
205        };
206        vec![type_name]
207    };
208
209    let mut new_context = context;
210    new_context.extend(impl_context);
211
212    let module_content = &impl_block.items.content;
213    for item_delimited in &module_content.items.0 {
214        extractions.extend(find_item_paths(
215            &item_delimited.value,
216            new_context.clone(),
217            base_path,
218            source_file,
219        ));
220    }
221
222    extractions
223}
224
225fn find_module_paths(
226    module: &ModuleSig,
227    context: Vec<String>,
228    base_path: &str,
229    source_file: &Path,
230) -> Vec<DocExtraction> {
231    let mut extractions = Vec::new();
232
233    let path = build_path(base_path, &context, &module.name.to_string());
234    let location = format!(
235        "{}:{}",
236        source_file.display(),
237        module.name.span().start().line
238    );
239    extractions.push(DocExtraction::new(
240        PathBuf::from(path),
241        String::new(),
242        location,
243    ));
244
245    let mut new_context = context;
246    new_context.push(module.name.to_string());
247
248    let module_content = &module.items.content;
249    for item_delimited in &module_content.items.0 {
250        extractions.extend(find_item_paths(
251            &item_delimited.value,
252            new_context.clone(),
253            base_path,
254            source_file,
255        ));
256    }
257
258    extractions
259}
260
261fn find_trait_paths(
262    trait_def: &TraitSig,
263    context: Vec<String>,
264    base_path: &str,
265    source_file: &Path,
266) -> Vec<DocExtraction> {
267    let mut extractions = Vec::new();
268
269    let path = build_path(base_path, &context, &trait_def.name.to_string());
270    let location = format!(
271        "{}:{}",
272        source_file.display(),
273        trait_def.name.span().start().line
274    );
275    extractions.push(DocExtraction::new(
276        PathBuf::from(path),
277        String::new(),
278        location,
279    ));
280
281    let mut new_context = context;
282    new_context.push(trait_def.name.to_string());
283
284    let module_content = &trait_def.items.content;
285    for item_delimited in &module_content.items.0 {
286        extractions.extend(find_item_paths(
287            &item_delimited.value,
288            new_context.clone(),
289            base_path,
290            source_file,
291        ));
292    }
293
294    extractions
295}
296
297fn find_enum_paths(
298    enum_sig: &EnumSig,
299    context: Vec<String>,
300    base_path: &str,
301    source_file: &Path,
302) -> Vec<DocExtraction> {
303    let mut extractions = Vec::new();
304    let enum_name = enum_sig.name.to_string();
305
306    let path = build_path(base_path, &context, &enum_name);
307    let location = format!(
308        "{}:{}",
309        source_file.display(),
310        enum_sig.name.span().start().line
311    );
312    extractions.push(DocExtraction::new(
313        PathBuf::from(path),
314        String::new(),
315        location,
316    ));
317
318    // Access parsed variants directly
319    if let Some(variants_cdv) = enum_sig.variants.content.as_ref() {
320        for variant_delimited in &variants_cdv.0 {
321            let variant = &variant_delimited.value;
322            let path = build_path(
323                base_path,
324                &context,
325                &format!("{}/{}", enum_name, variant.name),
326            );
327            extractions.push(DocExtraction::new(
328                PathBuf::from(path),
329                String::new(),
330                format!(
331                    "{}:{}",
332                    source_file.display(),
333                    variant.name.span().start().line
334                ),
335            ));
336
337            // Handle struct variant fields
338            if let Some(EnumVariantData::Struct(fields_containing)) = &variant.data {
339                if let Some(fields_cdv) = fields_containing.content.as_ref() {
340                    for field_delimited in &fields_cdv.0 {
341                        let field = &field_delimited.value;
342                        let path = build_path(
343                            base_path,
344                            &context,
345                            &format!("{}/{}/{}", enum_name, variant.name, field.name),
346                        );
347                        extractions.push(DocExtraction::new(
348                            PathBuf::from(path),
349                            String::new(),
350                            format!(
351                                "{}:{}",
352                                source_file.display(),
353                                field.name.span().start().line
354                            ),
355                        ));
356                    }
357                }
358            }
359        }
360    }
361
362    extractions
363}
364
365fn find_struct_paths(
366    struct_sig: &StructSig,
367    context: Vec<String>,
368    base_path: &str,
369    source_file: &Path,
370) -> Vec<DocExtraction> {
371    let mut extractions = Vec::new();
372    let struct_name = struct_sig.name.to_string();
373
374    let path = build_path(base_path, &context, &struct_name);
375    let location = format!(
376        "{}:{}",
377        source_file.display(),
378        struct_sig.name.span().start().line
379    );
380    extractions.push(DocExtraction::new(
381        PathBuf::from(path),
382        String::new(),
383        location,
384    ));
385
386    if let syncdoc_core::parse::StructBody::Named(fields_containing) = &struct_sig.body {
387        if let Some(fields_cdv) = fields_containing.content.as_ref() {
388            for field_delimited in &fields_cdv.0 {
389                let field = &field_delimited.value;
390                let path = build_path(
391                    base_path,
392                    &context,
393                    &format!("{}/{}", struct_name, field.name),
394                );
395                extractions.push(DocExtraction::new(
396                    PathBuf::from(path),
397                    String::new(),
398                    format!(
399                        "{}:{}",
400                        source_file.display(),
401                        field.name.span().start().line
402                    ),
403                ));
404            }
405        }
406    }
407
408    extractions
409}
410
411fn build_path(base_path: &str, context: &[String], item_name: &str) -> String {
412    let mut parts = vec![base_path.to_string()];
413    parts.extend(context.iter().cloned());
414    parts.push(format!("{}.md", item_name));
415    parts.join("/")
416}
417
418#[cfg(test)]
419mod expected_tests;