mod rust;
mod typescript;
mod python;
mod go;
mod javascript;
mod csharp;
mod java;
use std::path::{Path, PathBuf};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use tree_sitter::{Language, Node, Parser};
pub const SUPPORTED_EXTENSIONS: &[&str] = &[
".rs", ".ts", ".tsx", ".py", ".go", ".js", ".jsx", ".mjs", ".cjs", ".cs", ".java",
];
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileInsight {
pub path: PathBuf,
pub language: String,
pub entities: Vec<Entity>,
pub imports: Vec<ImportStmt>,
pub doc_comments: Vec<String>,
pub source: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Entity {
pub name: String,
pub kind: String,
pub line_start: usize,
pub line_end: usize,
pub doc_comment: Option<String>,
pub signature: Option<String>,
#[serde(default)]
pub visibility: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportStmt {
pub source: String,
pub alias: Option<String>,
pub line: usize,
}
pub trait LanguageProcessor: Send + Sync {
fn name(&self) -> &'static str;
fn extensions(&self) -> &[&str];
fn parse(&self, source: &str, path: &Path) -> Result<FileInsight>;
}
#[derive(Debug, Clone, Copy)]
pub struct KindRule {
pub node_kind: &'static str,
pub entity_kind: &'static str,
pub with_signature: bool,
pub sig_delim: char,
}
impl KindRule {
pub const fn plain(node_kind: &'static str, entity_kind: &'static str) -> Self {
Self { node_kind, entity_kind, with_signature: false, sig_delim: '{' }
}
pub const fn with_sig(node_kind: &'static str, entity_kind: &'static str, sig_delim: char) -> Self {
Self { node_kind, entity_kind, with_signature: true, sig_delim }
}
}
pub trait SharedProcessor: Sized {
fn language() -> &'static str;
fn grammar() -> Language;
fn kinds() -> &'static [KindRule];
fn handle_special(node: Node, bytes: &[u8], entities: &mut Vec<Entity>, imports: &mut Vec<ImportStmt>);
fn fallback(source: &str) -> (Vec<Entity>, Vec<ImportStmt>);
fn post_process(_source: &str, _entities: &mut Vec<Entity>) {}
fn extract(source: &str) -> (Vec<Entity>, Vec<ImportStmt>) {
let bytes = source.as_bytes();
let mut entities = Vec::new();
let mut imports = Vec::new();
let mut parser = Parser::new();
if parser.set_language(&Self::grammar()).is_err() {
let (mut e, i) = Self::fallback(source);
fill_visibilities(source, &mut e);
return (e, i);
}
let tree = match parser.parse(source, None) {
Some(t) => t,
None => return Self::fallback(source),
};
let mut cursor = tree.walk();
if !cursor.goto_first_child() { return (entities, imports); }
'walk: loop {
let node = cursor.node();
match Self::kinds().iter().find(|r| r.node_kind == node.kind()) {
Some(rule) => Self::record_by_rule(node, bytes, rule, &mut entities),
None => Self::handle_special(node, bytes, &mut entities, &mut imports),
}
if cursor.goto_first_child() { continue; }
loop {
if cursor.goto_next_sibling() { continue 'walk; }
if !cursor.goto_parent() { break 'walk; }
}
}
Self::post_process(source, &mut entities);
fill_visibilities(source, &mut entities);
(entities, imports)
}
fn record_by_rule(node: Node, bytes: &[u8], rule: &KindRule, entities: &mut Vec<Entity>) {
if let Some(name) = node.child_by_field_name("name").and_then(|n| n.utf8_text(bytes).ok()) {
let sig = if rule.with_signature {
node.utf8_text(bytes).ok()
.and_then(|t| t.split(rule.sig_delim).next().map(|s| s.trim().to_string()))
} else { None };
entities.push(Entity {
name: name.to_string(), kind: rule.entity_kind.to_string(),
line_start: node.start_position().row + 1, line_end: node.end_position().row + 1,
doc_comment: None, signature: sig, visibility: None,
});
}
}
fn parse_file(source: &str, path: &Path) -> Result<FileInsight> { let language = Self::language();
if source.is_empty() {
return Ok(FileInsight { path: path.to_path_buf(), language: language.into(), entities: vec![], imports: vec![], doc_comments: vec![], source: source.to_string() });
}
let (entities, imports) = Self::extract(source);
Ok(FileInsight { path: path.to_path_buf(), language: language.into(), entities, imports, doc_comments: vec![], source: source.to_string() })
}
}
fn fill_visibilities(source: &str, entities: &mut Vec<Entity>) {
let lines: Vec<&str> = source.lines().collect();
for e in entities {
if e.visibility.is_some() {
continue;
}
let mut i = e.line_start.saturating_sub(1);
while let Some(line) = lines.get(i) {
let t = line.trim();
if t.is_empty() || t.starts_with('#') || t.starts_with('[') {
if i == 0 {
break;
}
i -= 1;
continue;
}
let token = t.split_whitespace().next().unwrap_or("");
e.visibility = match token {
"pub" | "pub(crate)" | "pub(super)" | "private" | "protected" | "internal" | "export" => {
Some(token.to_string())
}
_ => None,
};
break;
}
}
}
pub struct ParserRegistry {
parsers: Vec<Box<dyn LanguageProcessor>>,
}
impl ParserRegistry {
pub fn new() -> Self {
let mut reg = Self { parsers: Vec::new() };
reg.register(Box::new(rust::RustProcessor::new().unwrap()));
reg.register(Box::new(typescript::TypeScriptProcessor::new().unwrap()));
reg.register(Box::new(python::PythonProcessor::new().unwrap()));
reg.register(Box::new(go::GoProcessor::new().unwrap()));
reg.register(Box::new(javascript::JavaScriptProcessor::new().unwrap()));
reg.register(Box::new(csharp::CSharpProcessor::new().unwrap()));
reg.register(Box::new(java::JavaProcessor::new().unwrap()));
reg
}
pub fn register(&mut self, parser: Box<dyn LanguageProcessor>) {
self.parsers.push(parser);
}
pub fn get_for_file(&self, path: &Path) -> Option<&dyn LanguageProcessor> {
let ext = path.extension()?.to_str()?;
let ext_str = format!(".{}", ext);
self.parsers.iter().find(|p| p.extensions().contains(&ext_str.as_str())).map(|b| b.as_ref())
}
}
impl Default for ParserRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entity(name: &str, start: usize) -> Entity {
Entity {
name: name.into(),
kind: "function".into(),
line_start: start,
line_end: start,
doc_comment: None,
signature: None,
visibility: None,
}
}
#[test]
fn test_fill_visibilities_extracts_modifiers() {
let src = "pub fn a() {}\n\nprivate int x;\n";
let mut es = vec![entity("a", 1), entity("x", 3)];
fill_visibilities(src, &mut es);
assert_eq!(es[0].visibility.as_deref(), Some("pub"));
assert_eq!(es[1].visibility.as_deref(), Some("private"));
}
#[test]
fn test_fill_visibilities_skips_attribute_lines() {
let src = "#[derive(Debug)]\npub struct Foo;\n\n[SerializeField]\nprivate float speed;\n";
let mut es = vec![entity("Foo", 2), entity("speed", 5)];
fill_visibilities(src, &mut es);
assert_eq!(es[0].visibility.as_deref(), Some("pub"));
assert_eq!(es[1].visibility.as_deref(), Some("private"));
}
#[test]
fn test_fill_visibilities_none_without_modifier() {
let src = "def run():\n pass\n\nfunc Run() {}\n";
let mut es = vec![entity("run", 1), entity("Run", 4)];
fill_visibilities(src, &mut es);
assert!(es[0].visibility.is_none());
assert!(es[1].visibility.is_none());
}
#[test]
fn test_fill_visibilities_keeps_pub_crate_variant() {
let src = "pub(crate) fn internal() {}\npub(super) fn child() {}\n";
let mut es = vec![entity("internal", 1), entity("child", 2)];
fill_visibilities(src, &mut es);
assert_eq!(es[0].visibility.as_deref(), Some("pub(crate)"));
assert_eq!(es[1].visibility.as_deref(), Some("pub(super)"));
}
}