use super::lang_config::{CommentStyle, LanguageConfig};
use super::LanguageParser;
use dk_core::{
CallKind, Error, FileAnalysis, Import, RawCallEdge, Result, Span, Symbol, TypeInfo,
};
use std::path::Path;
use std::sync::Mutex;
use streaming_iterator::StreamingIterator;
use tree_sitter::{Node, Parser, Query, QueryCursor, Tree};
use uuid::Uuid;
pub struct QueryDrivenParser {
config: Box<dyn LanguageConfig>,
parser: Mutex<Parser>,
symbols_query: Query,
calls_query: Option<Query>,
imports_query: Option<Query>,
}
impl QueryDrivenParser {
pub fn new(config: Box<dyn LanguageConfig>) -> Result<Self> {
let lang = config.language();
let symbols_query = Query::new(&lang, config.symbols_query()).map_err(|e| {
Error::ParseError(format!("Failed to compile symbols query: {e}"))
})?;
let calls_query = {
let q = config.calls_query();
if q.is_empty() {
None
} else {
Some(Query::new(&lang, q).map_err(|e| {
Error::ParseError(format!("Failed to compile calls query: {e}"))
})?)
}
};
let imports_query = {
let q = config.imports_query();
if q.is_empty() {
None
} else {
Some(Query::new(&lang, q).map_err(|e| {
Error::ParseError(format!("Failed to compile imports query: {e}"))
})?)
}
};
let mut parser = Parser::new();
parser
.set_language(&lang)
.map_err(|e| Error::ParseError(format!("Failed to set language: {e}")))?;
Ok(Self {
config,
parser: Mutex::new(parser),
symbols_query,
calls_query,
imports_query,
})
}
fn parse_tree(&self, source: &[u8]) -> Result<tree_sitter::Tree> {
let mut parser = self.parser.lock().unwrap_or_else(|e| e.into_inner());
parser
.parse(source, None)
.ok_or_else(|| Error::ParseError("tree-sitter parse returned None".into()))
}
fn node_text<'a>(node: &Node, source: &'a [u8]) -> &'a str {
let bytes = &source[node.start_byte()..node.end_byte()];
std::str::from_utf8(bytes).unwrap_or("")
}
fn node_signature(node: &Node, source: &[u8]) -> Option<String> {
let text = Self::node_text(node, source);
let first_line = text.lines().next()?;
let trimmed = first_line.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
fn collect_doc_comments(&self, node: &Node, source: &[u8]) -> Option<String> {
let comment_prefix = match self.config.comment_style() {
CommentStyle::TripleSlash => "///",
CommentStyle::Hash => "#",
CommentStyle::SlashSlash => "//",
CommentStyle::DashDash => "--",
};
let mut lines = Vec::new();
let mut sibling = node.prev_sibling();
while let Some(prev) = sibling {
if prev.kind() == "line_comment" || prev.kind() == "comment" {
if let Some(before_comment) = prev.prev_sibling() {
if before_comment.kind() != "comment"
&& before_comment.kind() != "line_comment"
&& before_comment.end_position().row == prev.start_position().row
{
break;
}
}
let text = Self::node_text(&prev, source).trim();
if text.starts_with(comment_prefix) || text.starts_with("/*") {
lines.push(text.to_string());
sibling = prev.prev_sibling();
continue;
}
}
break;
}
if lines.is_empty() {
None
} else {
lines.reverse();
Some(lines.join("\n"))
}
}
fn enclosing_function_name(&self, node: &Node, source: &[u8]) -> String {
let named_function_kinds = [
"function_item",
"function_definition",
"function_declaration",
"method_definition",
];
let anonymous_function_kinds = ["arrow_function", "function_expression", "function"];
let mut current = node.parent();
while let Some(parent) = current {
let kind = parent.kind();
if named_function_kinds.contains(&kind) {
if let Some(name_node) = parent.child_by_field_name("name") {
let name = Self::node_text(&name_node, source);
if !name.is_empty() {
return name.to_string();
}
}
} else if anonymous_function_kinds.contains(&kind) {
if let Some(gp) = parent.parent() {
if gp.kind() == "variable_declarator" {
if let Some(name_node) = gp.child_by_field_name("name") {
let name = Self::node_text(&name_node, source);
if !name.is_empty() {
return name.to_string();
}
}
}
}
}
current = parent.parent();
}
"<module>".to_string()
}
}
impl QueryDrivenParser {
fn symbols_from_tree(
&self,
tree: &Tree,
source: &[u8],
file_path: &Path,
) -> Vec<Symbol> {
let root = tree.root_node();
let capture_names = self.symbols_query.capture_names();
let mut cursor = QueryCursor::new();
let mut symbols = Vec::new();
let mut matches = cursor.matches(&self.symbols_query, root, source);
while let Some(m) = { matches.advance(); matches.get() } {
let mut name_text: Option<String> = None;
let mut definition_node: Option<Node> = None;
let mut kind_suffix: Option<String> = None;
let mut modifiers_text: Option<String> = None;
for capture in m.captures {
let capture_name = capture_names[capture.index as usize];
if capture_name == "name" {
name_text = Some(Self::node_text(&capture.node, source).to_string());
} else if let Some(suffix) = capture_name.strip_prefix("definition.") {
definition_node = Some(capture.node);
kind_suffix = Some(suffix.to_string());
} else if capture_name == "modifiers" {
modifiers_text = Some(Self::node_text(&capture.node, source).to_string());
}
}
let name = match &name_text {
Some(n) if !n.is_empty() => n.as_str(),
_ => continue,
};
let def_node = match definition_node {
Some(n) => n,
None => continue,
};
let suffix = match &kind_suffix {
Some(s) => s.as_str(),
None => continue,
};
let symbol_kind = match self.config.map_capture_to_kind(suffix) {
Some(k) => k,
None => continue,
};
let visibility = self
.config
.resolve_visibility(modifiers_text.as_deref(), name);
let signature = Self::node_signature(&def_node, source);
let doc_comment = self.collect_doc_comments(&def_node, source);
let mut sym = Symbol {
id: Uuid::new_v4(),
name: name.to_string(),
qualified_name: name.to_string(),
kind: symbol_kind,
visibility,
file_path: file_path.to_path_buf(),
span: Span {
start_byte: def_node.start_byte() as u32,
end_byte: def_node.end_byte() as u32,
},
signature,
doc_comment,
parent: None,
last_modified_by: None,
last_modified_intent: None,
};
self.config.adjust_symbol(&mut sym, &def_node, source);
symbols.push(sym);
}
symbols
}
fn calls_from_tree(&self, tree: &Tree, source: &[u8]) -> Vec<RawCallEdge> {
let calls_query = match &self.calls_query {
Some(q) => q,
None => return vec![],
};
let root = tree.root_node();
let capture_names = calls_query.capture_names();
let mut cursor = QueryCursor::new();
let mut calls = Vec::new();
let mut matches = cursor.matches(calls_query, root, source);
while let Some(m) = { matches.advance(); matches.get() } {
let mut callee_text: Option<String> = None;
let mut method_callee_text: Option<String> = None;
let mut call_node: Option<Node> = None;
let mut first_node: Option<Node> = None;
for capture in m.captures {
let capture_name = capture_names[capture.index as usize];
if first_node.is_none() {
first_node = Some(capture.node);
}
match capture_name {
"callee" => {
callee_text =
Some(Self::node_text(&capture.node, source).to_string());
}
"method_callee" => {
method_callee_text =
Some(Self::node_text(&capture.node, source).to_string());
}
"call" => call_node = Some(capture.node),
_ => {}
}
}
let (callee_name, call_kind) = if let Some(method) =
method_callee_text.filter(|s| !s.is_empty())
{
(method, CallKind::MethodCall)
} else if let Some(direct) = callee_text.filter(|s| !s.is_empty()) {
(direct, CallKind::DirectCall)
} else {
continue;
};
let span_node = call_node
.or(first_node)
.expect("match has at least one capture");
let caller_name = self.enclosing_function_name(&span_node, source);
calls.push(RawCallEdge {
caller_name,
callee_name,
call_site: Span {
start_byte: span_node.start_byte() as u32,
end_byte: span_node.end_byte() as u32,
},
kind: call_kind,
});
}
calls
}
fn imports_from_tree(&self, tree: &Tree, source: &[u8]) -> Vec<Import> {
let imports_query = match &self.imports_query {
Some(q) => q,
None => return vec![],
};
let root = tree.root_node();
let capture_names = imports_query.capture_names();
let mut cursor = QueryCursor::new();
let mut imports = Vec::new();
let mut matches = cursor.matches(imports_query, root, source);
while let Some(m) = { matches.advance(); matches.get() } {
let mut module_text: Option<String> = None;
let mut import_name_text: Option<String> = None;
let mut alias_text: Option<String> = None;
let mut is_relative_import = false;
for capture in m.captures {
let capture_name = capture_names[capture.index as usize];
match capture_name {
"module" => {
let text = Self::node_text(&capture.node, source);
module_text = Some(
text.trim_matches(|c| c == '"' || c == '\'').to_string(),
);
}
"import_name" => {
import_name_text =
Some(Self::node_text(&capture.node, source).to_string());
}
"alias" => {
alias_text = Some(Self::node_text(&capture.node, source).to_string());
}
"_relative" => {
is_relative_import = true;
}
_ => {}
}
}
let module_path = match module_text {
Some(ref m) if !m.is_empty() => m.clone(),
_ => continue,
};
let imported_name = import_name_text
.filter(|s| !s.is_empty())
.unwrap_or_else(|| {
module_path
.rsplit(['/', '.', ':'])
.next()
.unwrap_or(&module_path)
.to_string()
});
let alias = alias_text.filter(|s| !s.is_empty());
let is_external = if is_relative_import {
false
} else {
self.config.is_external_import(&module_path)
};
imports.push(Import {
module_path,
imported_name,
alias,
is_external,
});
}
imports
}
}
impl LanguageParser for QueryDrivenParser {
fn extensions(&self) -> &[&str] {
self.config.extensions()
}
fn extract_symbols(&self, source: &[u8], file_path: &Path) -> Result<Vec<Symbol>> {
if source.is_empty() {
return Ok(vec![]);
}
let tree = self.parse_tree(source)?;
Ok(self.symbols_from_tree(&tree, source, file_path))
}
fn extract_calls(&self, source: &[u8], _file_path: &Path) -> Result<Vec<RawCallEdge>> {
if source.is_empty() {
return Ok(vec![]);
}
let tree = self.parse_tree(source)?;
Ok(self.calls_from_tree(&tree, source))
}
fn extract_types(&self, _source: &[u8], _file_path: &Path) -> Result<Vec<TypeInfo>> {
Ok(vec![])
}
fn extract_imports(&self, source: &[u8], _file_path: &Path) -> Result<Vec<Import>> {
if source.is_empty() {
return Ok(vec![]);
}
let tree = self.parse_tree(source)?;
Ok(self.imports_from_tree(&tree, source))
}
fn parse_file(&self, source: &[u8], file_path: &Path) -> Result<FileAnalysis> {
if source.is_empty() {
return Ok(FileAnalysis {
symbols: vec![],
calls: vec![],
types: vec![],
imports: vec![],
});
}
let tree = self.parse_tree(source)?;
Ok(FileAnalysis {
symbols: self.symbols_from_tree(&tree, source, file_path),
calls: self.calls_from_tree(&tree, source),
types: vec![],
imports: self.imports_from_tree(&tree, source),
})
}
}