syncdoc_migrate/
discover.rs

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