xbp_analysis/adapters/
filesystem.rs1use 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 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}