xbp-analysis 10.57.0

Language-agnostic static analysis (error-handling / Aspirator-style) for XBP
Documentation
//! Filesystem discovery and source loading.

use crate::application::suppress::path_matches_glob;
use crate::domain::types::{LanguageId, SourceFile};
use crate::ports::outbound::{SourceLoader, WorkspaceDiscovery};
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Default)]
pub struct IgnoreWalkDiscovery;

impl WorkspaceDiscovery for IgnoreWalkDiscovery {
    fn discover(
        &self,
        root: &Path,
        languages: &[LanguageId],
        exclusions: &[String],
    ) -> Result<Vec<PathBuf>, String> {
        let mut out = Vec::new();
        let walker = ignore::WalkBuilder::new(root)
            .hidden(false)
            .git_ignore(true)
            .git_global(true)
            .git_exclude(true)
            .build();

        for entry in walker {
            let entry = entry.map_err(|e| e.to_string())?;
            let path = entry.path();
            if !path.is_file() {
                continue;
            }
            let rel = path
                .strip_prefix(root)
                .unwrap_or(path)
                .to_string_lossy()
                .replace('\\', "/");
            if exclusions.iter().any(|ex| path_matches_glob(&rel, ex)) {
                continue;
            }
            // Also match absolute path exclusions
            let abs = path.to_string_lossy().replace('\\', "/");
            if exclusions.iter().any(|ex| path_matches_glob(&abs, ex)) {
                continue;
            }
            if languages.iter().any(|l| language_matches(l, path)) {
                out.push(path.to_path_buf());
            }
        }
        out.sort();
        Ok(out)
    }
}

fn language_matches(lang: &LanguageId, path: &Path) -> bool {
    let ext = path
        .extension()
        .and_then(|e| e.to_str())
        .unwrap_or("")
        .to_ascii_lowercase();
    match lang.as_str() {
        "rust" => ext == "rs",
        "typescript" | "ts" => ext == "ts" || ext == "tsx",
        "javascript" | "js" => ext == "js" || ext == "jsx" || ext == "mjs" || ext == "cjs",
        "python" | "py" => ext == "py",
        "go" => ext == "go",
        "java" => ext == "java",
        _ => false,
    }
}

pub struct FsSourceLoader;

impl SourceLoader for FsSourceLoader {
    fn load(&self, path: &Path, language: LanguageId) -> Result<SourceFile, String> {
        let content = fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?;
        Ok(SourceFile {
            path: path.to_path_buf(),
            content,
            language,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::tempdir;

    #[test]
    fn discovers_rust_files() {
        let dir = tempdir().unwrap();
        let src = dir.path().join("src");
        fs::create_dir_all(&src).unwrap();
        let mut f = fs::File::create(src.join("lib.rs")).unwrap();
        writeln!(f, "fn main() {{}}").unwrap();
        fs::create_dir_all(dir.path().join("target/debug")).unwrap();
        fs::write(dir.path().join("target/debug/x.rs"), "x").unwrap();

        let d = IgnoreWalkDiscovery;
        let paths = d
            .discover(
                dir.path(),
                &[LanguageId::rust()],
                &["**/target/**".into()],
            )
            .unwrap();
        assert_eq!(paths.len(), 1);
        assert!(paths[0].ends_with("lib.rs"));
    }
}