Skip to main content

xbp_analysis/adapters/
filesystem.rs

1//! Filesystem discovery and source loading.
2
3use crate::application::suppress::path_matches_glob;
4use crate::domain::types::{LanguageId, SourceFile};
5use crate::ports::outbound::{SourceLoader, WorkspaceDiscovery};
6use std::fs;
7use std::path::{Path, PathBuf};
8
9#[derive(Default)]
10pub struct IgnoreWalkDiscovery;
11
12impl WorkspaceDiscovery for IgnoreWalkDiscovery {
13    fn discover(
14        &self,
15        root: &Path,
16        languages: &[LanguageId],
17        exclusions: &[String],
18    ) -> Result<Vec<PathBuf>, String> {
19        let mut out = Vec::new();
20        let walker = ignore::WalkBuilder::new(root)
21            .hidden(false)
22            .git_ignore(true)
23            .git_global(true)
24            .git_exclude(true)
25            .build();
26
27        for entry in walker {
28            let entry = entry.map_err(|e| e.to_string())?;
29            let path = entry.path();
30            if !path.is_file() {
31                continue;
32            }
33            let rel = path
34                .strip_prefix(root)
35                .unwrap_or(path)
36                .to_string_lossy()
37                .replace('\\', "/");
38            if exclusions.iter().any(|ex| path_matches_glob(&rel, ex)) {
39                continue;
40            }
41            // Also match absolute path exclusions
42            let abs = path.to_string_lossy().replace('\\', "/");
43            if exclusions.iter().any(|ex| path_matches_glob(&abs, ex)) {
44                continue;
45            }
46            if languages.iter().any(|l| language_matches(l, path)) {
47                out.push(path.to_path_buf());
48            }
49        }
50        out.sort();
51        Ok(out)
52    }
53}
54
55fn language_matches(lang: &LanguageId, path: &Path) -> bool {
56    let ext = path
57        .extension()
58        .and_then(|e| e.to_str())
59        .unwrap_or("")
60        .to_ascii_lowercase();
61    match lang.as_str() {
62        "rust" => ext == "rs",
63        "typescript" | "ts" => ext == "ts" || ext == "tsx",
64        "javascript" | "js" => ext == "js" || ext == "jsx" || ext == "mjs" || ext == "cjs",
65        "python" | "py" => ext == "py",
66        "go" => ext == "go",
67        "java" => ext == "java",
68        _ => false,
69    }
70}
71
72pub struct FsSourceLoader;
73
74impl SourceLoader for FsSourceLoader {
75    fn load(&self, path: &Path, language: LanguageId) -> Result<SourceFile, String> {
76        let content = fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?;
77        Ok(SourceFile {
78            path: path.to_path_buf(),
79            content,
80            language,
81        })
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use std::io::Write;
89    use tempfile::tempdir;
90
91    #[test]
92    fn discovers_rust_files() {
93        let dir = tempdir().unwrap();
94        let src = dir.path().join("src");
95        fs::create_dir_all(&src).unwrap();
96        let mut f = fs::File::create(src.join("lib.rs")).unwrap();
97        writeln!(f, "fn main() {{}}").unwrap();
98        fs::create_dir_all(dir.path().join("target/debug")).unwrap();
99        fs::write(dir.path().join("target/debug/x.rs"), "x").unwrap();
100
101        let d = IgnoreWalkDiscovery;
102        let paths = d
103            .discover(
104                dir.path(),
105                &[LanguageId::rust()],
106                &["**/target/**".into()],
107            )
108            .unwrap();
109        assert_eq!(paths.len(), 1);
110        assert!(paths[0].ends_with("lib.rs"));
111    }
112}