use anyhow::{Context, Result};
use tree_sitter_language_pack::{
detect_language_from_path, detect_language_from_content, available_languages, has_parser,
Node, Parser, ProcessConfig, ProcessResult, StructureItem,
Tree,
};
#[cfg(test)]
use tree_sitter_language_pack::StructureKind;
use crate::engine::tokenizer::{normalize};
#[derive(Debug, Clone, PartialEq)]
pub struct ParseNode {
pub id: String,
pub doc_id: String,
pub parent_id: Option<String>,
pub kind: String,
pub start_byte: usize,
pub end_byte: usize,
pub depth: u32,
}
impl ParseNode {
#[must_use]
pub fn is_indexable(&self) -> bool {
matches!(
self.kind.as_str(),
"function_item" | "function_definition" | "method_definition" |
"class_definition" | "class_declaration" | "struct_item" |
"interface_declaration" | "trait_item" | "impl_item" |
"mod_item" | "use_declaration" | "import_statement" |
"import_declaration" | "export_statement" | "export_declaration" |
"call_expression" | "macro_invocation" | "attribute" | "annotation"
)
}
}
#[derive(Debug, Clone)]
pub struct ParseResult {
pub nodes: Vec<ParseNode>,
pub structure: Vec<StructureItem>,
pub process_result: Option<ProcessResult>,
}
#[derive(Clone, Default)]
pub struct ParseTreeEngine {
}
impl ParseTreeEngine {
pub fn new() -> Self {
Self {}
}
fn parser(&self, language: &str) -> Result<Parser> {
let mut parser = Parser::new();
parser.set_language(language)
.with_context(|| format!("set parser language `{language}`"))?;
Ok(parser)
}
pub fn parse(
&self,
text: &str,
language: &str,
already_normalized: bool,
) -> Result<ParseResult> {
let normalized = if already_normalized {
text.to_string()
} else {
normalize(text)
};
let mut parser = self.parser(language)?;
let tree = parser.parse(&normalized)
.with_context(|| format!("parse `{language}` source ({} bytes)", normalized.len()))?;
let nodes = self.tree_to_nodes(&normalized, &tree, "root".to_string())?;
let config = ProcessConfig::new(language).all();
let process_result = tree_sitter_language_pack::process(&normalized, &config)
.ok();
let structure = process_result.as_ref().map(|r| r.structure.clone()).unwrap_or_default();
Ok(ParseResult { nodes, structure, process_result })
}
fn tree_to_nodes(
&self,
_normalized: &str,
tree: &Tree,
doc_id: String,
) -> Result<Vec<ParseNode>> {
let root = tree.root_node();
let mut nodes = Vec::new();
self.walk(_normalized, root, None, 0, &doc_id, &mut nodes)?;
Ok(nodes)
}
fn walk(
&self,
_normalized: &str,
node: Node,
parent_id: Option<String>,
depth: u32,
doc_id: &str,
nodes: &mut Vec<ParseNode>,
) -> Result<()> {
let start_byte = node.start_byte();
let end_byte = node.end_byte();
let kind = node.kind().to_string();
let id = format!("{}#{}:{}", doc_id, kind, start_byte);
if end_byte > start_byte {
nodes.push(ParseNode {
id: id.clone(),
doc_id: doc_id.to_string(),
parent_id: parent_id.clone(),
kind,
start_byte,
end_byte,
depth,
});
}
let child_count = node.child_count();
for i in 0..child_count {
if let Some(child) = node.child(i as u32) {
self.walk(_normalized, child, Some(id.clone()), depth + 1, doc_id, nodes)?;
}
}
Ok(())
}
pub fn parse_auto(
&self,
url: &str,
text: &str,
) -> Result<ParseResult> {
let normalized = normalize(text);
let language = detect_language_from_path(url)
.or_else(|| detect_language_from_content(&normalized))
.unwrap_or("text");
if language == "text" || !has_parser(language) {
return Ok(ParseResult {
nodes: Vec::new(),
structure: Vec::new(),
process_result: None,
});
}
self.parse(&normalized, language, true)
}
#[must_use]
pub fn available_languages(&self) -> Vec<String> {
available_languages()
}
#[must_use]
pub fn can_parse_offline(&self, language: &str) -> bool {
has_parser(language)
}
}
pub async fn parse_and_store(
engine: &ParseTreeEngine,
_store: &dyn crate::engine::crawl_graph::CrawlGraphStore,
_doc_id: &str,
url: &str,
text: &str,
) -> Result<()> {
let result = engine.parse_auto(url, text)?;
for _node in &result.nodes {
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn engine_creates_and_parses_rust() {
let engine = ParseTreeEngine::new();
let rust = r#"
fn main() {
println!("hello");
}
"#;
let result = engine.parse(rust, "rust", false).unwrap();
assert!(!result.nodes.is_empty(), "must produce nodes");
let kinds: Vec<String> = result.nodes.iter().map(|n| n.kind.clone()).collect();
assert!(kinds.contains(&"source_file".to_string()));
assert!(kinds.iter().any(|k| k.contains("function")));
}
#[test]
fn engine_creates_and_parses_python() {
let engine = ParseTreeEngine::new();
let py = "def hello():\n print('world')\n";
let result = engine.parse(py, "python", false).unwrap();
assert!(!result.nodes.is_empty());
let kinds: Vec<String> = result.nodes.iter().map(|n| n.kind.clone()).collect();
assert!(kinds.contains(&"module".to_string()));
assert!(kinds.iter().any(|k| k.contains("function")));
}
#[test]
fn parse_tree_nodes_have_dense_ordinal_ids() {
let engine = ParseTreeEngine::new();
let code = "fn a() {}\nfn b() {}\n";
let result = engine.parse(code, "rust", false).unwrap();
let ids: Vec<&str> = result.nodes.iter().map(|n| n.id.as_str()).collect();
assert!(ids.iter().all(|id| id.contains('#')));
let id_set: std::collections::HashSet<_> = ids.iter().cloned().collect();
for node in &result.nodes {
if let Some(parent) = &node.parent_id {
assert!(
id_set.contains(parent.as_str()),
"parent_id {} must exist",
parent
);
}
}
}
#[test]
fn parse_auto_detects_rust_from_url() {
let engine = ParseTreeEngine::new();
let rust = "fn main() {}";
let result = engine.parse_auto("https://example.com/main.rs", rust).unwrap();
assert!(!result.nodes.is_empty(), "must parse rust from .rs URL");
}
#[test]
fn parse_auto_detects_python_from_url() {
let engine = ParseTreeEngine::new();
let py = "def hello():\n pass\n";
let result = engine.parse_auto("https://example.com/script.py", py).unwrap();
assert!(!result.nodes.is_empty(), "must parse python from .py URL");
}
#[test]
fn parse_auto_falls_back_to_text_for_unknown_extension() {
let engine = ParseTreeEngine::new();
let unknown = "this is plain text";
let result = engine.parse_auto("https://example.com/weird.xyz", unknown).unwrap();
assert!(result.nodes.is_empty(), "unknown extension must fall back to empty nodes");
}
#[test]
fn structure_extraction_finds_functions_and_classes() {
let engine = ParseTreeEngine::new();
let code = r#"
class MyClass {
fn method(&self) {}
}
fn standalone() {}
"#;
let result = engine.parse(code, "rust", false).unwrap();
assert!(
!result.structure.is_empty(),
"ProcessConfig::all() must extract structure"
);
let kinds: Vec<StructureKind> = result.structure.iter().map(|s| s.kind.clone()).collect();
assert!(
kinds.iter().any(|k| matches!(k, StructureKind::Function | StructureKind::Class | StructureKind::Struct)),
"must extract functions or classes, got {kinds:?}"
);
}
#[test]
fn parse_nodes_have_valid_byte_spans() {
let engine = ParseTreeEngine::new();
let code = "fn foo() {}";
let result = engine.parse(code, "rust", false).unwrap();
for node in &result.nodes {
assert!(node.start_byte < node.end_byte, "span must be non-empty");
assert!(code.is_char_boundary(node.start_byte) && code.is_char_boundary(node.end_byte));
}
}
#[test]
fn depth_increases_from_root() {
let engine = ParseTreeEngine::new();
let code = "fn outer() { fn inner() {} }";
let result = engine.parse(code, "rust", false).unwrap();
let max_depth = result.nodes.iter().map(|n| n.depth).max().unwrap();
assert!(max_depth >= 2, "nested function must have depth >= 2, got {}", max_depth);
}
#[test]
fn available_languages_is_non_empty() {
let engine = ParseTreeEngine::new();
let langs = engine.available_languages();
assert!(!langs.is_empty(), "at least one language must be available");
assert!(langs.contains(&"rust".to_string()));
assert!(langs.contains(&"python".to_string()));
}
#[test]
fn can_parse_offline_is_false_for_unknown_language() {
let engine = ParseTreeEngine::new();
assert!(!engine.can_parse_offline("nonexistent_language_xyz"));
}
#[test]
fn process_result_extracts_imports_and_exports() {
let engine = ParseTreeEngine::new();
let py = "import os\nfrom sys import path\n\ndef hello():\n print('world')\n";
let result = engine.parse(py, "python", false).unwrap();
assert!(
!result.structure.is_empty(),
"structure extraction must produce items"
);
let kinds: Vec<StructureKind> = result.structure.iter().map(|s| s.kind.clone()).collect();
assert!(
!kinds.is_empty(),
"python code must produce some structure kinds: {kinds:?}"
);
}}