cargo_docs_md/source/
mod.rs

1//! Source code parsing for enhanced documentation.
2//!
3//! This module provides functionality to collect and parse Rust source code
4//! from dependencies, extracting information not available in rustdoc JSON:
5//!
6//! - Function bodies and implementation details
7//! - Private items (functions, structs, etc.)
8//! - Constant and static values
9//! - Macro definitions
10//! - Test examples
11//!
12//! # Architecture
13//!
14//! The source parsing system has four main components:
15//!
16//! 1. [`SourceCollector`] - Collects dependency sources to `.source_*/`
17//! 2. [`SourceLocator`] - Finds crate sources in the Cargo registry
18//! 3. [`SourceParser`] - Parses Rust source files using `syn`
19//! 4. [`types`] - Data structures for parsed source information
20//!
21//! # Workflow
22//!
23//! ```no_run
24//! use cargo_docs_md::source::{SourceCollector, CollectOptions};
25//!
26//! // Collect dependency sources to .source_{timestamp}/
27//! let collector = SourceCollector::new()?;
28//! let result = collector.collect(&CollectOptions::default())?;
29//! println!("Collected {} crates to {}", result.crates_collected, result.output_dir.display());
30//! # Ok::<(), cargo_docs_md::error::Error>(())
31//! ```
32//!
33//! # Feature Flag
34//!
35//! This module requires the `source-parsing` feature:
36//!
37//! ```toml
38//! cargo-docs-md = { version = "0.1", features = ["source-parsing"] }
39//! ```
40
41use std::fs;
42use std::path::{Path, PathBuf};
43use std::result::Result;
44
45mod collector;
46mod integration;
47mod locator;
48mod parser;
49pub mod types;
50
51pub use collector::{
52    CollectOptions, CollectedCrate, CollectionResult, SourceCollector, SourceManifest,
53};
54pub use locator::SourceLocator;
55pub use parser::SourceParser;
56pub use types::{
57    ConstInfo, CrateSource, EnumInfo, FieldInfo, FunctionInfo, ImplInfo, MacroInfo, PrivateItem,
58    StaticInfo, StructInfo, TraitInfo, TypeAliasInfo, VariantInfo,
59};
60
61/// Find the most recent `.source_*` directory in the given root.
62///
63/// Scans for directories matching `.source_*` pattern and returns
64/// the one with the highest timestamp (most recent).
65///
66/// # Arguments
67///
68/// * `root` - Directory to search in (typically workspace or project root)
69///
70/// # Returns
71///
72/// Path to the most recent `.source_*` directory, or `None` if not found.
73///
74/// # Example
75///
76/// ```no_run
77/// use std::path::Path;
78/// use cargo_docs_md::source::find_source_dir;
79///
80/// if let Some(source_dir) = find_source_dir(Path::new(".")) {
81///     println!("Found source directory: {}", source_dir.display());
82/// }
83/// ```
84#[must_use]
85pub fn find_source_dir(root: &Path) -> Option<PathBuf> {
86    let entries = fs::read_dir(root).ok()?;
87
88    let mut source_dirs: Vec<PathBuf> = entries
89        .filter_map(Result::ok)
90        .map(|e| e.path())
91        .filter(|p| {
92            p.is_dir()
93                && p.file_name()
94                    .and_then(|n| n.to_str())
95                    .is_some_and(|n| n.starts_with(".source_"))
96        })
97        .collect();
98
99    // Sort by timestamp descending (higher timestamp = more recent)
100    // Directory names are `.source_{timestamp}` so lexicographic sort works
101    source_dirs.sort_by(|a, b| {
102        let ts_a = extract_source_timestamp(a);
103        let ts_b = extract_source_timestamp(b);
104        ts_b.cmp(&ts_a)
105    });
106
107    source_dirs.into_iter().next()
108}
109
110/// Extract timestamp from a `.source_*` directory path.
111///
112/// Returns the numeric timestamp suffix, or 0 if parsing fails.
113fn extract_source_timestamp(path: &Path) -> u64 {
114    path.file_name()
115        .and_then(|n| n.to_str())
116        .and_then(|s| s.strip_prefix(".source_"))
117        .and_then(|s| s.parse().ok())
118        .unwrap_or(0)
119}
120
121#[cfg(test)]
122mod tests {
123    use std::fs as StdFs;
124
125    use tempfile::TempDir;
126
127    use super::*;
128
129    #[test]
130    fn find_source_dir_returns_none_for_empty_dir() {
131        let temp = TempDir::new().unwrap();
132        let result = find_source_dir(temp.path());
133        assert!(result.is_none());
134    }
135
136    #[test]
137    fn find_source_dir_finds_single_source_dir() {
138        let temp = TempDir::new().unwrap();
139        StdFs::create_dir(temp.path().join(".source_12345")).unwrap();
140
141        let result = find_source_dir(temp.path());
142        assert!(result.is_some());
143        assert!(result.unwrap().ends_with(".source_12345"));
144    }
145
146    #[test]
147    fn find_source_dir_returns_most_recent() {
148        let temp = TempDir::new().unwrap();
149        StdFs::create_dir(temp.path().join(".source_10000")).unwrap();
150        StdFs::create_dir(temp.path().join(".source_99999")).unwrap();
151        StdFs::create_dir(temp.path().join(".source_50000")).unwrap();
152
153        let result = find_source_dir(temp.path());
154        assert!(result.is_some());
155        assert!(result.unwrap().ends_with(".source_99999"));
156    }
157
158    #[test]
159    fn find_source_dir_ignores_non_source_dirs() {
160        let temp = TempDir::new().unwrap();
161        StdFs::create_dir(temp.path().join("source_12345")).unwrap(); // No dot
162        StdFs::create_dir(temp.path().join(".sourcecode")).unwrap(); // No underscore
163        StdFs::create_dir(temp.path().join("src")).unwrap();
164
165        let result = find_source_dir(temp.path());
166        assert!(result.is_none());
167    }
168
169    #[test]
170    fn extract_source_timestamp_parses_valid() {
171        let path = PathBuf::from("/project/.source_1733660400");
172        assert_eq!(extract_source_timestamp(&path), 1_733_660_400);
173    }
174
175    #[test]
176    fn extract_source_timestamp_returns_zero_for_invalid() {
177        assert_eq!(
178            extract_source_timestamp(&PathBuf::from("/project/.source_abc")),
179            0
180        );
181        assert_eq!(
182            extract_source_timestamp(&PathBuf::from("/project/source_123")),
183            0
184        );
185    }
186}