syncdoc_migrate/
discover.rs

1// syncdoc-migrate/src/discover.rs
2
3use crate::config::DocsPathMode;
4use proc_macro2::TokenStream;
5use std::fs;
6use std::path::{Path, PathBuf};
7use std::str::FromStr;
8use syncdoc_core::parse::ModuleContent;
9use unsynn::*;
10
11/// Represents a parsed Rust file with its content and metadata
12#[derive(Debug)]
13pub struct ParsedFile {
14    pub path: PathBuf,
15    pub content: ModuleContent,
16    pub original_source: String,
17}
18
19/// Errors that can occur during file parsing
20#[derive(Debug)]
21pub enum ParseError {
22    IoError(std::io::Error),
23    ParseFailed(String),
24}
25
26impl From<std::io::Error> for ParseError {
27    fn from(err: std::io::Error) -> Self {
28        ParseError::IoError(err)
29    }
30}
31
32impl std::fmt::Display for ParseError {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            ParseError::IoError(e) => write!(f, "IO error: {}", e),
36            ParseError::ParseFailed(msg) => write!(f, "Parse failed: {}", msg),
37        }
38    }
39}
40
41impl std::error::Error for ParseError {}
42
43/// Errors that can occur during configuration
44#[derive(Debug)]
45pub enum ConfigError {
46    IoError(std::io::Error),
47    Other(String),
48}
49
50impl From<std::io::Error> for ConfigError {
51    fn from(err: std::io::Error) -> Self {
52        ConfigError::IoError(err)
53    }
54}
55
56impl std::fmt::Display for ConfigError {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        match self {
59            ConfigError::IoError(e) => write!(f, "IO error: {}", e),
60            ConfigError::Other(msg) => write!(f, "{}", msg),
61        }
62    }
63}
64
65impl std::error::Error for ConfigError {}
66
67/// Recursively discovers all Rust source files in a directory
68///
69/// Returns a sorted vector of absolute paths to `.rs` files for deterministic processing.
70pub fn discover_rust_files(source_dir: &Path) -> std::result::Result<Vec<PathBuf>, std::io::Error> {
71    let mut rust_files = Vec::new();
72    discover_rust_files_recursive(source_dir, &mut rust_files)?;
73    rust_files.sort();
74    Ok(rust_files)
75}
76
77fn discover_rust_files_recursive(
78    dir: &Path,
79    files: &mut Vec<PathBuf>,
80) -> std::result::Result<(), std::io::Error> {
81    for entry in fs::read_dir(dir)? {
82        let entry = entry?;
83        let path = entry.path();
84
85        if path.is_dir() {
86            discover_rust_files_recursive(&path, files)?;
87        } else if path.extension() == Some(std::ffi::OsStr::new("rs")) {
88            files.push(path.canonicalize()?);
89        }
90    }
91    Ok(())
92}
93
94/// Parses a Rust source file into a structured representation
95///
96/// Returns `ParseError::ParseFailed` if the file cannot be parsed, allowing
97/// the caller to skip unparseable files.
98pub fn parse_file(path: &Path) -> std::result::Result<ParsedFile, ParseError> {
99    let original_source = fs::read_to_string(path)?;
100
101    let token_stream = TokenStream::from_str(&original_source)
102        .map_err(|e| ParseError::ParseFailed(format!("Failed to tokenize: {}", e)))?;
103
104    let content = token_stream
105        .into_token_iter()
106        .parse::<ModuleContent>()
107        .map_err(|e| ParseError::ParseFailed(format!("Failed to parse module: {}", e)))?;
108
109    Ok(ParsedFile {
110        path: path.to_path_buf(),
111        content,
112        original_source,
113    })
114}
115
116/// Gets or creates the docs-path configuration
117///
118/// Returns a tuple of (path, mode) where mode indicates whether the path
119/// is configured via TOML or should be inlined.
120///
121/// If the docs-path is not set in Cargo.toml, this function will append
122/// the default configuration and return "docs" (unless dry_run is true).
123pub fn get_or_create_docs_path(
124    source_file: &Path,
125    dry_run: bool,
126) -> std::result::Result<(String, DocsPathMode), ConfigError> {
127    // Try to get existing docs-path
128    match syncdoc_core::config::get_docs_path(source_file.to_str().unwrap()) {
129        Ok(path) => Ok((path, DocsPathMode::TomlConfig)),
130        Err(_) => {
131            // Need to add default docs-path to Cargo.toml
132            if !dry_run {
133                let source_dir = source_file.parent().ok_or_else(|| {
134                    ConfigError::Other("Source file has no parent directory".to_string())
135                })?;
136
137                let manifest_dir = syncdoc_core::path_utils::find_manifest_dir(source_dir)
138                    .ok_or_else(|| ConfigError::Other("Could not find Cargo.toml".to_string()))?;
139
140                let cargo_toml_path = manifest_dir.join("Cargo.toml");
141
142                // Read existing content
143                let mut content = fs::read_to_string(&cargo_toml_path)?;
144
145                // Check if syncdoc section exists
146                if !content.contains("[package.metadata.syncdoc]") {
147                    // Append the section
148                    content.push_str("\n[package.metadata.syncdoc]\n");
149                    content.push_str("docs-path = \"docs\"\n");
150
151                    // Write back
152                    fs::write(&cargo_toml_path, content)?;
153                }
154            }
155
156            // We just created/will create TOML config
157            Ok(("docs".to_string(), DocsPathMode::TomlConfig))
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests;