use std::sync::Arc;
use crate::db::graph::{GraphDb, QueryExt};
pub async fn run_find_callers(
entity_name: &str,
repo_name: Option<&str>,
graph_db: &Arc<GraphDb>,
) -> anyhow::Result<serde_json::Value> {
let references = graph_db.find_references(entity_name, repo_name).await?;
Ok(references)
}
pub fn format_references_result(entity_name: &str, references: &serde_json::Value) -> String {
use std::collections::HashMap;
let mut output = format!("# References to `{}`\n\n", entity_name);
let mut total_refs = 0;
let rel_types = vec![
("calls", "Calls (function/method invocations)"),
("extends", "Extends (class inheritance)"),
("implements", "Implements (interface implementation)"),
("references", "References (type annotations/usages)"),
];
for (key, _) in &rel_types {
if let Some(arr) = references.get(key).and_then(|v| v.as_array()) {
total_refs += arr.len();
}
}
if total_refs == 0 {
output.push_str(&format!(
"No references found for `{}`. This entity may be unused.\n",
entity_name
));
return output;
}
output.push_str(&format!(
"Found {} reference(s) across all relationship types:\n\n",
total_refs
));
for (key, label) in rel_types {
if let Some(arr) = references.get(key).and_then(|v| v.as_array())
&& !arr.is_empty()
{
output.push_str(&format!("## {} ({})\n\n", label, arr.len()));
let mut grouped: HashMap<String, Vec<&serde_json::Value>> = HashMap::new();
for entity in arr {
let target_key = if let (Some(target_file), Some(target_line)) = (
entity.get("target_file_path").and_then(|v| v.as_str()),
entity.get("target_start_line").and_then(|v| v.as_i64()),
) {
format!("{}:{}", target_file, target_line)
} else if let Some(target_file) =
entity.get("target_file_path").and_then(|v| v.as_str())
{
target_file.to_string()
} else {
"unknown".to_string()
};
grouped.entry(target_key).or_default().push(entity);
}
if grouped.len() == 1 {
for entity in arr {
output.push_str(&format_reference_entry(entity));
}
} else {
for (target_key, entities) in grouped {
let first_entity = entities[0];
let target_name = first_entity
.get("target_name")
.and_then(|v| v.as_str())
.unwrap_or(entity_name);
output.push_str(&format!(
"### Target: `{}` at `{}`\n\n",
target_name, target_key
));
if let Some(target_sig) = first_entity
.get("target_signature")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
{
output.push_str(&format!("Signature: `{}`\n\n", target_sig));
}
for entity in entities {
output.push_str(&format_reference_entry(entity));
}
}
}
}
}
output
}
pub fn format_reference_entry(entity: &serde_json::Value) -> String {
let mut output = String::new();
if let Some(name) = entity.get("name").and_then(|v| v.as_str()) {
if let Some(kind) = entity.get("kind").and_then(|v| v.as_str()) {
output.push_str(&format!("- **`{}`** ({})", name, kind));
} else {
output.push_str(&format!("- **`{}`**", name));
}
}
if let Some(file_path) = entity.get("file_path").and_then(|v| v.as_str()) {
if let Some(start_line) = entity.get("start_line").and_then(|v| v.as_i64()) {
output.push_str(&format!(" at `{}:{}`", file_path, start_line));
} else {
output.push_str(&format!(" at `{}`", file_path));
}
}
output.push('\n');
if let Some(signature) = entity
.get("signature")
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
{
output.push_str(&format!(" - Signature: `{}`\n", signature));
}
output.push('\n');
output
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_format_references_result_empty() {
let references = json!({
"calls": [],
"extends": [],
"implements": [],
"references": []
});
let formatted = format_references_result("MyEntity", &references);
assert!(formatted.contains("No references found"));
}
#[test]
fn test_format_references_result_with_data() {
let references = json!({
"calls": [
{
"name": "caller1",
"kind": "method",
"file_path": "file1.java",
"start_line": 10,
"signature": "void caller1()"
}
],
"extends": [],
"implements": [],
"references": []
});
let formatted = format_references_result("MyEntity", &references);
assert!(formatted.contains("caller1"));
assert!(formatted.contains("file1.java:10"));
}
#[test]
fn test_format_references_result_with_multiple_relationship_types() {
let references = json!({
"calls": [{"name": "caller1", "kind": "method", "file_path": "file1.java", "start_line": 10}],
"extends": [{"name": "ChildClass", "kind": "class", "file_path": "file2.java", "start_line": 20}],
"implements": [{"name": "ImplClass", "kind": "class", "file_path": "file3.java", "start_line": 30}],
"references": [{"name": "refUser", "kind": "method", "file_path": "file4.java", "start_line": 40}]
});
let formatted = format_references_result("MyEntity", &references);
assert!(formatted.contains("Found 4 reference(s)"));
assert!(formatted.contains("Calls (function/method invocations)"));
assert!(formatted.contains("Extends (class inheritance)"));
assert!(formatted.contains("Implements (interface implementation)"));
assert!(formatted.contains("References (type annotations/usages)"));
}
#[test]
fn test_format_reference_entry_complete() {
let entity = json!({
"name": "myMethod",
"kind": "method",
"file_path": "src/Handler.java",
"start_line": 42,
"signature": "public void myMethod() throws Exception"
});
let formatted = format_reference_entry(&entity);
assert!(formatted.contains("myMethod"));
assert!(formatted.contains("method"));
assert!(formatted.contains("src/Handler.java:42"));
assert!(formatted.contains("public void myMethod() throws Exception"));
}
#[test]
fn test_format_reference_entry_without_line_number() {
let entity = json!({
"name": "myMethod",
"kind": "method",
"file_path": "src/Handler.java"
});
let formatted = format_reference_entry(&entity);
assert!(formatted.contains("myMethod"));
assert!(formatted.contains("src/Handler.java"));
assert!(!formatted.contains(":"));
}
#[test]
fn test_format_reference_entry_without_kind() {
let entity = json!({
"name": "UnknownEntity",
"file_path": "src/Unknown.java",
"start_line": 50
});
let formatted = format_reference_entry(&entity);
assert!(formatted.contains("UnknownEntity"));
assert!(formatted.contains("src/Unknown.java:50"));
}
#[test]
fn test_format_references_result_only_extends() {
let references = json!({
"calls": [],
"extends": [
{"name": "ChildClass1", "kind": "class", "file_path": "file1.java", "start_line": 10},
{"name": "ChildClass2", "kind": "class", "file_path": "file2.java", "start_line": 20}
],
"implements": [],
"references": []
});
let formatted = format_references_result("BaseClass", &references);
assert!(formatted.contains("Found 2 reference(s)"));
assert!(formatted.contains("Extends (class inheritance) (2)"));
assert!(!formatted.contains("Calls (function/method invocations)"));
}
#[test]
fn test_format_references_result_dead_code() {
let references = json!({
"calls": [],
"extends": [],
"implements": [],
"references": []
});
let formatted = format_references_result("UnusedMethod", &references);
assert!(formatted.contains("No references found"));
assert!(formatted.contains("This entity may be unused"));
}
#[test]
fn test_format_references_result_multiple_targets_same_name() {
let references = json!({
"calls": [
{
"name": "caller1",
"kind": "function",
"file_path": "src/parser/orphans.rs",
"start_line": 8,
"target_name": "find_nearest_entity_by_line",
"target_file_path": "src/parser/orphans.rs",
"target_start_line": 92,
"target_signature": "pub(crate) fn find_nearest_entity_by_line(entities: &[ParsedEntity], target_line: usize) -> usize"
},
{
"name": "caller2",
"kind": "function",
"file_path": "src/parser/languages/rust.rs",
"start_line": 258,
"target_name": "find_nearest_entity_by_line",
"target_file_path": "src/parser/languages/rust.rs",
"target_start_line": 445,
"target_signature": "fn find_nearest_entity_by_line(entities: &[ParsedEntity], line: usize) -> usize"
},
{
"name": "caller3",
"kind": "function",
"file_path": "src/parser/orphans.rs",
"start_line": 175,
"target_name": "find_nearest_entity_by_line",
"target_file_path": "src/parser/orphans.rs",
"target_start_line": 92,
"target_signature": "pub(crate) fn find_nearest_entity_by_line(entities: &[ParsedEntity], target_line: usize) -> usize"
}
],
"extends": [],
"implements": [],
"references": []
});
let formatted = format_references_result("find_nearest_entity_by_line", &references);
assert!(formatted.contains("Found 3 reference(s)"));
assert!(formatted.contains("### Target:"));
assert!(formatted.contains("src/parser/orphans.rs:92"));
assert!(formatted.contains("src/parser/languages/rust.rs:445"));
assert!(formatted.contains("pub(crate) fn find_nearest_entity_by_line"));
assert!(formatted.contains("fn find_nearest_entity_by_line"));
assert!(formatted.contains("caller1"));
assert!(formatted.contains("caller2"));
assert!(formatted.contains("caller3"));
}
#[test]
fn test_format_references_result_single_target_no_grouping() {
let references = json!({
"calls": [
{
"name": "caller1",
"kind": "method",
"file_path": "file1.java",
"start_line": 10,
"target_name": "myMethod",
"target_file_path": "src/Handler.java",
"target_start_line": 42,
"target_signature": "public void myMethod()"
},
{
"name": "caller2",
"kind": "method",
"file_path": "file2.java",
"start_line": 20,
"target_name": "myMethod",
"target_file_path": "src/Handler.java",
"target_start_line": 42,
"target_signature": "public void myMethod()"
}
],
"extends": [],
"implements": [],
"references": []
});
let formatted = format_references_result("myMethod", &references);
assert!(formatted.contains("Found 2 reference(s)"));
assert!(!formatted.contains("### Target:"));
assert!(formatted.contains("caller1"));
assert!(formatted.contains("caller2"));
}
}