morph-cli 0.1.0

AST-based codebase migration and codemod tool for JavaScript and TypeScript projects.
Documentation
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

pub struct FixtureRepo {
    pub name: String,
    pub path: PathBuf,
    pub files: HashMap<PathBuf, FixtureFile>,
}

pub struct FixtureFile {
    pub path: PathBuf,
    pub content: String,
    pub language: String,
}

impl FixtureRepo {
    pub fn new(name: &str, path: PathBuf) -> Self {
        Self {
            name: name.to_string(),
            path,
            files: HashMap::new(),
        }
    }

    pub fn load(&mut self) -> std::io::Result<()> {
        if !self.path.exists() {
            return Ok(());
        }

        for entry in walkdir::WalkDir::new(&self.path)
            .into_iter()
            .filter_map(|e| e.ok())
        {
            if entry.file_type().is_file() {
                if let Ok(content) = fs::read_to_string(entry.path()) {
                    let language = detect_language(entry.path());
                    self.files.insert(
                        entry.path().to_path_buf(),
                        FixtureFile {
                            path: entry.path().to_path_buf(),
                            content,
                            language,
                        },
                    );
                }
            }
        }

        Ok(())
    }

    pub fn get_file(&self, relative: &str) -> Option<&FixtureFile> {
        let path = self.path.join(relative);
        self.files.get(&path)
    }

    pub fn list_files(&self) -> Vec<&FixtureFile> {
        self.files.values().collect()
    }
}

fn detect_language(path: &Path) -> String {
    match path.extension().and_then(|e| e.to_str()) {
        Some("js") => "javascript".to_string(),
        Some("jsx") => "javascript".to_string(),
        Some("ts") => "typescript".to_string(),
        Some("tsx") => "typescript".to_string(),
        Some("json") => "json".to_string(),
        Some("md") => "markdown".to_string(),
        Some("rs") => "rust".to_string(),
        _ => "unknown".to_string(),
    }
}

pub fn load_fixture_repo(name: &str, fixtures_path: &Path) -> Option<FixtureRepo> {
    let repo_path = fixtures_path.join(name);
    if !repo_path.exists() {
        return None;
    }

    let mut repo = FixtureRepo::new(name, repo_path);
    repo.load().ok()?;
    Some(repo)
}

pub fn list_fixture_repos(fixtures_path: &Path) -> Vec<String> {
    let mut repos = Vec::new();

    if let Ok(entries) = fs::read_dir(fixtures_path) {
        for entry in entries.filter_map(|e| e.ok()) {
            if entry.path().is_dir() {
                if let Some(name) = entry.path().file_name().and_then(|n| n.to_str()) {
                    repos.push(name.to_string());
                }
            }
        }
    }

    repos
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;

    #[test]
    fn test_fixture_repo_new() {
        let dir = TempDir::new().unwrap();
        let repo = FixtureRepo::new("test", dir.path().to_path_buf());
        assert_eq!(repo.name, "test");
        assert!(repo.files.is_empty());
    }

    #[test]
    fn test_detect_language_js() {
        let path = Path::new("test.js");
        assert_eq!(detect_language(path), "javascript");
    }

    #[test]
    fn test_detect_language_ts() {
        let path = Path::new("test.ts");
        assert_eq!(detect_language(path), "typescript");
    }

    #[test]
    fn test_detect_language_unknown() {
        let path = Path::new("test.xyz");
        assert_eq!(detect_language(path), "unknown");
    }

    #[test]
    fn test_list_fixture_repos_empty() {
        let dir = TempDir::new().unwrap();
        let repos = list_fixture_repos(dir.path());
        assert!(repos.is_empty());
    }
}