use crate::core::FunctionMetrics;
use crate::priority::call_graph::{CallGraph, FunctionId};
use std::collections::HashMap;
pub fn populate_call_graph_data(
mut function_metrics: Vec<FunctionMetrics>,
call_graph: &CallGraph,
) -> Vec<FunctionMetrics> {
let metric_to_id_map: HashMap<usize, FunctionId> = function_metrics
.iter()
.enumerate()
.map(|(idx, metric)| {
let func_id = FunctionId::new(metric.file.clone(), metric.name.clone(), metric.line);
(idx, func_id)
})
.collect();
for (idx, metric) in function_metrics.iter_mut().enumerate() {
if let Some(func_id) = metric_to_id_map.get(&idx) {
let (mut upstream_callers, mut downstream_callees) =
graph_dependencies_for_function(call_graph, func_id);
if upstream_callers.is_empty() && downstream_callees.is_empty() {
let func_id_zero_line =
FunctionId::new(func_id.file.clone(), func_id.name.clone(), 0);
(upstream_callers, downstream_callees) =
graph_dependencies_for_function(call_graph, &func_id_zero_line);
}
metric.upstream_callers = if upstream_callers.is_empty() {
None
} else {
Some(upstream_callers)
};
metric.downstream_callees = if downstream_callees.is_empty() {
None
} else {
Some(downstream_callees)
};
}
}
function_metrics
}
fn graph_dependencies_for_function(
call_graph: &CallGraph,
func_id: &FunctionId,
) -> (Vec<String>, Vec<String>) {
let exact_match = call_graph.get_function_info(func_id).is_some();
let callers = if exact_match {
call_graph.get_callers_exact(func_id)
} else {
call_graph.get_callers(func_id)
};
let callees = if exact_match {
call_graph.get_callees_exact(func_id)
} else {
call_graph.get_callees(func_id)
};
(
callers
.into_iter()
.map(|caller_id| format_function_name(&caller_id))
.collect(),
callees
.into_iter()
.map(|callee_id| format_function_name(&callee_id))
.collect(),
)
}
fn format_function_name(func_id: &FunctionId) -> String {
let file_name = func_id
.file
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("unknown");
format!("{}:{}", file_name, func_id.name)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn create_test_function_metric(name: &str, file: &str, line: usize) -> FunctionMetrics {
FunctionMetrics {
name: name.to_string(),
file: PathBuf::from(file),
line,
cyclomatic: 1,
cognitive: 1,
nesting: 1,
length: 10,
is_test: false,
visibility: None,
is_trait_method: false,
in_test_module: false,
entropy_score: None,
is_pure: None,
purity_confidence: None,
purity_reason: None,
call_dependencies: None,
detected_patterns: None,
upstream_callers: None,
downstream_callees: None,
mapping_pattern_result: None,
adjusted_complexity: None,
composition_metrics: None,
language_specific: None,
purity_level: None,
error_swallowing_count: None,
error_swallowing_patterns: None,
entropy_analysis: None,
}
}
#[test]
fn test_populate_call_graph_data_empty() {
let metrics = vec![];
let call_graph = CallGraph::new();
let result = populate_call_graph_data(metrics, &call_graph);
assert!(result.is_empty());
}
#[test]
fn test_populate_call_graph_data_single_function() {
let metrics = vec![create_test_function_metric("test_func", "test.py", 10)];
let mut call_graph = CallGraph::new();
let func_id = FunctionId::new(PathBuf::from("test.py"), "test_func".to_string(), 10);
call_graph.add_function(func_id.clone(), false, false, 1, 10);
let result = populate_call_graph_data(metrics, &call_graph);
assert_eq!(result.len(), 1);
assert_eq!(result[0].upstream_callers, None);
assert_eq!(result[0].downstream_callees, None);
}
#[test]
fn test_populate_call_graph_data_with_calls() {
let metrics = vec![
create_test_function_metric("caller", "test.py", 5),
create_test_function_metric("callee", "test.py", 15),
];
let mut call_graph = CallGraph::new();
let caller_id = FunctionId::new(PathBuf::from("test.py"), "caller".to_string(), 5);
let callee_id = FunctionId::new(PathBuf::from("test.py"), "callee".to_string(), 15);
call_graph.add_function(caller_id.clone(), false, false, 1, 10);
call_graph.add_function(callee_id.clone(), false, false, 1, 10);
call_graph.add_call(crate::priority::call_graph::FunctionCall {
caller: caller_id.clone(),
callee: callee_id.clone(),
call_type: crate::priority::call_graph::CallType::Direct,
});
let result = populate_call_graph_data(metrics, &call_graph);
assert_eq!(result.len(), 2);
let caller_metric = &result[0];
assert_eq!(caller_metric.name, "caller");
assert_eq!(caller_metric.upstream_callers, None);
assert_eq!(
caller_metric.downstream_callees,
Some(vec!["test.py:callee".to_string()])
);
let callee_metric = &result[1];
assert_eq!(callee_metric.name, "callee");
assert_eq!(
callee_metric.upstream_callers,
Some(vec!["test.py:caller".to_string()])
);
assert_eq!(callee_metric.downstream_callees, None);
}
#[test]
fn test_format_function_name() {
let func_id = FunctionId::new(
PathBuf::from("/path/to/test.py"),
"my_function".to_string(),
10,
);
let formatted = format_function_name(&func_id);
assert_eq!(formatted, "test.py:my_function");
}
#[test]
fn test_populate_call_graph_data_cross_file() {
use crate::analyzers::rust_call_graph::extract_call_graph_multi_file;
let metrics = vec![
create_test_function_metric(
"diagnose_coverage_file",
"src/commands/diagnose_coverage.rs",
6,
),
create_test_function_metric(
"generate_suggestions",
"src/commands/diagnose_coverage.rs",
12,
),
create_test_function_metric("parse_lcov_file", "src/risk/lcov.rs", 8),
];
let file1_code = r#"
use crate::risk::lcov::parse_lcov_file;
use anyhow::Result;
use std::path::Path;
pub fn diagnose_coverage_file(lcov_path: &Path, format: &str) -> Result<()> {
let lcov_data = parse_lcov_file(lcov_path)?;
let total_files = lcov_data.functions.len();
let suggestions = generate_suggestions(total_files);
Ok(())
}
fn generate_suggestions(total_files: usize) -> Vec<String> {
vec![]
}
"#;
let file2_code = r#"
use anyhow::Result;
use std::path::Path;
pub struct LcovData {
pub functions: std::collections::HashMap<String, Vec<()>>,
}
pub fn parse_lcov_file(path: &Path) -> Result<LcovData> {
Ok(LcovData {
functions: std::collections::HashMap::new(),
})
}
"#;
let file1 = syn::parse_str::<syn::File>(file1_code).expect("Failed to parse file1");
let file2 = syn::parse_str::<syn::File>(file2_code).expect("Failed to parse file2");
let files = vec![
(file1, PathBuf::from("src/commands/diagnose_coverage.rs")),
(file2, PathBuf::from("src/risk/lcov.rs")),
];
let call_graph = extract_call_graph_multi_file(&files);
eprintln!("Call graph functions:");
for func in call_graph.get_all_functions() {
let callers_vec = call_graph.get_callers(func);
let callers: Vec<_> = callers_vec.iter().map(|f| &f.name).collect();
let callees_vec = call_graph.get_callees(func);
let callees: Vec<_> = callees_vec.iter().map(|f| &f.name).collect();
eprintln!(
" {}:{} (line {}) - callers: {:?}, callees: {:?}",
func.file.display(),
func.name,
func.line,
callers,
callees
);
}
let result = populate_call_graph_data(metrics, &call_graph);
eprintln!("\nPopulated metrics:");
for metric in &result {
eprintln!(
" {}:{} (line {}) - upstream: {:?}, downstream: {:?}",
metric.file.display(),
metric.name,
metric.line,
metric.upstream_callers,
metric.downstream_callees
);
}
let diagnose_metric = result
.iter()
.find(|m| m.name == "diagnose_coverage_file")
.expect("diagnose_coverage_file should exist");
assert!(
diagnose_metric.downstream_callees.is_some(),
"diagnose_coverage_file should have downstream_callees. Got: {:?}",
diagnose_metric.downstream_callees
);
let callees = diagnose_metric.downstream_callees.as_ref().unwrap();
eprintln!("diagnose_coverage_file callees: {:?}", callees);
assert!(
callees.iter().any(|c| c.contains("parse_lcov_file")),
"diagnose_coverage_file should call parse_lcov_file. Found: {:?}",
callees
);
assert!(
callees.iter().any(|c| c.contains("generate_suggestions")),
"diagnose_coverage_file should call generate_suggestions. Found: {:?}",
callees
);
let parse_metric = result
.iter()
.find(|m| m.name == "parse_lcov_file")
.expect("parse_lcov_file should exist");
assert!(
parse_metric.upstream_callers.is_some(),
"parse_lcov_file should have upstream_callers. Got: {:?}",
parse_metric.upstream_callers
);
let callers = parse_metric.upstream_callers.as_ref().unwrap();
assert!(
callers.iter().any(|c| c.contains("diagnose_coverage_file")),
"parse_lcov_file should be called by diagnose_coverage_file. Found: {:?}",
callers
);
}
#[test]
fn test_populate_call_graph_data_with_mismatched_lines() {
use crate::analyzers::rust_call_graph::extract_call_graph_multi_file;
let metrics = vec![
create_test_function_metric(
"diagnose_coverage_file",
"src/commands/diagnose_coverage.rs",
63, ),
create_test_function_metric(
"generate_suggestions",
"src/commands/diagnose_coverage.rs",
202, ),
create_test_function_metric("parse_lcov_file", "src/risk/lcov.rs", 268),
];
let file1_code = r#"
use crate::risk::lcov::parse_lcov_file;
use anyhow::Result;
use std::path::Path;
pub fn diagnose_coverage_file(lcov_path: &Path, format: &str) -> Result<()> {
let lcov_data = parse_lcov_file(lcov_path)?;
let total_files = lcov_data.functions.len();
let suggestions = generate_suggestions(total_files);
Ok(())
}
fn generate_suggestions(total_files: usize) -> Vec<String> {
vec![]
}
"#;
let file2_code = r#"
use anyhow::Result;
use std::path::Path;
pub struct LcovData {
pub functions: std::collections::HashMap<String, Vec<()>>,
}
pub fn parse_lcov_file(path: &Path) -> Result<LcovData> {
Ok(LcovData {
functions: std::collections::HashMap::new(),
})
}
"#;
let file1 = syn::parse_str::<syn::File>(file1_code).expect("Failed to parse file1");
let file2 = syn::parse_str::<syn::File>(file2_code).expect("Failed to parse file2");
let files = vec![
(file1, PathBuf::from("src/commands/diagnose_coverage.rs")),
(file2, PathBuf::from("src/risk/lcov.rs")),
];
let call_graph = extract_call_graph_multi_file(&files);
eprintln!("\nCall graph functions (with their extracted line numbers):");
for func in call_graph.get_all_functions() {
eprintln!(
" {}:{} (line {})",
func.file.display(),
func.name,
func.line
);
}
let result = populate_call_graph_data(metrics, &call_graph);
eprintln!("\nPopulated metrics (with their DIFFERENT line numbers):");
for metric in &result {
eprintln!(
" {}:{} (line {}) - upstream: {:?}, downstream: {:?}",
metric.file.display(),
metric.name,
metric.line,
metric.upstream_callers,
metric.downstream_callees
);
}
let diagnose_metric = result
.iter()
.find(|m| m.name == "diagnose_coverage_file")
.expect("diagnose_coverage_file should exist");
assert!(
diagnose_metric.downstream_callees.is_some(),
"BUG: diagnose_coverage_file should have downstream_callees even with mismatched line numbers. Got: {:?}",
diagnose_metric.downstream_callees
);
let callees = diagnose_metric.downstream_callees.as_ref().unwrap();
assert!(
callees.iter().any(|c| c.contains("parse_lcov_file")),
"BUG: diagnose_coverage_file should call parse_lcov_file. Found: {:?}",
callees
);
}
#[test]
fn test_populate_call_graph_data_exact_and_fuzzy_paths_match() {
let exact_metrics = vec![
create_test_function_metric("caller", "src/exact.rs", 5),
create_test_function_metric("callee", "src/exact.rs", 15),
];
let fuzzy_metrics = vec![
create_test_function_metric("caller", "src/exact.rs", 50),
create_test_function_metric("callee", "src/exact.rs", 150),
];
let mut call_graph = CallGraph::new();
let caller_id = FunctionId::new(PathBuf::from("src/exact.rs"), "caller".to_string(), 5);
let callee_id = FunctionId::new(PathBuf::from("src/exact.rs"), "callee".to_string(), 15);
call_graph.add_function(caller_id.clone(), false, false, 1, 10);
call_graph.add_function(callee_id.clone(), false, false, 1, 10);
call_graph.add_call(crate::priority::call_graph::FunctionCall {
caller: caller_id,
callee: callee_id,
call_type: crate::priority::call_graph::CallType::Direct,
});
let exact_result = populate_call_graph_data(exact_metrics, &call_graph);
let fuzzy_result = populate_call_graph_data(fuzzy_metrics, &call_graph);
assert_eq!(
exact_result[0].downstream_callees,
fuzzy_result[0].downstream_callees
);
assert_eq!(
exact_result[1].upstream_callers,
fuzzy_result[1].upstream_callers
);
}
}