use crate::analyzers::LanguageAnalyzer;
use crate::types::{
AnalysisError, ExportStatement, FileAnalysis, FunctionCall, FunctionSignature, ImportStatement,
Parameter, PartialAnalysis, Result, StructField, StructSignature, TreeNode, TypeKind,
};
use async_trait::async_trait;
use blake3;
use regex::Regex;
use std::time::Instant;
use streaming_iterator::StreamingIterator;
use tree_sitter::{Language, Node, Parser, Query, QueryCursor, Tree};
#[derive(Clone)]
pub struct RustAnalyzer {
language: Language,
}
impl RustAnalyzer {
pub fn new() -> Result<Self> {
Ok(Self {
language: tree_sitter_rust::LANGUAGE.into(),
})
}
fn parse_parameter(&self, param_node: &Node, source: &str) -> Result<Parameter> {
let query_str = r#"
(parameter
pattern: (identifier) @param_name
type: (_) @param_type
)
(parameter
pattern: (mut_pattern (identifier) @mut_param_name)
type: (_) @param_type
)
"#;
let query =
Query::new(&self.language, query_str).map_err(|e| AnalysisError::QueryError {
message: format!("{:?}", e),
})?;
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&query, *param_node, source.as_bytes());
let mut param_name = String::new();
let mut param_type = String::new();
let mut is_mutable = false;
while let Some(query_match) = matches.next() {
for capture in query_match.captures {
let capture_name = query.capture_names()[capture.index as usize];
let text = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
match capture_name {
"param_name" => param_name = text.to_string(),
"mut_param_name" => {
param_name = text.to_string();
is_mutable = true;
}
"param_type" => param_type = text.to_string(),
_ => {}
}
}
}
if param_name.is_empty() {
let node_text = param_node.utf8_text(source.as_bytes()).unwrap_or("");
if let Some((name_part, type_part)) = node_text.split_once(':') {
param_name = name_part.trim().to_string();
param_type = type_part.trim().to_string();
is_mutable = param_name.starts_with("mut ");
if is_mutable {
param_name = param_name.trim_start_matches("mut ").to_string();
}
}
}
Ok(Parameter::new(param_name, param_type).with_mutability(is_mutable))
}
fn parse_struct_field(&self, field_node: &Node, source: &str) -> Result<StructField> {
let query_str = r#"
(field_declaration
(visibility_modifier)? @visibility
name: (field_identifier) @field_name
type: (_) @field_type
)
"#;
let query =
Query::new(&self.language, query_str).map_err(|e| AnalysisError::QueryError {
message: format!("{:?}", e),
})?;
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&query, *field_node, source.as_bytes());
let mut field_name = String::new();
let mut field_type = String::new();
let mut is_public = false;
while let Some(query_match) = matches.next() {
for capture in query_match.captures {
let capture_name = query.capture_names()[capture.index as usize];
let text = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
match capture_name {
"field_name" => field_name = text.to_string(),
"field_type" => field_type = text.to_string(),
"visibility" => is_public = text.contains("pub"),
_ => {}
}
}
}
Ok(StructField::new(field_name, field_type).with_visibility(is_public))
}
fn extract_generics(&self, node: &Node, source: &str) -> Vec<String> {
let query_str = r#"
(type_parameters
(type_parameter name: (type_identifier) @generic)
)
"#;
if let Ok(query) = Query::new(&self.language, query_str) {
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&query, *node, source.as_bytes());
let mut generics = Vec::new();
while let Some(query_match) = matches.next() {
for capture in query_match.captures {
let capture_name = query.capture_names()[capture.index as usize];
if capture_name == "generic" {
let text = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
generics.push(text.to_string());
}
}
}
generics
} else {
Vec::new()
}
}
fn calculate_content_hash(&self, content: &str) -> String {
blake3::hash(content.as_bytes()).to_hex().to_string()
}
fn extract_declared_modules(&self, tree: &Tree, source: &str) -> Result<Vec<String>> {
let query =
Query::new(&self.language, "(mod_item name: (identifier) @name)").map_err(|e| {
AnalysisError::QueryError {
message: format!("{:?}", e),
}
})?;
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
let mut modules = Vec::new();
while let Some(query_match) = matches.next() {
for capture in query_match.captures {
if let Ok(name) = capture.node.utf8_text(source.as_bytes()) {
let name = name.to_string();
if !modules.contains(&name) {
modules.push(name);
}
}
}
}
Ok(modules)
}
}
#[async_trait]
impl LanguageAnalyzer for RustAnalyzer {
fn language(&self) -> &'static str {
"rust"
}
fn file_extensions(&self) -> &[&'static str] {
&["rs"]
}
fn supports_async(&self) -> bool {
true
}
async fn analyze_file(&self, content: &str, file_path: &str) -> Result<FileAnalysis> {
let start_time = Instant::now();
let mut tree_node = TreeNode::new(file_path.to_string(), "rust".to_string());
tree_node.content_hash = self.calculate_content_hash(content);
tree_node.last_modified = std::time::SystemTime::now();
let mut parser = Parser::new();
parser
.set_language(&self.language)
.map_err(|e| AnalysisError::ParseError {
message: format!("Failed to set language: {:?}", e),
})?;
let tree = parser
.parse(content, None)
.ok_or_else(|| AnalysisError::ParseError {
message: "Failed to parse file".to_string(),
})?;
match self.extract_functions(&tree, content, file_path) {
Ok(functions) => tree_node.functions = functions,
Err(e) => tree_node.add_error(format!("Function extraction failed: {}", e)),
}
match self.extract_structs(&tree, content, file_path) {
Ok(structs) => tree_node.structs = structs,
Err(e) => tree_node.add_error(format!("Struct extraction failed: {}", e)),
}
match self.extract_imports(&tree, content, file_path) {
Ok(imports) => tree_node.imports = imports,
Err(e) => tree_node.add_error(format!("Import extraction failed: {}", e)),
}
match self.extract_exports(&tree, content, file_path) {
Ok(exports) => tree_node.exports = exports,
Err(e) => tree_node.add_error(format!("Export extraction failed: {}", e)),
}
match self.extract_function_calls(&tree, content, file_path) {
Ok(function_calls) => tree_node.function_calls = function_calls,
Err(e) => tree_node.add_error(format!("Function call extraction failed: {}", e)),
}
match self.extract_declared_modules(&tree, content) {
Ok(modules) => tree_node.declared_modules = modules,
Err(e) => tree_node.add_error(format!("Module declaration extraction failed: {}", e)),
}
let duration = start_time.elapsed().as_millis() as u64;
Ok(FileAnalysis::new(tree_node, duration))
}
fn extract_functions(
&self,
tree: &Tree,
source: &str,
file_path: &str,
) -> Result<Vec<FunctionSignature>> {
let query_str = r#"
[
(function_item name: (identifier) @name)
(function_signature_item name: (identifier) @name)
] @function
"#;
let query =
Query::new(&self.language, query_str).map_err(|e| AnalysisError::QueryError {
message: format!("{:?}", e),
})?;
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
let mut functions = Vec::new();
while let Some(query_match) = matches.next() {
let mut function_sig = FunctionSignature::new(String::new(), file_path.to_string());
let mut function_node: Option<Node> = None;
for capture in query_match.captures {
let capture_name = query.capture_names()[capture.index as usize];
let text = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
match capture_name {
"name" => function_sig.name = text.to_string(),
"function" => {
function_node = Some(capture.node);
let start_point = capture.node.start_position();
let end_point = capture.node.end_position();
function_sig.start_line = start_point.row as u32 + 1;
function_sig.end_line = end_point.row as u32 + 1;
let function_text = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
function_sig.is_public = function_text.contains("pub ");
function_sig.is_async = function_text.contains("async ");
function_sig.is_const = function_text.contains("const ");
function_sig.is_extern = function_text.contains("extern ");
function_sig.generics = self.extract_generics(&capture.node, source);
if let Some(arrow_pos) = function_text.find(" -> ") {
let after_arrow = &function_text[arrow_pos + 4..];
if let Some(brace_pos) = after_arrow.find(" {") {
let return_type = after_arrow[..brace_pos].trim();
function_sig.return_type = Some(return_type.to_string());
} else if let Some(semicolon_pos) = after_arrow.find(";") {
let return_type = after_arrow[..semicolon_pos].trim();
function_sig.return_type = Some(return_type.to_string());
}
}
let mut child_cursor = capture.node.walk();
if child_cursor.goto_first_child() {
loop {
let child = child_cursor.node();
if child.kind() == "parameters" {
let mut param_cursor = child.walk();
if param_cursor.goto_first_child() {
loop {
let param_child = param_cursor.node();
if param_child.kind() == "self_parameter" {
function_sig.parameters.push(Parameter::new(
"self".to_string(),
String::new(),
));
} else if param_child.kind() == "parameter" {
let param_text = param_child
.utf8_text(source.as_bytes())
.unwrap_or("");
if let Some((name_part, type_part)) =
param_text.split_once(':')
{
let param_name = name_part.trim().to_string();
let param_type = type_part.trim().to_string();
let is_mutable = param_name.starts_with("mut ");
let clean_name = if is_mutable {
param_name
.trim_start_matches("mut ")
.to_string()
} else {
param_name
};
let param =
Parameter::new(clean_name, param_type)
.with_mutability(is_mutable);
function_sig.parameters.push(param);
}
}
if !param_cursor.goto_next_sibling() {
break;
}
}
}
break;
}
if !child_cursor.goto_next_sibling() {
break;
}
}
}
}
_ => {}
}
}
if let Some(node) = function_node {
let container = node
.parent()
.filter(|p| p.kind() == "declaration_list")
.and_then(|parent| parent.parent())
.filter(|gp| gp.kind() == "impl_item" || gp.kind() == "trait_item");
if let Some(trait_node) = container.filter(|c| c.kind() == "trait_item") {
if let Some(name_node) = trait_node.child_by_field_name("name")
&& let Ok(owner) = name_node.utf8_text(source.as_bytes())
{
function_sig.owner = Some(owner.to_string());
}
let has_self = function_sig
.parameters
.first()
.map(|p| p.name == "self")
.unwrap_or(false);
function_sig.is_static = !has_self;
}
let impl_item = container.filter(|c| c.kind() == "impl_item");
if let Some(impl_node) = impl_item {
let has_self = function_sig
.parameters
.first()
.map(|p| p.name == "self")
.unwrap_or(false);
function_sig.is_static = !has_self;
if let Some(type_node) = impl_node.child_by_field_name("type") {
let base_node = if type_node.kind() == "generic_type" {
type_node.child_by_field_name("type").unwrap_or(type_node)
} else {
type_node
};
if let Ok(owner) = base_node.utf8_text(source.as_bytes()) {
function_sig.owner = Some(owner.to_string());
}
}
}
}
if !function_sig.name.is_empty() {
functions.push(function_sig);
}
}
Ok(functions)
}
fn extract_structs(
&self,
tree: &Tree,
source: &str,
file_path: &str,
) -> Result<Vec<StructSignature>> {
let query_str = r#"
(struct_item
(visibility_modifier)? @visibility
name: (type_identifier) @name
(type_parameters)? @generics
body: (field_declaration_list) @fields
) @struct
(struct_item
(visibility_modifier)? @tuple_visibility
name: (type_identifier) @tuple_name
(type_parameters)? @tuple_generics
body: (ordered_field_declaration_list) @tuple_fields
) @tuple_struct
; Unit struct (`struct Foo;`) — it has no field list of either kind, so
; neither pattern above can match it. ripgrep's flag definitions are ~100
; of these; a SCIP parity run surfaced every one as a missing symbol.
(struct_item
(visibility_modifier)? @unit_visibility
name: (type_identifier) @unit_name
(type_parameters)? @unit_generics
!body
) @unit_struct
"#;
let query =
Query::new(&self.language, query_str).map_err(|e| AnalysisError::QueryError {
message: format!("{:?}", e),
})?;
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
let mut structs = Vec::new();
while let Some(query_match) = matches.next() {
let mut struct_sig = StructSignature::new(String::new(), file_path.to_string());
struct_sig.kind = TypeKind::Struct;
for capture in query_match.captures {
let capture_name = query.capture_names()[capture.index as usize];
let text = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
match capture_name {
"name" | "tuple_name" | "unit_name" => struct_sig.name = text.to_string(),
"visibility" | "tuple_visibility" | "unit_visibility" => {
struct_sig.is_public = text.contains("pub")
}
"generics" | "tuple_generics" | "unit_generics" => {
struct_sig.generics = self.extract_generics(&capture.node, source);
}
"fields" => {
let mut child_cursor = capture.node.walk();
if child_cursor.goto_first_child() {
loop {
let child = child_cursor.node();
if child.kind() == "field_declaration" {
if let Ok(field) = self.parse_struct_field(&child, source) {
struct_sig.fields.push(field);
}
}
if !child_cursor.goto_next_sibling() {
break;
}
}
}
}
"tuple_fields" => {
struct_sig.is_tuple_struct = true;
let mut child_cursor = capture.node.walk();
let mut field_index = 0;
let mut pending_pub = false;
if child_cursor.goto_first_child() {
loop {
let child = child_cursor.node();
if child.kind() == "visibility_modifier" {
pending_pub = true;
} else if child_cursor.field_name() == Some("type") {
let field_type = child
.utf8_text(source.as_bytes())
.unwrap_or("")
.to_string();
let field =
StructField::new(field_index.to_string(), field_type)
.with_visibility(pending_pub);
struct_sig.fields.push(field);
field_index += 1;
pending_pub = false;
}
if !child_cursor.goto_next_sibling() {
break;
}
}
}
}
"struct" | "tuple_struct" | "unit_struct" => {
let start_point = capture.node.start_position();
let end_point = capture.node.end_position();
struct_sig.start_line = start_point.row as u32 + 1;
struct_sig.end_line = end_point.row as u32 + 1;
}
_ => {}
}
}
if !struct_sig.name.is_empty() {
structs.push(struct_sig);
}
}
let enum_trait_query_str = r#"
(enum_item
(visibility_modifier)? @enum_visibility
name: (type_identifier) @enum_name
(type_parameters)? @enum_generics
) @enum
(trait_item
(visibility_modifier)? @trait_visibility
name: (type_identifier) @trait_name
(type_parameters)? @trait_generics
bounds: (trait_bounds)? @trait_bounds
) @trait
(type_item
(visibility_modifier)? @alias_visibility
name: (type_identifier) @alias_name
(type_parameters)? @alias_generics
) @type_alias
; An associated type in a trait (`type Captures: Captures;`) is a
; distinct node from a free `type` alias, so it needs its own pattern.
(associated_type
name: (type_identifier) @alias_name
(type_parameters)? @alias_generics
) @type_alias
"#;
let enum_trait_query = Query::new(&self.language, enum_trait_query_str).map_err(|e| {
AnalysisError::QueryError {
message: format!("{:?}", e),
}
})?;
let mut et_cursor = QueryCursor::new();
let mut et_matches =
et_cursor.matches(&enum_trait_query, tree.root_node(), source.as_bytes());
while let Some(query_match) = et_matches.next() {
let mut sig = StructSignature::new(String::new(), file_path.to_string());
for capture in query_match.captures {
let capture_name = enum_trait_query.capture_names()[capture.index as usize];
let text = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
match capture_name {
"enum_name" | "trait_name" | "alias_name" => sig.name = text.to_string(),
"enum_visibility" | "trait_visibility" | "alias_visibility" => {
sig.is_public = text.contains("pub")
}
"enum_generics" | "trait_generics" | "alias_generics" => {
sig.generics = self.extract_generics(&capture.node, source);
}
"trait_bounds" => {
let mut bound_cursor = capture.node.walk();
if bound_cursor.goto_first_child() {
loop {
let bound = bound_cursor.node();
if bound.kind() == "type_identifier" {
if let Ok(name) = bound.utf8_text(source.as_bytes()) {
sig.supertypes.push(name.to_string());
}
}
if !bound_cursor.goto_next_sibling() {
break;
}
}
}
}
"enum" => {
sig.kind = TypeKind::Enum;
let start_point = capture.node.start_position();
let end_point = capture.node.end_position();
sig.start_line = start_point.row as u32 + 1;
sig.end_line = end_point.row as u32 + 1;
}
"trait" => {
sig.kind = TypeKind::Trait;
let start_point = capture.node.start_position();
let end_point = capture.node.end_position();
sig.start_line = start_point.row as u32 + 1;
sig.end_line = end_point.row as u32 + 1;
}
"type_alias" => {
sig.kind = TypeKind::TypeAlias;
let start_point = capture.node.start_position();
let end_point = capture.node.end_position();
sig.start_line = start_point.row as u32 + 1;
sig.end_line = end_point.row as u32 + 1;
}
_ => {}
}
}
if !sig.name.is_empty() {
structs.push(sig);
}
}
let impl_query_str = r#"
(impl_item
trait: (_) @impl_trait
type: (_) @impl_type
)
"#;
let impl_query =
Query::new(&self.language, impl_query_str).map_err(|e| AnalysisError::QueryError {
message: format!("{:?}", e),
})?;
let mut impl_cursor = QueryCursor::new();
let mut impl_matches =
impl_cursor.matches(&impl_query, tree.root_node(), source.as_bytes());
let mut impl_pairs: Vec<(String, String)> = Vec::new();
while let Some(query_match) = impl_matches.next() {
let mut trait_name = String::new();
let mut type_name = String::new();
for capture in query_match.captures {
let capture_name = impl_query.capture_names()[capture.index as usize];
let text = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
match capture_name {
"impl_trait" => trait_name = text.to_string(),
"impl_type" => type_name = text.to_string(),
_ => {}
}
}
if !trait_name.is_empty() && !type_name.is_empty() {
impl_pairs.push((type_name, trait_name));
}
}
for (type_name, trait_name) in impl_pairs {
if let Some(target) = structs.iter_mut().find(|s| s.name == type_name) {
target.supertypes.push(trait_name);
}
}
Ok(structs)
}
fn extract_imports(
&self,
tree: &Tree,
source: &str,
file_path: &str,
) -> Result<Vec<ImportStatement>> {
let query_str = r#"
(use_declaration
argument: (_) @import_path
) @use_stmt
"#;
let query =
Query::new(&self.language, query_str).map_err(|e| AnalysisError::QueryError {
message: format!("{:?}", e),
})?;
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
let mut imports = Vec::new();
while let Some(query_match) = matches.next() {
let mut import_stmt = ImportStatement::new(String::new(), file_path.to_string());
for capture in query_match.captures {
let capture_name = query.capture_names()[capture.index as usize];
let text = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
match capture_name {
"import_path" => {
import_stmt.module_path = text.to_string();
import_stmt.is_external = !text.starts_with("crate::")
&& !text.starts_with("self::")
&& !text.starts_with("super::");
import_stmt.is_glob = text.contains("*");
}
"use_stmt" => {
let start_point = capture.node.start_position();
import_stmt.line_number = start_point.row as u32 + 1;
}
_ => {}
}
}
if !import_stmt.module_path.is_empty() {
imports.push(import_stmt);
}
}
Ok(imports)
}
fn extract_exports(
&self,
tree: &Tree,
source: &str,
file_path: &str,
) -> Result<Vec<ExportStatement>> {
let query_str = r#"
[
(function_item (visibility_modifier) @vis name: (identifier) @name)
(struct_item (visibility_modifier) @vis name: (type_identifier) @name)
(enum_item (visibility_modifier) @vis name: (type_identifier) @name)
(type_item (visibility_modifier) @vis name: (type_identifier) @name)
(const_item (visibility_modifier) @vis name: (identifier) @name)
(static_item (visibility_modifier) @vis name: (identifier) @name)
(mod_item (visibility_modifier) @vis name: (identifier) @name)
] @export_item
"#;
let query =
Query::new(&self.language, query_str).map_err(|e| AnalysisError::QueryError {
message: format!("{:?}", e),
})?;
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
let mut exports = Vec::new();
while let Some(query_match) = matches.next() {
let mut export_stmt = ExportStatement::new(String::new(), file_path.to_string());
for capture in query_match.captures {
let capture_name = query.capture_names()[capture.index as usize];
let text = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
match capture_name {
"name" => export_stmt.exported_item = text.to_string(),
"vis" => export_stmt.is_public = text.contains("pub"),
"export_item" => {
let start_point = capture.node.start_position();
export_stmt.line_number = start_point.row as u32 + 1;
}
_ => {}
}
}
if !export_stmt.exported_item.is_empty() && export_stmt.is_public {
exports.push(export_stmt);
}
}
Ok(exports)
}
fn extract_function_calls(
&self,
tree: &Tree,
source: &str,
file_path: &str,
) -> Result<Vec<FunctionCall>> {
let query_str = r#"
(call_expression
function: (identifier) @function_name
) @call
(call_expression
function: (field_expression
value: (_) @receiver
field: (field_identifier) @method_name
)
) @method_call
"#;
let query =
Query::new(&self.language, query_str).map_err(|e| AnalysisError::QueryError {
message: format!("{:?}", e),
})?;
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
let mut function_calls = Vec::new();
while let Some(query_match) = matches.next() {
let mut function_call = FunctionCall::new(String::new(), file_path.to_string(), 0);
for capture in query_match.captures {
let capture_name = query.capture_names()[capture.index as usize];
let text = capture.node.utf8_text(source.as_bytes()).unwrap_or("");
match capture_name {
"function_name" => {
function_call.function_name = text.to_string();
}
"method_name" => {
function_call.function_name = text.to_string();
}
"receiver" => {
function_call = function_call.with_method_call(text.to_string());
}
"call" | "method_call" => {
let start_point = capture.node.start_position();
function_call.line_number = start_point.row as u32 + 1;
function_call.column = start_point.column as u32;
}
_ => {}
}
}
if !function_call.function_name.is_empty() {
function_calls.push(function_call);
}
}
Ok(function_calls)
}
fn extract_with_fallback(&self, content: &str, file_path: &str) -> PartialAnalysis {
let mut analysis =
PartialAnalysis::new(file_path.to_string(), "rust".to_string()).with_fallback();
if let Ok(fn_regex) = Regex::new(r"(?m)^\s*(pub\s+)?(const\s+)?(async\s+)?fn\s+(\w+)") {
for caps in fn_regex.captures_iter(content) {
if let Some(name_match) = caps.get(4) {
let mut func = FunctionSignature::new(
name_match.as_str().to_string(),
file_path.to_string(),
);
func.is_public = caps.get(1).is_some();
func.is_const = caps.get(2).is_some();
func.is_async = caps.get(3).is_some();
analysis.functions.push(func);
}
}
} else {
analysis.add_error("Failed to create function regex".to_string());
}
if let Ok(struct_regex) = Regex::new(r"(?m)^\s*(pub\s+)?struct\s+(\w+)") {
for caps in struct_regex.captures_iter(content) {
if let Some(name_match) = caps.get(2) {
let mut struct_sig = StructSignature::new(
name_match.as_str().to_string(),
file_path.to_string(),
);
struct_sig.is_public = caps.get(1).is_some();
analysis.structs.push(struct_sig);
}
}
} else {
analysis.add_error("Failed to create struct regex".to_string());
}
analysis
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio;
#[tokio::test]
async fn test_debug_tree_sitter() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
fn hello() {
println!("Hello");
}
"#;
let mut parser = Parser::new();
parser.set_language(&analyzer.language).unwrap();
let tree = parser.parse(code, None).unwrap();
println!("Root node kind: {}", tree.root_node().kind());
println!("Root node S-expression: {}", tree.root_node().to_sexp());
let simple_query = r#"(function_item name: (identifier) @name)"#;
if let Ok(query) = Query::new(&analyzer.language, simple_query) {
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&query, tree.root_node(), code.as_bytes());
while let Some(query_match) = matches.next() {
for capture in query_match.captures {
let capture_name = query.capture_names()[capture.index as usize];
let text = capture.node.utf8_text(code.as_bytes()).unwrap_or("");
println!("Capture: {} = {}", capture_name, text);
}
}
}
}
#[tokio::test]
async fn test_rust_analyzer_basic() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
assert_eq!(analyzer.language(), "rust");
assert_eq!(analyzer.file_extensions(), &["rs"]);
assert!(analyzer.supports_async());
}
#[tokio::test]
async fn test_extract_simple_function() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
fn hello_world() {
println!("Hello, world!");
}
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let functions = &analysis.tree_node.functions;
println!("Found {} functions", functions.len());
for (i, func) in functions.iter().enumerate() {
println!("Function {}: {}", i, func.name);
}
assert_eq!(functions.len(), 1);
assert_eq!(functions[0].name, "hello_world");
assert!(!functions[0].is_public);
assert!(!functions[0].is_async);
assert!(!functions[0].is_const);
assert!(!functions[0].is_extern);
assert!(!functions[0].is_static);
}
#[tokio::test]
async fn test_extract_pub_async_function() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
pub async fn fetch_data(url: &str) -> Result<String, Error> {
// fetch implementation
Ok("data".to_string())
}
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let functions = &analysis.tree_node.functions;
assert_eq!(functions.len(), 1);
assert_eq!(functions[0].name, "fetch_data");
assert!(functions[0].is_public);
assert!(functions[0].is_async);
assert!(!functions[0].is_const);
assert_eq!(
functions[0].return_type,
Some("Result<String, Error>".to_string())
);
assert_eq!(functions[0].parameters.len(), 1);
assert_eq!(functions[0].parameters[0].name, "url");
assert_eq!(functions[0].parameters[0].param_type, "&str");
}
#[tokio::test]
async fn test_extract_const_function() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
pub const fn square(x: i32) -> i32 {
x * x
}
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let functions = &analysis.tree_node.functions;
assert_eq!(functions.len(), 1);
assert_eq!(functions[0].name, "square");
assert!(functions[0].is_public);
assert!(functions[0].is_const);
assert!(!functions[0].is_async);
assert_eq!(functions[0].return_type, Some("i32".to_string()));
}
#[tokio::test]
async fn test_extract_extern_function() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
extern fn c_function(x: i32) -> i32;
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let functions = &analysis.tree_node.functions;
assert_eq!(functions.len(), 1);
assert_eq!(functions[0].name, "c_function");
assert!(functions[0].is_extern);
assert!(!functions[0].is_public);
}
#[tokio::test]
async fn test_extract_function_with_generics() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
fn generic_function<T, U>(item: T, other: U) -> T
where
T: Clone,
U: Debug,
{
item.clone()
}
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let functions = &analysis.tree_node.functions;
assert_eq!(functions.len(), 1);
assert_eq!(functions[0].name, "generic_function");
assert_eq!(functions[0].generics.len(), 2);
assert!(functions[0].generics.contains(&"T".to_string()));
assert!(functions[0].generics.contains(&"U".to_string()));
}
#[tokio::test]
async fn test_extract_impl_methods() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
struct Point {
x: f64,
y: f64,
}
impl Point {
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
pub fn distance(&self, other: &Point) -> f64 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
}
}
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let functions = &analysis.tree_node.functions;
assert_eq!(functions.len(), 2);
let new_fn = functions.iter().find(|f| f.name == "new").unwrap();
assert!(new_fn.is_public);
assert!(new_fn.is_static);
let distance_fn = functions.iter().find(|f| f.name == "distance").unwrap();
assert!(distance_fn.is_public);
assert!(!distance_fn.is_static); }
#[tokio::test]
async fn test_extract_struct() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
pub struct User {
pub id: u64,
pub name: String,
email: String,
}
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let structs = &analysis.tree_node.structs;
assert_eq!(structs.len(), 1);
assert_eq!(structs[0].name, "User");
assert!(structs[0].is_public);
assert!(!structs[0].is_tuple_struct);
assert_eq!(structs[0].fields.len(), 3);
let id_field = structs[0].fields.iter().find(|f| f.name == "id").unwrap();
assert!(id_field.is_public);
let email_field = structs[0]
.fields
.iter()
.find(|f| f.name == "email")
.unwrap();
assert!(!email_field.is_public);
}
#[tokio::test]
async fn test_extract_tuple_struct() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
pub struct Point(pub f64, f64);
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let structs = &analysis.tree_node.structs;
assert_eq!(structs.len(), 1);
assert_eq!(structs[0].name, "Point");
assert!(structs[0].is_public);
assert!(structs[0].is_tuple_struct);
assert_eq!(structs[0].fields.len(), 2);
}
#[tokio::test]
async fn test_extract_imports() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
use std::collections::HashMap;
use crate::module::LocalType;
use super::parent_module::*;
use self::child_module::ChildType;
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let imports = &analysis.tree_node.imports;
assert_eq!(imports.len(), 4);
let std_import = imports
.iter()
.find(|i| i.module_path.contains("std"))
.unwrap();
assert!(std_import.is_external);
assert!(!std_import.is_glob);
let crate_import = imports
.iter()
.find(|i| i.module_path.contains("crate"))
.unwrap();
assert!(!crate_import.is_external);
let glob_import = imports
.iter()
.find(|i| i.module_path.contains("*"))
.unwrap();
assert!(glob_import.is_glob);
}
#[tokio::test]
async fn test_extract_exports() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
pub fn public_function() {}
fn private_function() {}
pub struct PublicStruct {}
struct PrivateStruct {}
pub const PUBLIC_CONST: i32 = 42;
const PRIVATE_CONST: i32 = 24;
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let exports = &analysis.tree_node.exports;
assert_eq!(exports.len(), 3);
let exported_names: Vec<&String> = exports.iter().map(|e| &e.exported_item).collect();
assert!(exported_names.contains(&&"public_function".to_string()));
assert!(exported_names.contains(&&"PublicStruct".to_string()));
assert!(exported_names.contains(&&"PUBLIC_CONST".to_string()));
}
#[tokio::test]
async fn test_extract_function_calls() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
fn main() {
println!("Hello");
let point = Point::new(1.0, 2.0);
let distance = point.distance_to_origin();
}
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let function_calls = &analysis.tree_node.function_calls;
assert!(!function_calls.is_empty());
let method_calls: Vec<&FunctionCall> = function_calls
.iter()
.filter(|fc| fc.is_method_call)
.collect();
assert!(!method_calls.is_empty());
}
#[tokio::test]
async fn test_fallback_parsing() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let malformed_code = r#"
pub fn valid_function() {
// This should be parsed normally
}
struct InvalidSyntax {
pub field: String
// Missing comma - this might cause parsing issues
}
pub async fn another_function(param: &str) -> Result<(), Error> {
Ok(())
}
"#;
let fallback_analysis = analyzer.extract_with_fallback(malformed_code, "test.rs");
assert!(fallback_analysis.fallback_used);
assert!(!fallback_analysis.functions.is_empty());
let function_names: Vec<&String> = fallback_analysis
.functions
.iter()
.map(|f| &f.name)
.collect();
assert!(function_names.contains(&&"valid_function".to_string()));
assert!(function_names.contains(&&"another_function".to_string()));
}
#[tokio::test]
async fn test_extract_enum_kind() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
pub enum ConfigError {
Missing,
Invalid(String),
}
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let structs = &analysis.tree_node.structs;
let e = structs
.iter()
.find(|s| s.name == "ConfigError")
.expect("ConfigError not found");
assert_eq!(e.kind, TypeKind::Enum);
assert!(e.is_public);
}
#[tokio::test]
async fn test_extract_unit_struct() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
/// --context-separator
#[derive(Debug)]
struct ContextSeparator;
pub struct Marker<T>;
pub struct Named { field: u32 }
pub struct Tuple(u32);
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let structs = &analysis.tree_node.structs;
let unit = structs
.iter()
.find(|s| s.name == "ContextSeparator")
.expect("unit struct not found");
assert_eq!(unit.kind, TypeKind::Struct);
assert!(!unit.is_public);
assert!(unit.fields.is_empty());
let generic_unit = structs
.iter()
.find(|s| s.name == "Marker")
.expect("generic unit struct not found");
assert!(generic_unit.is_public);
assert_eq!(generic_unit.generics, vec!["T".to_string()]);
assert_eq!(structs.iter().filter(|s| s.name == "Named").count(), 1);
assert_eq!(structs.iter().filter(|s| s.name == "Tuple").count(), 1);
}
#[tokio::test]
async fn test_extract_type_alias_kind() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
pub type Result<T> = std::result::Result<T, AnalysisError>;
type Private = u32;
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let structs = &analysis.tree_node.structs;
let public = structs
.iter()
.find(|s| s.name == "Result")
.expect("Result alias not found");
assert_eq!(public.kind, TypeKind::TypeAlias);
assert!(public.is_public);
assert_eq!(public.generics, vec!["T".to_string()]);
let private = structs
.iter()
.find(|s| s.name == "Private")
.expect("Private alias not found");
assert_eq!(private.kind, TypeKind::TypeAlias);
assert!(!private.is_public);
}
#[tokio::test]
async fn test_extract_trait_kind_and_supertraits() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
trait Greet {
fn hi(&self);
}
trait Fancy: Display + Debug {
fn show(&self);
}
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let structs = &analysis.tree_node.structs;
let greet = structs
.iter()
.find(|s| s.name == "Greet")
.expect("Greet not found");
assert_eq!(greet.kind, TypeKind::Trait);
assert!(greet.supertypes.is_empty());
let fancy = structs
.iter()
.find(|s| s.name == "Fancy")
.expect("Fancy not found");
assert_eq!(fancy.kind, TypeKind::Trait);
assert!(fancy.supertypes.contains(&"Display".to_string()));
assert!(fancy.supertypes.contains(&"Debug".to_string()));
}
#[tokio::test]
async fn test_method_owner_and_free_function() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
struct Loader;
impl Loader {
fn load(&self) {}
fn build() -> Self { Loader }
}
fn helper() {}
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let functions = &analysis.tree_node.functions;
let load = functions.iter().find(|f| f.name == "load").unwrap();
assert_eq!(load.owner, Some("Loader".to_string()));
let build = functions.iter().find(|f| f.name == "build").unwrap();
assert_eq!(build.owner, Some("Loader".to_string()));
assert!(build.is_static);
let helper = functions.iter().find(|f| f.name == "helper").unwrap();
assert_eq!(helper.owner, None);
}
#[tokio::test]
async fn test_trait_method_owner_is_the_trait() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
pub trait Flag {
/// Has a default body.
fn name_negated(&self) -> Option<&'static str> {
None
}
/// A required method, no body.
fn is_switch(&self) -> bool;
/// An associated function (no self receiver).
fn make() -> Self;
}
struct Real;
impl Flag for Real {
fn is_switch(&self) -> bool { true }
fn make() -> Self { Real }
}
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let fns = &analysis.tree_node.functions;
let defaulted = fns
.iter()
.find(|f| f.name == "name_negated")
.expect("defaulted trait method not found");
assert_eq!(defaulted.owner, Some("Flag".to_string()));
assert!(!defaulted.is_static, "it takes &self");
let assoc = fns
.iter()
.find(|f| f.name == "make" && f.owner.as_deref() == Some("Flag"))
.expect("trait associated fn not found");
assert!(assoc.is_static);
assert!(
fns.iter()
.any(|f| f.name == "is_switch" && f.owner.as_deref() == Some("Real")),
"impl method should be owned by Real"
);
}
#[tokio::test]
async fn test_generic_impl_owner_strips_generics() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
struct Repository<T> { inner: T }
impl<T> Repository<T> {
fn load(&self) {}
}
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let load = analysis
.tree_node
.functions
.iter()
.find(|f| f.name == "load")
.unwrap();
assert_eq!(load.owner, Some("Repository".to_string()));
}
#[tokio::test]
async fn test_impl_for_adds_supertype() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
struct Config {
x: i32,
}
impl Display for Config {}
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let structs = &analysis.tree_node.structs;
let config = structs
.iter()
.find(|s| s.name == "Config")
.expect("Config not found");
assert_eq!(config.kind, TypeKind::Struct);
assert!(config.supertypes.contains(&"Display".to_string()));
}
#[tokio::test]
async fn test_impl_for_missing_type_dropped() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
impl Display for Ghost {}
"#;
let analysis = analyzer
.analyze_file(code, "test.rs")
.await
.expect("Analysis failed");
let structs = &analysis.tree_node.structs;
assert!(structs.iter().all(|s| s.name != "Ghost"));
}
#[test]
fn test_content_hash() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let content1 = "fn test() {}";
let content2 = "fn test() {}";
let content3 = "fn different() {}";
let hash1 = analyzer.calculate_content_hash(content1);
let hash2 = analyzer.calculate_content_hash(content2);
let hash3 = analyzer.calculate_content_hash(content3);
assert_eq!(hash1, hash2);
assert_ne!(hash1, hash3);
assert!(!hash1.is_empty());
}
#[tokio::test]
async fn test_comprehensive_analysis() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let comprehensive_code = r#"
use std::collections::HashMap;
use crate::types::*;
pub struct User<T> {
pub id: u64,
name: String,
data: T,
}
impl<T> User<T> {
pub fn new(id: u64, name: String, data: T) -> Self {
Self { id, name, data }
}
pub async fn save(&self) -> Result<(), Error> {
database_save(self).await
}
const fn get_id(&self) -> u64 {
self.id
}
}
pub async fn create_user<T>(name: String, data: T) -> User<T> {
let id = generate_id().await;
User::new(id, name, data)
}
extern fn generate_id() -> u64;
pub const MAX_USERS: usize = 1000;
"#;
let analysis = analyzer
.analyze_file(comprehensive_code, "user.rs")
.await
.expect("Analysis failed");
let tree_node = &analysis.tree_node;
println!("Functions found: {}", tree_node.functions.len());
for func in &tree_node.functions {
println!(" - {}", func.name);
}
assert!(!tree_node.functions.is_empty());
assert!(!tree_node.structs.is_empty());
assert!(!tree_node.imports.is_empty());
assert!(!tree_node.exports.is_empty());
assert!(analysis.analysis_duration_ms > 0);
assert!(analysis.success);
assert!(!tree_node.content_hash.is_empty());
let user_struct = tree_node.structs.iter().find(|s| s.name == "User").unwrap();
assert!(user_struct.is_public);
assert_eq!(user_struct.generics.len(), 1);
let new_function = tree_node
.functions
.iter()
.find(|f| f.name == "new")
.unwrap();
assert!(new_function.is_static);
let save_function = tree_node
.functions
.iter()
.find(|f| f.name == "save")
.unwrap();
assert!(save_function.is_async);
assert!(!save_function.is_static);
let get_id_function = tree_node
.functions
.iter()
.find(|f| f.name == "get_id")
.unwrap();
assert!(get_id_function.is_const);
let generate_id_function = tree_node
.functions
.iter()
.find(|f| f.name == "generate_id")
.unwrap();
assert!(generate_id_function.is_extern);
}
#[tokio::test]
async fn test_file_path_stored_in_functions() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
fn hello_world() {
println!("Hello, world!");
}
pub async fn async_func() -> Result<(), Error> {
Ok(())
}
"#;
let file_path = "src/main.rs";
let analysis = analyzer
.analyze_file(code, file_path)
.await
.expect("Analysis failed");
let tree_node = &analysis.tree_node;
for function in &tree_node.functions {
assert_eq!(function.file_path, file_path);
}
if let Some(hello_func) = tree_node.functions.iter().find(|f| f.name == "hello_world") {
assert_eq!(hello_func.file_path, file_path);
}
if let Some(async_func) = tree_node.functions.iter().find(|f| f.name == "async_func") {
assert_eq!(async_func.file_path, file_path);
assert!(async_func.is_async);
}
}
#[tokio::test]
async fn test_file_path_stored_in_structs() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
pub struct User {
id: u64,
name: String,
}
struct PrivateData {
secret: String,
}
"#;
let file_path = "src/models.rs";
let analysis = analyzer
.analyze_file(code, file_path)
.await
.expect("Analysis failed");
let tree_node = &analysis.tree_node;
for struct_def in &tree_node.structs {
assert_eq!(struct_def.file_path, file_path);
}
if let Some(user_struct) = tree_node.structs.iter().find(|s| s.name == "User") {
assert_eq!(user_struct.file_path, file_path);
assert!(user_struct.is_public);
}
if let Some(private_struct) = tree_node.structs.iter().find(|s| s.name == "PrivateData") {
assert_eq!(private_struct.file_path, file_path);
assert!(!private_struct.is_public);
}
}
#[tokio::test]
async fn test_file_path_stored_in_imports() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
use std::collections::HashMap;
use crate::types::User;
use super::config::Settings;
"#;
let file_path = "src/handlers.rs";
let analysis = analyzer
.analyze_file(code, file_path)
.await
.expect("Analysis failed");
let tree_node = &analysis.tree_node;
for import in &tree_node.imports {
assert_eq!(import.file_path, file_path);
}
if let Some(hashmap_import) = tree_node
.imports
.iter()
.find(|i| i.module_path.contains("HashMap"))
{
assert_eq!(hashmap_import.file_path, file_path);
}
}
#[tokio::test]
async fn test_file_path_stored_in_exports() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
pub fn public_function() {}
pub struct PublicStruct {}
pub const PUBLIC_CONST: u32 = 42;
"#;
let file_path = "src/lib.rs";
let analysis = analyzer
.analyze_file(code, file_path)
.await
.expect("Analysis failed");
let tree_node = &analysis.tree_node;
for export in &tree_node.exports {
assert_eq!(export.file_path, file_path);
}
}
#[tokio::test]
async fn test_file_path_stored_in_function_calls() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
fn main() {
println!("Hello");
some_function();
calculate(42);
}
"#;
let file_path = "src/main.rs";
let analysis = analyzer
.analyze_file(code, file_path)
.await
.expect("Analysis failed");
let tree_node = &analysis.tree_node;
for call in &tree_node.function_calls {
assert_eq!(call.file_path, file_path);
}
}
#[tokio::test]
async fn test_file_path_consistency_across_components() {
let analyzer = RustAnalyzer::new().expect("Failed to create RustAnalyzer");
let code = r#"
use std::collections::HashMap;
pub struct DataProcessor {
data: HashMap<String, String>,
}
impl DataProcessor {
pub fn new() -> Self {
Self {
data: HashMap::new(),
}
}
pub fn process(&self, input: &str) -> String {
format_data(input)
}
}
pub fn format_data(input: &str) -> String {
input.to_uppercase()
}
"#;
let file_path = "src/processor.rs";
let analysis = analyzer
.analyze_file(code, file_path)
.await
.expect("Analysis failed");
let tree_node = &analysis.tree_node;
for function in &tree_node.functions {
assert_eq!(
function.file_path, file_path,
"Function {} has incorrect file path",
function.name
);
}
for struct_def in &tree_node.structs {
assert_eq!(
struct_def.file_path, file_path,
"Struct {} has incorrect file path",
struct_def.name
);
}
for import in &tree_node.imports {
assert_eq!(
import.file_path, file_path,
"Import {} has incorrect file path",
import.module_path
);
}
for export in &tree_node.exports {
assert_eq!(
export.file_path, file_path,
"Export {} has incorrect file path",
export.exported_item
);
}
for call in &tree_node.function_calls {
assert_eq!(
call.file_path, file_path,
"Function call {} has incorrect file path",
call.function_name
);
}
}
}