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;
9pub(crate) use 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
62pub(crate) fn 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::TraitMethod(method_sig) => {
72            let path = build_path(base_path, &context, &method_sig.name.to_string());
73            let location = format!(
74                "{}:{}",
75                source_file.display(),
76                method_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::Function(func_sig) => {
86            let path = build_path(base_path, &context, &func_sig.name.to_string());
87            let location = format!(
88                "{}:{}",
89                source_file.display(),
90                func_sig.name.span().start().line
91            );
92            extractions.push(DocExtraction::new(
93                PathBuf::from(path),
94                String::new(),
95                location,
96            ));
97        }
98
99        ModuleItem::ImplBlock(impl_block) => {
100            extractions.extend(find_impl_paths(impl_block, context, base_path, source_file));
101        }
102
103        ModuleItem::Module(module) => {
104            extractions.extend(find_module_paths(module, context, base_path, source_file));
105        }
106
107        ModuleItem::Trait(trait_def) => {
108            extractions.extend(find_trait_paths(trait_def, context, base_path, source_file));
109        }
110
111        ModuleItem::Enum(enum_sig) => {
112            extractions.extend(find_enum_paths(enum_sig, context, base_path, source_file));
113        }
114
115        ModuleItem::Struct(struct_sig) => {
116            extractions.extend(find_struct_paths(
117                struct_sig,
118                context,
119                base_path,
120                source_file,
121            ));
122        }
123
124        ModuleItem::TypeAlias(type_alias) => {
125            let path = build_path(base_path, &context, &type_alias.name.to_string());
126            let location = format!(
127                "{}:{}",
128                source_file.display(),
129                type_alias.name.span().start().line
130            );
131            extractions.push(DocExtraction::new(
132                PathBuf::from(path),
133                String::new(),
134                location,
135            ));
136        }
137
138        ModuleItem::Const(const_sig) => {
139            let path = build_path(base_path, &context, &const_sig.name.to_string());
140            let location = format!(
141                "{}:{}",
142                source_file.display(),
143                const_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::Static(static_sig) => {
153            let path = build_path(base_path, &context, &static_sig.name.to_string());
154            let location = format!(
155                "{}:{}",
156                source_file.display(),
157                static_sig.name.span().start().line
158            );
159            extractions.push(DocExtraction::new(
160                PathBuf::from(path),
161                String::new(),
162                location,
163            ));
164        }
165
166        ModuleItem::Other(_) => {}
167    }
168
169    extractions
170}
171
172pub(crate) fn find_impl_paths(
173    impl_block: &ImplBlockSig,
174    context: Vec<String>,
175    base_path: &str,
176    source_file: &Path,
177) -> Vec<DocExtraction> {
178    let mut extractions = Vec::new();
179
180    // Determine the context path for the impl block
181    // If this is `impl Trait for Type`, context is [Type, Trait]
182    // If this is `impl Type`, context is [Type]
183    let impl_context = if let Some(for_trait) = &impl_block.for_trait {
184        // This is `impl Trait for Type`
185        // target_type contains the TRAIT name (before "for")
186        let trait_name = if let Some(first) = impl_block.target_type.0.first() {
187            if let proc_macro2::TokenTree::Ident(ident) = &first.value.second {
188                ident.to_string()
189            } else {
190                "Unknown".to_string()
191            }
192        } else {
193            "Unknown".to_string()
194        };
195
196        // for_trait.second contains the TYPE name (after "for")
197        let type_name = if let Some(first) = for_trait.second.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
207        // Context is Type/Trait
208        vec![type_name, trait_name]
209    } else {
210        // This is `impl Type`, extract Type from target_type
211        let type_name = if let Some(first) = impl_block.target_type.0.first() {
212            if let proc_macro2::TokenTree::Ident(ident) = &first.value.second {
213                ident.to_string()
214            } else {
215                "Unknown".to_string()
216            }
217        } else {
218            "Unknown".to_string()
219        };
220        vec![type_name]
221    };
222
223    let mut new_context = context;
224    new_context.extend(impl_context);
225
226    let module_content = &impl_block.items.content;
227    for item_delimited in &module_content.items.0 {
228        extractions.extend(find_item_paths(
229            &item_delimited.value,
230            new_context.clone(),
231            base_path,
232            source_file,
233        ));
234    }
235
236    extractions
237}
238
239pub(crate) fn find_module_paths(
240    module: &ModuleSig,
241    context: Vec<String>,
242    base_path: &str,
243    source_file: &Path,
244) -> Vec<DocExtraction> {
245    let mut extractions = Vec::new();
246
247    let path = build_path(base_path, &context, &module.name.to_string());
248    let location = format!(
249        "{}:{}",
250        source_file.display(),
251        module.name.span().start().line
252    );
253    extractions.push(DocExtraction::new(
254        PathBuf::from(path),
255        String::new(),
256        location,
257    ));
258
259    let mut new_context = context;
260    new_context.push(module.name.to_string());
261
262    let module_content = &module.items.content;
263    for item_delimited in &module_content.items.0 {
264        extractions.extend(find_item_paths(
265            &item_delimited.value,
266            new_context.clone(),
267            base_path,
268            source_file,
269        ));
270    }
271
272    extractions
273}
274
275pub(crate) fn find_trait_paths(
276    trait_def: &TraitSig,
277    context: Vec<String>,
278    base_path: &str,
279    source_file: &Path,
280) -> Vec<DocExtraction> {
281    let mut extractions = Vec::new();
282
283    let path = build_path(base_path, &context, &trait_def.name.to_string());
284    let location = format!(
285        "{}:{}",
286        source_file.display(),
287        trait_def.name.span().start().line
288    );
289    extractions.push(DocExtraction::new(
290        PathBuf::from(path),
291        String::new(),
292        location,
293    ));
294
295    let mut new_context = context;
296    new_context.push(trait_def.name.to_string());
297
298    let module_content = &trait_def.items.content;
299    for item_delimited in &module_content.items.0 {
300        extractions.extend(find_item_paths(
301            &item_delimited.value,
302            new_context.clone(),
303            base_path,
304            source_file,
305        ));
306    }
307
308    extractions
309}
310
311pub(crate) fn find_enum_paths(
312    enum_sig: &EnumSig,
313    context: Vec<String>,
314    base_path: &str,
315    source_file: &Path,
316) -> Vec<DocExtraction> {
317    let mut extractions = Vec::new();
318    let enum_name = enum_sig.name.to_string();
319
320    let path = build_path(base_path, &context, &enum_name);
321    let location = format!(
322        "{}:{}",
323        source_file.display(),
324        enum_sig.name.span().start().line
325    );
326    extractions.push(DocExtraction::new(
327        PathBuf::from(path),
328        String::new(),
329        location,
330    ));
331
332    // Access parsed variants directly
333    if let Some(variants_cdv) = enum_sig.variants.content.as_ref() {
334        for variant_delimited in &variants_cdv.0 {
335            let variant = &variant_delimited.value;
336            let path = build_path(
337                base_path,
338                &context,
339                &format!("{}/{}", enum_name, variant.name),
340            );
341            extractions.push(DocExtraction::new(
342                PathBuf::from(path),
343                String::new(),
344                format!(
345                    "{}:{}",
346                    source_file.display(),
347                    variant.name.span().start().line
348                ),
349            ));
350
351            // Handle struct variant fields
352            if let Some(EnumVariantData::Struct(fields_containing)) = &variant.data {
353                if let Some(fields_cdv) = fields_containing.content.as_ref() {
354                    for field_delimited in &fields_cdv.0 {
355                        let field = &field_delimited.value;
356                        let path = build_path(
357                            base_path,
358                            &context,
359                            &format!("{}/{}/{}", enum_name, variant.name, field.name),
360                        );
361                        extractions.push(DocExtraction::new(
362                            PathBuf::from(path),
363                            String::new(),
364                            format!(
365                                "{}:{}",
366                                source_file.display(),
367                                field.name.span().start().line
368                            ),
369                        ));
370                    }
371                }
372            }
373        }
374    }
375
376    extractions
377}
378
379pub(crate) fn find_struct_paths(
380    struct_sig: &StructSig,
381    context: Vec<String>,
382    base_path: &str,
383    source_file: &Path,
384) -> Vec<DocExtraction> {
385    let mut extractions = Vec::new();
386    let struct_name = struct_sig.name.to_string();
387
388    let path = build_path(base_path, &context, &struct_name);
389    let location = format!(
390        "{}:{}",
391        source_file.display(),
392        struct_sig.name.span().start().line
393    );
394    extractions.push(DocExtraction::new(
395        PathBuf::from(path),
396        String::new(),
397        location,
398    ));
399
400    if let syncdoc_core::parse::StructBody::Named(fields_containing) = &struct_sig.body {
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                let path = build_path(
405                    base_path,
406                    &context,
407                    &format!("{}/{}", struct_name, field.name),
408                );
409                extractions.push(DocExtraction::new(
410                    PathBuf::from(path),
411                    String::new(),
412                    format!(
413                        "{}:{}",
414                        source_file.display(),
415                        field.name.span().start().line
416                    ),
417                ));
418            }
419        }
420    }
421
422    extractions
423}
424
425pub(crate) fn build_path(base_path: &str, context: &[String], item_name: &str) -> String {
426    let mut parts = vec![base_path.to_string()];
427    parts.extend(context.iter().cloned());
428    parts.push(format!("{}.md", item_name));
429    parts.join("/")
430}