use crate::types::{
CallInfo, ClassInfo, FunctionInfo, ImplTraitInfo, ImportInfo, ReferenceInfo, ReferenceType,
};
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use tree_sitter::{Node, StreamingIterator};
use crate::parser::{CompiledQueries, ParserError, QUERY_CURSOR, TimeoutConfig};
#[allow(clippy::too_many_lines)] pub(crate) fn extract_imports_from_node(
node: &Node,
source: &str,
prefix: &str,
line: usize,
imports: &mut Vec<ImportInfo>,
) {
match node.kind() {
"identifier" | "self" | "super" | "crate" => {
let name = source[node.start_byte()..node.end_byte()].to_string();
imports.push(ImportInfo {
module: prefix.to_string(),
items: vec![name],
line,
});
}
"scoped_identifier" => {
let item = node
.child_by_field_name("name")
.map(|n| source[n.start_byte()..n.end_byte()].to_string())
.unwrap_or_default();
let module = node.child_by_field_name("path").map_or_else(
|| prefix.to_string(),
|p| {
let path_text = source[p.start_byte()..p.end_byte()].to_string();
if prefix.is_empty() {
path_text
} else {
format!("{prefix}::{path_text}")
}
},
);
if !item.is_empty() {
imports.push(ImportInfo {
module,
items: vec![item],
line,
});
}
}
"use_list" => {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
extract_imports_from_node(&child, source, prefix, line, imports);
}
}
"scoped_use_list" => {
let path = node
.child_by_field_name("path")
.map(|n| source[n.start_byte()..n.end_byte()].to_string())
.unwrap_or_default();
let new_prefix = if prefix.is_empty() {
path
} else {
format!("{prefix}::{path}")
};
if let Some(list) = node.child_by_field_name("list") {
let mut cursor = list.walk();
for child in list.named_children(&mut cursor) {
extract_imports_from_node(&child, source, &new_prefix, line, imports);
}
}
}
"use_wildcard" => {
let stripped = if prefix.ends_with("::*") {
prefix.strip_suffix("::*").unwrap_or(prefix)
} else {
prefix
};
let module = if stripped.is_empty() {
"*".to_string()
} else if stripped.ends_with("::") || stripped.ends_with(':') {
format!("{stripped}*")
} else {
format!("{stripped}::*")
};
imports.push(ImportInfo {
module,
items: vec!["*".to_string()],
line,
});
}
"use_as_clause" => {
let alias = node
.child_by_field_name("alias")
.map(|n| source[n.start_byte()..n.end_byte()].to_string())
.unwrap_or_default();
let module = if let Some(path_node) = node.child_by_field_name("path") {
match path_node.kind() {
"scoped_identifier" => path_node.child_by_field_name("path").map_or_else(
|| prefix.to_string(),
|p| {
let p_text = source[p.start_byte()..p.end_byte()].to_string();
if prefix.is_empty() {
p_text
} else {
format!("{prefix}::{p_text}")
}
},
),
_ => prefix.to_string(),
}
} else {
prefix.to_string()
};
if !alias.is_empty() {
imports.push(ImportInfo {
module,
items: vec![alias],
line,
});
}
}
"import_from_statement" => {
extract_python_import_from(node, source, line, imports);
}
_ => {
let text = source[node.start_byte()..node.end_byte()]
.trim()
.to_string();
if !text.is_empty() {
imports.push(ImportInfo {
module: text,
items: vec![],
line,
});
}
}
}
}
pub(crate) fn extract_import_item_name(child: &Node, source: &str) -> Option<String> {
match child.kind() {
"dotted_name" => {
let name = source[child.start_byte()..child.end_byte()]
.trim()
.to_string();
if name.is_empty() { None } else { Some(name) }
}
"aliased_import" => child.child_by_field_name("name").and_then(|n| {
let name = source[n.start_byte()..n.end_byte()].trim().to_string();
if name.is_empty() { None } else { Some(name) }
}),
_ => None,
}
}
pub(crate) fn collect_import_items(
node: &Node,
source: &str,
is_wildcard: &mut bool,
items: &mut Vec<String>,
) {
if let Some(import_list) = node.child_by_field_name("import_list") {
let mut cursor = import_list.walk();
for child in import_list.named_children(&mut cursor) {
if child.kind() == "wildcard_import" {
*is_wildcard = true;
} else if let Some(name) = extract_import_item_name(&child, source) {
items.push(name);
}
}
return;
}
let mut cursor = node.walk();
let mut first = true;
for child in node.named_children(&mut cursor) {
if first {
first = false;
continue;
}
if child.kind() == "wildcard_import" {
*is_wildcard = true;
} else if let Some(name) = extract_import_item_name(&child, source) {
items.push(name);
}
}
}
pub(crate) fn extract_python_import_from(
node: &Node,
source: &str,
line: usize,
imports: &mut Vec<ImportInfo>,
) {
let module = if let Some(m) = node.child_by_field_name("module_name") {
source[m.start_byte()..m.end_byte()].trim().to_string()
} else if let Some(r) = node.child_by_field_name("relative_import") {
source[r.start_byte()..r.end_byte()].trim().to_string()
} else {
String::new()
};
let mut is_wildcard = false;
let mut items = Vec::new();
collect_import_items(node, source, &mut is_wildcard, &mut items);
if !module.is_empty() {
imports.push(ImportInfo {
module,
items: if is_wildcard {
vec!["*".to_string()]
} else {
items
},
line,
});
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn extract_elements(
source: &str,
compiled: &CompiledQueries,
root: Node<'_>,
max_depth: Option<u32>,
functions: &mut Vec<FunctionInfo>,
classes: &mut Vec<ClassInfo>,
tc: TimeoutConfig,
lang_info: &crate::languages::LanguageInfo,
) -> Result<(), ParserError> {
let mut seen_functions = HashSet::new();
let mut timed_out = false;
QUERY_CURSOR.with(|c| {
let mut cursor = c.borrow_mut();
cursor.set_max_start_depth(None);
if let Some(depth) = max_depth {
cursor.set_max_start_depth(Some(depth));
}
let mut matches = cursor.matches(&compiled.element, root, source.as_bytes());
while let Some(mat) = matches.next() {
if tc.is_exceeded() {
timed_out = true;
break;
}
let mut func_node: Option<Node> = None;
let mut class_node: Option<Node> = None;
let mut func_name_text: Option<String> = None;
let mut class_name_text: Option<String> = None;
for capture in mat.captures {
let capture_name = compiled.element.capture_names()[capture.index as usize];
let node = capture.node;
match capture_name {
"function" => func_node = Some(node),
"func_name" | "method_name" => {
func_name_text =
Some(source[node.start_byte()..node.end_byte()].to_string());
}
"class" => class_node = Some(node),
"class_name" | "type_name" => {
class_name_text =
Some(source[node.start_byte()..node.end_byte()].to_string());
}
_ => {}
}
}
if let Some(func_node) = func_node {
let parent_kind = func_node.parent().map(|p| p.kind());
let parent_is_wrapper = parent_kind
.map(|k| k == "template_declaration" || k == "decorated_definition")
.unwrap_or(false);
if func_node.kind() == "function_definition" && parent_is_wrapper {
} else {
let func_def = if func_node.kind() == "template_declaration" {
let mut cursor = func_node.walk();
func_node
.children(&mut cursor)
.find(|n| n.kind() == "function_definition")
.unwrap_or(func_node)
} else if func_node.kind() == "decorated_definition" {
func_node
.child_by_field_name("definition")
.unwrap_or(func_node)
} else {
func_node
};
let name = func_name_text
.or_else(|| {
func_def
.child_by_field_name("name")
.map(|n| source[n.start_byte()..n.end_byte()].to_string())
})
.unwrap_or_default();
let func_key = (name.clone(), func_node.start_position().row);
if !name.is_empty() && seen_functions.insert(func_key) {
let params = func_def
.child_by_field_name("declarator")
.and_then(|d| d.child_by_field_name("parameters"))
.or_else(|| func_def.child_by_field_name("parameters"))
.map(|p| source[p.start_byte()..p.end_byte()].to_string())
.unwrap_or_default();
let return_type = func_def
.child_by_field_name("type")
.or_else(|| func_def.child_by_field_name("return_type"))
.map(|r| source[r.start_byte()..r.end_byte()].to_string());
let first_line = if func_node.kind() == "function_item" {
let mut attrs: Vec<Node> = Vec::new();
let mut sib = func_node.prev_named_sibling();
while let Some(s) = sib {
if s.kind() == "attribute_item" {
attrs.push(s);
sib = s.prev_named_sibling();
} else {
break;
}
}
attrs
.last()
.map(|n| n.start_position().row + 1)
.unwrap_or_else(|| func_node.start_position().row + 1)
} else {
func_node.start_position().row + 1
};
functions.push(FunctionInfo {
name,
line: first_line,
end_line: func_node.end_position().row + 1,
parameters: if params.is_empty() {
Vec::new()
} else {
vec![params]
},
return_type,
});
}
}
}
if let Some(class_node) = class_node {
let name = class_name_text
.or_else(|| {
class_node
.child_by_field_name("name")
.map(|n| source[n.start_byte()..n.end_byte()].to_string())
})
.unwrap_or_default();
if !name.is_empty() {
let inherits = if let Some(handler) = lang_info.extract_inheritance {
handler(&class_node, source)
} else {
Vec::new()
};
classes.push(ClassInfo {
name,
line: class_node.start_position().row + 1,
end_line: class_node.end_position().row + 1,
methods: Vec::new(),
fields: Vec::new(),
inherits,
});
}
}
}
});
if timed_out {
return Err(ParserError::Timeout(tc.micros));
}
Ok(())
}
pub(crate) fn enclosing_function_name(mut node: Node<'_>, source: &str) -> Option<String> {
let mut depth = 0;
loop {
let parent = node.parent()?;
depth += 1;
if depth > 64 {
return None;
}
let name_node = match parent.kind() {
"function_item"
| "method_item"
| "function_definition"
| "function_declaration"
| "method_declaration"
| "method_definition" => parent.child_by_field_name("name"),
"subroutine" => {
let mut cursor = parent.walk();
parent
.children(&mut cursor)
.find(|c| c.kind() == "subroutine_statement")
.and_then(|s| s.child_by_field_name("name"))
}
"function" => {
let mut cursor = parent.walk();
parent
.children(&mut cursor)
.find(|c| c.kind() == "function_statement")
.and_then(|s| s.child_by_field_name("name"))
}
_ => {
node = parent;
continue;
}
};
return name_node.map(|n| source[n.start_byte()..n.end_byte()].to_string());
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn extract_calls(
source: &str,
compiled: &CompiledQueries,
root: Node<'_>,
max_depth: Option<u32>,
calls: &mut Vec<CallInfo>,
call_frequency: &mut HashMap<String, usize>,
tc: TimeoutConfig,
) -> Result<(), ParserError> {
let mut timed_out = false;
QUERY_CURSOR.with(|c| {
let mut cursor = c.borrow_mut();
cursor.set_max_start_depth(None);
if let Some(depth) = max_depth {
cursor.set_max_start_depth(Some(depth));
}
let mut matches = cursor.matches(&compiled.call, root, source.as_bytes());
while let Some(mat) = matches.next() {
if tc.is_exceeded() {
timed_out = true;
break;
}
for capture in mat.captures {
let capture_name = compiled.call.capture_names()[capture.index as usize];
if capture_name != "call" {
continue;
}
let node = capture.node;
let call_name = source[node.start_byte()..node.end_byte()].to_string();
*call_frequency.entry(call_name.clone()).or_insert(0) += 1;
let caller = enclosing_function_name(node, source)
.unwrap_or_else(|| "<module>".to_string());
let mut arg_count = None;
let mut arg_node = node;
let mut hop = 0u32;
while let Some(parent) = arg_node.parent() {
hop += 1;
if hop > 16 {
tracing::debug!(hop, callee = %call_name, "extract_calls: parent traversal cap reached; arg_count will be None");
break;
}
if parent.kind() == "call_expression" {
if let Some(args) = parent.child_by_field_name("arguments") {
arg_count = Some(args.named_child_count());
}
break;
}
arg_node = parent;
}
calls.push(CallInfo {
caller,
callee: call_name,
line: node.start_position().row + 1,
column: node.start_position().column,
arg_count,
});
}
}
});
if timed_out {
return Err(ParserError::Timeout(tc.micros));
}
Ok(())
}
pub(crate) fn extract_imports(
source: &str,
compiled: &CompiledQueries,
root: Node<'_>,
max_depth: Option<u32>,
imports: &mut Vec<ImportInfo>,
tc: TimeoutConfig,
) -> Result<(), ParserError> {
let Some(import_query) = &compiled.import else {
return Ok(());
};
let mut timed_out = false;
QUERY_CURSOR.with(|c| {
let mut cursor = c.borrow_mut();
cursor.set_max_start_depth(None);
if let Some(depth) = max_depth {
cursor.set_max_start_depth(Some(depth));
}
let mut matches = cursor.matches(import_query, root, source.as_bytes());
while let Some(mat) = matches.next() {
if tc.is_exceeded() {
timed_out = true;
break;
}
for capture in mat.captures {
let capture_name = import_query.capture_names()[capture.index as usize];
let node = capture.node;
let line = node.start_position().row + 1;
if capture_name == "import_path" {
extract_imports_from_node(&node, source, "", line, imports);
}
}
}
});
if timed_out {
return Err(ParserError::Timeout(tc.micros));
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn extract_impl_methods(
source: &str,
compiled: &CompiledQueries,
root: Node<'_>,
max_depth: Option<u32>,
classes: &mut [ClassInfo],
tc: TimeoutConfig,
) -> Result<(), ParserError> {
let Some(impl_query) = &compiled.impl_block else {
return Ok(());
};
let mut timed_out = false;
QUERY_CURSOR.with(|c| {
let mut cursor = c.borrow_mut();
cursor.set_max_start_depth(None);
if let Some(depth) = max_depth {
cursor.set_max_start_depth(Some(depth));
}
let mut matches = cursor.matches(impl_query, root, source.as_bytes());
while let Some(mat) = matches.next() {
if tc.is_exceeded() {
timed_out = true;
break;
}
let mut impl_type_name = String::new();
let mut method_name = String::new();
let mut method_line = 0usize;
let mut method_end_line = 0usize;
let mut method_params = String::new();
let mut method_return_type: Option<String> = None;
for capture in mat.captures {
let capture_name = impl_query.capture_names()[capture.index as usize];
let node = capture.node;
match capture_name {
"impl_type" => {
impl_type_name = source[node.start_byte()..node.end_byte()].to_string();
}
"method_name" => {
method_name = source[node.start_byte()..node.end_byte()].to_string();
}
"method_params" => {
method_params = source[node.start_byte()..node.end_byte()].to_string();
}
"method" => {
let mut method_attrs: Vec<Node> = Vec::new();
let mut msib = node.prev_named_sibling();
while let Some(s) = msib {
if s.kind() == "attribute_item" {
method_attrs.push(s);
msib = s.prev_named_sibling();
} else {
break;
}
}
method_line = method_attrs
.last()
.map(|n| n.start_position().row + 1)
.unwrap_or_else(|| node.start_position().row + 1);
method_end_line = node.end_position().row + 1;
method_return_type = node
.child_by_field_name("return_type")
.map(|r| source[r.start_byte()..r.end_byte()].to_string());
}
_ => {}
}
}
if !impl_type_name.is_empty() && !method_name.is_empty() {
let func = FunctionInfo {
name: method_name,
line: method_line,
end_line: method_end_line,
parameters: if method_params.is_empty() {
Vec::new()
} else {
vec![method_params]
},
return_type: method_return_type,
};
if let Some(class) = classes.iter_mut().find(|c| c.name == impl_type_name) {
class.methods.push(func);
}
}
}
});
if timed_out {
return Err(ParserError::Timeout(tc.micros));
}
Ok(())
}
pub(crate) fn extract_references(
source: &str,
compiled: &CompiledQueries,
root: Node<'_>,
max_depth: Option<u32>,
references: &mut Vec<ReferenceInfo>,
tc: TimeoutConfig,
) -> Result<(), ParserError> {
let Some(ref ref_query) = compiled.reference else {
return Ok(());
};
let mut seen_refs = HashSet::new();
let mut timed_out = false;
QUERY_CURSOR.with(|c| {
let mut cursor = c.borrow_mut();
cursor.set_max_start_depth(None);
if let Some(depth) = max_depth {
cursor.set_max_start_depth(Some(depth));
}
let mut matches = cursor.matches(ref_query, root, source.as_bytes());
while let Some(mat) = matches.next() {
if tc.is_exceeded() {
timed_out = true;
break;
}
for capture in mat.captures {
let capture_name = ref_query.capture_names()[capture.index as usize];
if capture_name == "type_ref" {
let node = capture.node;
let type_ref = source[node.start_byte()..node.end_byte()].to_string();
if seen_refs.insert(type_ref.clone()) {
references.push(ReferenceInfo {
symbol: type_ref,
reference_type: ReferenceType::Usage,
location: String::new(),
line: node.start_position().row + 1,
});
}
}
}
}
});
if timed_out {
return Err(ParserError::Timeout(tc.micros));
}
Ok(())
}
pub(crate) fn extract_impl_traits_from_tree(
source: &str,
compiled: &CompiledQueries,
root: Node<'_>,
tc: TimeoutConfig,
) -> Result<Vec<ImplTraitInfo>, ParserError> {
let Some(query) = &compiled.impl_trait else {
return Ok(vec![]);
};
let mut results = Vec::new();
let mut timed_out = false;
QUERY_CURSOR.with(|c| {
let mut cursor = c.borrow_mut();
cursor.set_max_start_depth(None);
let mut matches = cursor.matches(query, root, source.as_bytes());
while let Some(mat) = matches.next() {
if tc.is_exceeded() {
timed_out = true;
break;
}
let mut trait_name = String::new();
let mut impl_type = String::new();
let mut line = 0usize;
for capture in mat.captures {
let capture_name = query.capture_names()[capture.index as usize];
let node = capture.node;
let text = source[node.start_byte()..node.end_byte()].to_string();
match capture_name {
"trait_name" => {
trait_name = text;
line = node.start_position().row + 1;
}
"impl_type" => {
impl_type = text;
}
_ => {}
}
}
if !trait_name.is_empty() && !impl_type.is_empty() {
results.push(ImplTraitInfo {
trait_name,
impl_type,
path: PathBuf::new(), line,
});
}
}
});
if timed_out {
return Err(ParserError::Timeout(tc.micros));
}
Ok(results)
}
pub(crate) fn extract_def_use(
source: &str,
compiled: &CompiledQueries,
root: Node<'_>,
symbol_name: &str,
file_path: &str,
max_depth: Option<u32>,
) -> Vec<crate::types::DefUseSite> {
let Some(defuse_query) = &compiled.defuse else {
return vec![];
};
let mut sites = Vec::new();
let mut write_offsets = HashSet::new();
let source_lines: Vec<&str> = source.lines().collect();
QUERY_CURSOR.with(|c| {
let mut cursor = c.borrow_mut();
cursor.set_max_start_depth(None);
if let Some(depth) = max_depth {
cursor.set_max_start_depth(Some(depth));
}
let mut matches = cursor.matches(defuse_query, root, source.as_bytes());
while let Some(mat) = matches.next() {
for capture in mat.captures {
let capture_name = defuse_query.capture_names()[capture.index as usize];
let node = capture.node;
let node_text = node.utf8_text(source.as_bytes()).unwrap_or_default();
if node_text != symbol_name {
continue;
}
let kind = if capture_name.starts_with("write.") {
crate::types::DefUseKind::Write
} else if capture_name.starts_with("read.") {
crate::types::DefUseKind::Read
} else if capture_name.starts_with("writeread.") {
crate::types::DefUseKind::WriteRead
} else {
continue;
};
let byte_offset = node.start_byte();
if kind == crate::types::DefUseKind::Read && write_offsets.contains(&byte_offset) {
continue;
}
if kind != crate::types::DefUseKind::Read {
write_offsets.insert(byte_offset);
}
let line = node.start_position().row + 1;
let snippet = {
let row = node.start_position().row;
let last_line = source_lines.len().saturating_sub(1);
let prev = if row > 0 { row - 1 } else { 0 };
let next = std::cmp::min(row + 1, last_line);
let prev_text = if row == 0 {
""
} else {
source_lines[prev].trim_end()
};
let cur_text = source_lines[row].trim_end();
let next_text = if row >= last_line {
""
} else {
source_lines[next].trim_end()
};
format!("{prev_text}\n{cur_text}\n{next_text}")
};
let enclosing_scope = enclosing_function_name(node, source);
let column = node.start_position().column;
sites.push(crate::types::DefUseSite {
kind,
symbol: node_text.to_string(),
file: file_path.to_string(),
line,
column,
snippet,
enclosing_scope,
});
}
}
});
sites
}