1use crate::facts::RawNode;
44use icb_common::{IcbError, Language};
45use std::path::{Path, PathBuf};
46use walkdir::WalkDir;
47
48#[derive(Default)]
50pub struct ParserManager;
51
52impl ParserManager {
53 pub fn new() -> Self {
55 Self
56 }
57
58 pub fn parse_file(&self, lang: Language, source: &str) -> Result<Vec<RawNode>, IcbError> {
72 match lang {
73 Language::Python => crate::lang::python::parse_python(source),
74 Language::CppTreeSitter => crate::cpp_tree_sitter::parse_cpp_file(source),
75 Language::Go => crate::lang::go::parse_go(source),
76 Language::Ruby => crate::lang::ruby::parse_ruby(source),
77
78 Language::JavaScript | Language::Rust | Language::Unknown => {
79 Ok(crate::heuristic_parser::parse_universal(source, ""))
80 }
81
82 Language::Cpp => Ok(crate::heuristic_parser::parse_universal(source, "")),
83
84 _ => Ok(crate::heuristic_parser::parse_universal(source, "")),
85 }
86 }
87
88 pub fn parse_directory(
105 &self,
106 lang: Language,
107 root: &Path,
108 ) -> Result<Vec<(String, Vec<RawNode>)>, IcbError> {
109 let files = discover_files(root, lang)?;
110 let base = normalize_root(root);
111 let mut results = Vec::with_capacity(files.len());
112
113 for path in files {
114 match process_file(self, lang, &path, &base) {
115 Ok(Some(entry)) => results.push(entry),
116 Ok(None) | Err(_) => continue,
117 }
118 }
119
120 Ok(results)
121 }
122}
123
124fn discover_files(root: &Path, lang: Language) -> Result<Vec<PathBuf>, IcbError> {
129 let extensions = extensions_for_language(lang);
130 let mut out = Vec::new();
131
132 for entry in WalkDir::new(root).follow_links(false) {
133 let entry = match entry {
134 Ok(e) => e,
135 Err(e) => return Err(IcbError::Parse(e.to_string())),
136 };
137 if !entry.file_type().is_file() {
138 continue;
139 }
140 let path = entry.path();
141 if should_include(path, &extensions) {
142 out.push(path.to_path_buf());
143 }
144 }
145
146 Ok(out)
147}
148
149fn process_file(
151 manager: &ParserManager,
152 lang: Language,
153 path: &Path,
154 base: &Path,
155) -> Result<Option<(String, Vec<RawNode>)>, IcbError> {
156 let source = match std::fs::read_to_string(path) {
157 Ok(s) => s,
158 Err(_) => return Ok(None),
159 };
160 let facts = match manager.parse_file(lang, &source) {
161 Ok(f) => f,
162 Err(_) => return Ok(None),
163 };
164 if facts.is_empty() {
165 return Ok(None);
166 }
167 let rel = relative_path(path, base);
168 Ok(Some((rel, facts)))
169}
170
171fn normalize_root(root: &Path) -> PathBuf {
173 root.canonicalize().unwrap_or_else(|_| root.to_path_buf())
174}
175
176fn relative_path(path: &Path, base: &Path) -> String {
178 path.strip_prefix(base)
179 .unwrap_or(path)
180 .to_string_lossy()
181 .to_string()
182}
183
184fn should_include(path: &Path, exts: &[&str]) -> bool {
188 if exts.is_empty() {
189 return true;
190 }
191 match path.extension().and_then(|s| s.to_str()) {
192 Some(ext) => {
193 let ext = ext.to_lowercase();
194 exts.iter().any(|e| *e == ext)
195 }
196 None => false,
197 }
198}
199
200fn extensions_for_language(lang: Language) -> Vec<&'static str> {
202 match lang {
203 Language::Python => vec!["py"],
204 Language::Cpp | Language::CppTreeSitter => vec![
205 "c", "cpp", "cc", "cxx", "h", "hpp", "hxx", "hh", "inl", "inc",
206 ],
207 Language::Rust => vec!["rs"],
208 Language::JavaScript => vec!["js", "jsx", "ts", "tsx"],
209 Language::Go => vec!["go"],
210 Language::Java => vec!["java"],
211 Language::Ruby => vec!["rb"],
212 Language::Php => vec!["php"],
213 Language::Swift => vec!["swift"],
214 Language::Kotlin => vec!["kt", "kts"],
215 Language::Scala => vec!["scala"],
216 Language::CSharp => vec!["cs"],
217 Language::Lua => vec!["lua"],
218 Language::R => vec!["r"],
219 Language::Bash => vec!["sh", "bash"],
220 Language::Perl => vec!["pl", "pm"],
221 Language::Tcl => vec!["tcl"],
222 Language::Dart => vec!["dart"],
223 Language::Unknown => vec![],
224 }
225}