pub mod go;
pub mod java;
pub mod kotlin;
pub mod python;
pub mod rust;
pub mod typescript;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use tracing::trace;
pub use astmap_core::{
ExtractedCall, ExtractedImport, ExtractedSymbol, ExtractedTypeRef, LanguageParser,
LanguageRegistry, LanguageResolver, ParseResult,
};
pub fn find_enclosing_symbol(symbols: &[ExtractedSymbol], line: usize) -> Option<&str> {
symbols
.iter()
.filter(|sym| line >= sym.start_line && line <= sym.end_line)
.min_by_key(|sym| sym.end_line - sym.start_line)
.map(|s| s.name.as_str())
}
pub fn node_text(node: &tree_sitter::Node, source: &str) -> String {
source[node.start_byte()..node.end_byte()].to_string()
}
pub fn first_line(node: &tree_sitter::Node, source: &str) -> String {
let text = &source[node.start_byte()..node.end_byte()];
text.lines().next().unwrap_or("").to_string()
}
pub fn symbol_from_node(
name: String,
kind: astmap_core::SymbolKind,
node: &tree_sitter::Node,
source: &str,
parent_index: Option<usize>,
) -> ExtractedSymbol {
ExtractedSymbol {
name,
kind,
signature: Some(first_line(node, source)),
start_line: node.start_position().row + 1,
end_line: node.end_position().row + 1,
start_byte: node.start_byte(),
end_byte: node.end_byte(),
parent_index,
}
}
pub fn parse_with_tree_sitter(
source: &str,
language: &tree_sitter::Language,
extract_symbols: impl FnOnce(
tree_sitter::Node,
&str,
&mut Vec<ExtractedSymbol>,
&mut Vec<ExtractedImport>,
),
extract_calls: impl FnOnce(tree_sitter::Node, &str, &[ExtractedSymbol]) -> Vec<ExtractedCall>,
extract_type_refs: impl FnOnce(tree_sitter::Node, &str, &[ExtractedSymbol]) -> Vec<ExtractedTypeRef>,
) -> ParseResult {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(language)
.expect("failed to set language");
let tree = match parser.parse(source, None) {
Some(t) => t,
None => {
return ParseResult {
symbols: vec![],
imports: vec![],
calls: vec![],
type_refs: vec![],
has_errors: true,
};
}
};
let has_errors = tree.root_node().has_error();
let mut symbols = Vec::new();
let mut imports = Vec::new();
extract_symbols(tree.root_node(), source, &mut symbols, &mut imports);
let calls = extract_calls(tree.root_node(), source, &symbols);
let type_refs = extract_type_refs(tree.root_node(), source, &symbols);
ParseResult {
symbols,
calls,
type_refs,
imports,
has_errors,
}
}
pub fn collect_type_refs_by_kind(
root: tree_sitter::Node,
source: &str,
symbols: &[ExtractedSymbol],
node_kind: &str,
is_builtin: &dyn Fn(&str) -> bool,
) -> Vec<ExtractedTypeRef> {
let mut refs = Vec::new();
walk_type_refs(root, source, symbols, node_kind, is_builtin, &mut refs);
refs
}
fn walk_type_refs(
node: tree_sitter::Node,
source: &str,
symbols: &[ExtractedSymbol],
node_kind: &str,
is_builtin: &dyn Fn(&str) -> bool,
refs: &mut Vec<ExtractedTypeRef>,
) {
if node.kind() == node_kind {
let type_name = node_text(&node, source);
let line = node.start_position().row + 1;
if !is_builtin(&type_name) {
if let Some(from_sym) = find_enclosing_symbol(symbols, line) {
refs.push(ExtractedTypeRef {
from_symbol: from_sym.to_string(),
to_type: type_name,
line,
});
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
walk_type_refs(child, source, symbols, node_kind, is_builtin, refs);
}
}
}
pub fn detect_jvm_source_roots(root: &Path) -> Vec<PathBuf> {
let candidates = [
"src/main/java",
"src/main/kotlin",
"src/test/java",
"src/test/kotlin",
"src/commonMain/kotlin",
"src/jvmMain/kotlin",
"src/androidMain/kotlin",
"src",
];
let mut roots = Vec::new();
for candidate in &candidates {
let path = root.join(candidate);
if path.is_dir() {
roots.push(path);
}
}
if roots.is_empty() {
roots.push(root.to_path_buf());
}
roots
}
pub(crate) struct LanguageSupport {
pub name: &'static str,
pub extensions: &'static [&'static str],
pub parser: &'static dyn LanguageParser,
pub resolver_factory: fn(&Path) -> Box<dyn LanguageResolver>,
pub config_files: &'static [&'static str],
pub sibling_fn: fn(&str) -> Option<astmap_core::SiblingExpansion>,
}
fn all() -> &'static [LanguageSupport] {
static LANGS: OnceLock<Vec<LanguageSupport>> = OnceLock::new();
LANGS.get_or_init(|| {
vec![
rust::lang(),
typescript::lang(),
python::lang(),
go::lang(),
java::lang(),
kotlin::lang(),
]
})
}
pub fn parser_for(ext: &str) -> Option<&'static dyn LanguageParser> {
all().iter().find(|l| l.extensions.contains(&ext)).map(|l| {
trace!("selected parser: {} for extension: {}", l.name, ext);
l.parser
})
}
pub fn resolver_for(ext: &str, root: &Path) -> Option<Box<dyn LanguageResolver>> {
all()
.iter()
.find(|l| l.extensions.contains(&ext))
.map(|l| (l.resolver_factory)(root))
}
pub fn supported_extensions() -> &'static [&'static str] {
static EXTENSIONS: OnceLock<Vec<&str>> = OnceLock::new();
EXTENSIONS.get_or_init(|| {
all()
.iter()
.flat_map(|l| l.extensions.iter().copied())
.collect()
})
}
pub fn config_files() -> &'static [&'static str] {
static CONFIGS: OnceLock<Vec<&str>> = OnceLock::new();
CONFIGS.get_or_init(|| {
let mut files: Vec<&str> = all()
.iter()
.flat_map(|l| l.config_files.iter().copied())
.collect();
files.dedup();
files
})
}
pub fn sibling_expansion(rel_path: &str) -> Option<astmap_core::SiblingExpansion> {
let ext = Path::new(rel_path).extension()?.to_str()?;
let lang = all().iter().find(|l| l.extensions.contains(&ext))?;
(lang.sibling_fn)(rel_path)
}
pub struct DefaultLanguageRegistry;
impl LanguageRegistry for DefaultLanguageRegistry {
fn parser_for(&self, ext: &str) -> Option<&dyn LanguageParser> {
parser_for(ext)
}
fn resolver_for(&self, ext: &str, root: &Path) -> Option<Box<dyn LanguageResolver>> {
resolver_for(ext, root)
}
fn supported_extensions(&self) -> &[&str] {
supported_extensions()
}
fn config_files(&self) -> &[&str] {
config_files()
}
fn sibling_expansion(&self, rel_path: &str) -> Option<astmap_core::SiblingExpansion> {
sibling_expansion(rel_path)
}
}
#[cfg(test)]
mod tests {
use super::*;
use astmap_core::SymbolKind;
#[test]
fn test_parser_for_known_extensions() {
assert!(parser_for("rs").is_some());
assert!(parser_for("ts").is_some());
assert!(parser_for("tsx").is_some());
assert!(parser_for("go").is_some());
assert_eq!(parser_for("rs").unwrap().language_name(), "rust");
assert_eq!(parser_for("ts").unwrap().language_name(), "typescript");
assert_eq!(parser_for("tsx").unwrap().language_name(), "typescript");
assert_eq!(parser_for("go").unwrap().language_name(), "go");
}
#[test]
fn test_parser_for_python() {
assert!(parser_for("py").is_some());
assert_eq!(parser_for("py").unwrap().language_name(), "python");
}
#[test]
fn test_parser_for_unknown_extension() {
assert!(parser_for("rb").is_none());
assert!(parser_for("js").is_none());
assert!(parser_for("").is_none());
}
#[test]
fn test_supported_extensions() {
let extensions = supported_extensions();
assert!(extensions.contains(&"rs"));
assert!(extensions.contains(&"ts"));
assert!(extensions.contains(&"tsx"));
assert!(extensions.contains(&"py"));
assert!(extensions.contains(&"go"));
assert!(!extensions.contains(&"js"));
}
#[test]
fn test_resolver_for() {
let tmp = tempfile::tempdir().unwrap();
assert!(resolver_for("rs", tmp.path()).is_some());
assert!(resolver_for("ts", tmp.path()).is_some());
assert!(resolver_for("tsx", tmp.path()).is_some());
assert!(resolver_for("py", tmp.path()).is_some());
assert!(resolver_for("go", tmp.path()).is_some());
}
#[test]
fn test_parser_produces_results() {
let parser = parser_for("ts").unwrap();
let result = parser.parse("function foo() {}", &std::path::PathBuf::from("test.ts"));
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 1);
}
#[test]
fn test_node_text_basic() {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&tree_sitter_rust::LANGUAGE.into())
.unwrap();
let source = "fn hello() {}";
let tree = parser.parse(source, None).unwrap();
let root = tree.root_node();
let text = node_text(&root, source);
assert_eq!(text, source);
}
#[test]
fn test_node_text_child_node() {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&tree_sitter_rust::LANGUAGE.into())
.unwrap();
let source = "fn hello() {}";
let tree = parser.parse(source, None).unwrap();
let func = tree.root_node().child(0).unwrap();
let name_node = func.child_by_field_name("name").unwrap();
let text = node_text(&name_node, source);
assert_eq!(text, "hello");
}
#[test]
fn test_first_line_single_line() {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&tree_sitter_rust::LANGUAGE.into())
.unwrap();
let source = "fn hello() {}";
let tree = parser.parse(source, None).unwrap();
let root = tree.root_node();
let line = first_line(&root, source);
assert_eq!(line, "fn hello() {}");
}
#[test]
fn test_first_line_multi_line() {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&tree_sitter_rust::LANGUAGE.into())
.unwrap();
let source = "fn hello() {\n 42\n}";
let tree = parser.parse(source, None).unwrap();
let func = tree.root_node().child(0).unwrap();
let line = first_line(&func, source);
assert_eq!(line, "fn hello() {");
}
#[test]
fn test_all_returns_known_languages() {
let langs = all();
assert!(langs.len() >= 4);
let names: Vec<&str> = langs.iter().map(|l| l.name).collect();
assert!(names.contains(&"rust"));
assert!(names.contains(&"typescript"));
assert!(names.contains(&"python"));
assert!(names.contains(&"go"));
}
#[test]
fn test_default_language_registry_parser_for() {
let registry = DefaultLanguageRegistry;
assert!(registry.parser_for("rs").is_some());
assert!(registry.parser_for("ts").is_some());
assert!(registry.parser_for("py").is_some());
assert!(registry.parser_for("js").is_none());
}
#[test]
fn test_default_language_registry_resolver_for() {
let registry = DefaultLanguageRegistry;
let tmp = tempfile::tempdir().unwrap();
assert!(registry.resolver_for("rs", tmp.path()).is_some());
assert!(registry.resolver_for("tsx", tmp.path()).is_some());
assert!(registry.resolver_for("py", tmp.path()).is_some());
assert!(registry.resolver_for("rb", tmp.path()).is_none());
}
#[test]
fn test_default_language_registry_supported_extensions() {
let registry = DefaultLanguageRegistry;
let exts = registry.supported_extensions();
assert!(exts.contains(&"rs"));
assert!(exts.contains(&"ts"));
assert!(exts.contains(&"tsx"));
assert!(exts.contains(&"py"));
assert!(!exts.contains(&"js"));
}
#[test]
fn test_resolver_for_unknown_extension() {
let tmp = tempfile::tempdir().unwrap();
assert!(resolver_for("rb", tmp.path()).is_none());
assert!(resolver_for("", tmp.path()).is_none());
}
#[test]
fn test_find_enclosing_symbol_innermost() {
let symbols = vec![
ExtractedSymbol {
name: "Outer".to_string(),
kind: SymbolKind::Struct,
signature: None,
start_line: 1,
end_line: 20,
start_byte: 0,
end_byte: 200,
parent_index: None,
},
ExtractedSymbol {
name: "inner_fn".to_string(),
kind: SymbolKind::Method,
signature: None,
start_line: 5,
end_line: 10,
start_byte: 50,
end_byte: 100,
parent_index: Some(0),
},
];
assert_eq!(find_enclosing_symbol(&symbols, 7), Some("inner_fn"));
assert_eq!(find_enclosing_symbol(&symbols, 15), Some("Outer"));
assert_eq!(find_enclosing_symbol(&symbols, 25), None);
}
}