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