code_repo_wiki/ingest/parser/
mod.rs1mod rust;
2mod typescript;
3mod python;
4mod go;
5mod javascript;
6mod csharp;
7mod java;
8
9use std::path::{Path, PathBuf};
10use anyhow::Result;
11use serde::{Deserialize, Serialize};
12use tree_sitter::{Language, Node, Parser};
13
14pub const SUPPORTED_EXTENSIONS: &[&str] = &[
17 ".rs", ".ts", ".tsx", ".py", ".go", ".js", ".jsx", ".mjs", ".cjs", ".cs", ".java",
18];
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct FileInsight {
26 pub path: PathBuf,
27 pub language: String,
28 pub entities: Vec<Entity>,
29 pub imports: Vec<ImportStmt>,
30 pub doc_comments: Vec<String>,
31 pub source: String,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Entity {
38 pub name: String,
39 pub kind: String,
41 pub line_start: usize,
43 pub line_end: usize,
45 pub doc_comment: Option<String>,
46 pub signature: Option<String>,
47 #[serde(default)]
54 pub visibility: Option<String>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ImportStmt {
60 pub source: String,
62 pub alias: Option<String>,
64 pub line: usize,
66}
67
68pub trait LanguageProcessor: Send + Sync {
72 fn name(&self) -> &'static str;
73 fn extensions(&self) -> &[&str];
74 fn parse(&self, source: &str, path: &Path) -> Result<FileInsight>;
75}
76
77#[derive(Debug, Clone, Copy)]
82pub struct KindRule {
83 pub node_kind: &'static str,
85 pub entity_kind: &'static str,
87 pub with_signature: bool,
89 pub sig_delim: char,
91}
92
93impl KindRule {
94 pub const fn plain(node_kind: &'static str, entity_kind: &'static str) -> Self {
96 Self { node_kind, entity_kind, with_signature: false, sig_delim: '{' }
97 }
98 pub const fn with_sig(node_kind: &'static str, entity_kind: &'static str, sig_delim: char) -> Self {
100 Self { node_kind, entity_kind, with_signature: true, sig_delim }
101 }
102}
103
104pub trait SharedProcessor: Sized {
128 fn language() -> &'static str;
130 fn grammar() -> Language;
132 fn kinds() -> &'static [KindRule];
134 fn handle_special(node: Node, bytes: &[u8], entities: &mut Vec<Entity>, imports: &mut Vec<ImportStmt>);
136 fn fallback(source: &str) -> (Vec<Entity>, Vec<ImportStmt>);
138 fn post_process(_source: &str, _entities: &mut Vec<Entity>) {}
140
141 fn extract(source: &str) -> (Vec<Entity>, Vec<ImportStmt>) {
146 let bytes = source.as_bytes();
147 let mut entities = Vec::new();
148 let mut imports = Vec::new();
149
150 let mut parser = Parser::new();
151 if parser.set_language(&Self::grammar()).is_err() {
152 let (mut e, i) = Self::fallback(source);
153 fill_visibilities(source, &mut e);
154 return (e, i);
155 }
156 let tree = match parser.parse(source, None) {
157 Some(t) => t,
158 None => return Self::fallback(source),
159 };
160
161 let mut cursor = tree.walk();
162 if !cursor.goto_first_child() { return (entities, imports); }
163
164 'walk: loop {
165 let node = cursor.node();
166 match Self::kinds().iter().find(|r| r.node_kind == node.kind()) {
167 Some(rule) => Self::record_by_rule(node, bytes, rule, &mut entities),
168 None => Self::handle_special(node, bytes, &mut entities, &mut imports),
169 }
170 if cursor.goto_first_child() { continue; }
171 loop {
172 if cursor.goto_next_sibling() { continue 'walk; }
173 if !cursor.goto_parent() { break 'walk; }
174 }
175 }
176
177 Self::post_process(source, &mut entities);
178 fill_visibilities(source, &mut entities);
179 (entities, imports)
180 }
181
182 fn record_by_rule(node: Node, bytes: &[u8], rule: &KindRule, entities: &mut Vec<Entity>) {
184 if let Some(name) = node.child_by_field_name("name").and_then(|n| n.utf8_text(bytes).ok()) {
185 let sig = if rule.with_signature {
186 node.utf8_text(bytes).ok()
187 .and_then(|t| t.split(rule.sig_delim).next().map(|s| s.trim().to_string()))
188 } else { None };
189 entities.push(Entity {
190 name: name.to_string(), kind: rule.entity_kind.to_string(),
191 line_start: node.start_position().row + 1, line_end: node.end_position().row + 1,
192 doc_comment: None, signature: sig, visibility: None,
193 });
194 }
195 }
196
197 fn parse_file(source: &str, path: &Path) -> Result<FileInsight> { let language = Self::language();
199 if source.is_empty() {
200 return Ok(FileInsight { path: path.to_path_buf(), language: language.into(), entities: vec![], imports: vec![], doc_comments: vec![], source: source.to_string() });
201 }
202 let (entities, imports) = Self::extract(source);
203 Ok(FileInsight { path: path.to_path_buf(), language: language.into(), entities, imports, doc_comments: vec![], source: source.to_string() })
204 }
205}
206
207fn fill_visibilities(source: &str, entities: &mut Vec<Entity>) {
219 let lines: Vec<&str> = source.lines().collect();
220 for e in entities {
221 if e.visibility.is_some() {
222 continue;
223 }
224 let mut i = e.line_start.saturating_sub(1);
225 while let Some(line) = lines.get(i) {
226 let t = line.trim();
227 if t.is_empty() || t.starts_with('#') || t.starts_with('[') {
228 if i == 0 {
229 break;
230 }
231 i -= 1;
232 continue;
233 }
234 let token = t.split_whitespace().next().unwrap_or("");
235 e.visibility = match token {
236 "pub" | "pub(crate)" | "pub(super)" | "private" | "protected" | "internal" | "export" => {
237 Some(token.to_string())
238 }
239 _ => None,
240 };
241 break;
242 }
243 }
244}
245
246pub struct ParserRegistry {
248 parsers: Vec<Box<dyn LanguageProcessor>>,
249}
250
251impl ParserRegistry {
252 pub fn new() -> Self {
254 let mut reg = Self { parsers: Vec::new() };
255 reg.register(Box::new(rust::RustProcessor::new().unwrap()));
256 reg.register(Box::new(typescript::TypeScriptProcessor::new().unwrap()));
257 reg.register(Box::new(python::PythonProcessor::new().unwrap()));
258 reg.register(Box::new(go::GoProcessor::new().unwrap()));
259 reg.register(Box::new(javascript::JavaScriptProcessor::new().unwrap()));
260 reg.register(Box::new(csharp::CSharpProcessor::new().unwrap()));
261 reg.register(Box::new(java::JavaProcessor::new().unwrap()));
262 reg
263 }
264
265 pub fn register(&mut self, parser: Box<dyn LanguageProcessor>) {
267 self.parsers.push(parser);
268 }
269
270 pub fn get_for_file(&self, path: &Path) -> Option<&dyn LanguageProcessor> {
272 let ext = path.extension()?.to_str()?;
273 let ext_str = format!(".{}", ext);
274 self.parsers.iter().find(|p| p.extensions().contains(&ext_str.as_str())).map(|b| b.as_ref())
275 }
276}
277
278impl Default for ParserRegistry {
279 fn default() -> Self {
280 Self::new()
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 fn entity(name: &str, start: usize) -> Entity {
289 Entity {
290 name: name.into(),
291 kind: "function".into(),
292 line_start: start,
293 line_end: start,
294 doc_comment: None,
295 signature: None,
296 visibility: None,
297 }
298 }
299
300 #[test]
301 fn test_fill_visibilities_extracts_modifiers() {
302 let src = "pub fn a() {}\n\nprivate int x;\n";
304 let mut es = vec![entity("a", 1), entity("x", 3)];
305 fill_visibilities(src, &mut es);
306 assert_eq!(es[0].visibility.as_deref(), Some("pub"));
307 assert_eq!(es[1].visibility.as_deref(), Some("private"));
308 }
309
310 #[test]
311 fn test_fill_visibilities_skips_attribute_lines() {
312 let src = "#[derive(Debug)]\npub struct Foo;\n\n[SerializeField]\nprivate float speed;\n";
315 let mut es = vec![entity("Foo", 2), entity("speed", 5)];
316 fill_visibilities(src, &mut es);
317 assert_eq!(es[0].visibility.as_deref(), Some("pub"));
318 assert_eq!(es[1].visibility.as_deref(), Some("private"));
319 }
320
321 #[test]
322 fn test_fill_visibilities_none_without_modifier() {
323 let src = "def run():\n pass\n\nfunc Run() {}\n";
326 let mut es = vec![entity("run", 1), entity("Run", 4)];
327 fill_visibilities(src, &mut es);
328 assert!(es[0].visibility.is_none());
329 assert!(es[1].visibility.is_none());
330 }
331
332 #[test]
333 fn test_fill_visibilities_keeps_pub_crate_variant() {
334 let src = "pub(crate) fn internal() {}\npub(super) fn child() {}\n";
336 let mut es = vec![entity("internal", 1), entity("child", 2)];
337 fill_visibilities(src, &mut es);
338 assert_eq!(es[0].visibility.as_deref(), Some("pub(crate)"));
339 assert_eq!(es[1].visibility.as_deref(), Some("pub(super)"));
340 }
341}