use crate::graph::{DependencyGraph, DependencyType};
use crate::imports::ModuleIdentifier;
use crate::tools::common;
use anyhow::Result;
#[derive(Debug)]
pub struct ImpactAnalysisResult {
pub target_module: String,
pub affected_modules: Vec<(String, DependencyType, usize)>,
pub total_affected_count: usize,
}
pub fn get_impact_analysis(
graph: &DependencyGraph,
module_id: &ModuleIdentifier,
) -> Result<(Vec<(String, DependencyType, usize)>, usize)> {
let mut affected_modules = graph.get_transitive_dependents_with_types(module_id)?;
affected_modules.retain(|(module_path, _)| {
!module_path.contains(".tests.") && !module_path.ends_with(".tests")
});
let additional_parents =
find_parent_modules_with_all_children_affected(graph, &affected_modules)?;
affected_modules.extend(additional_parents);
let total_count = affected_modules.len();
let deduplicated = common::filter_hierarchical(affected_modules);
Ok((deduplicated, total_count))
}
fn find_parent_modules_with_all_children_affected(
graph: &DependencyGraph,
affected_modules: &[(String, DependencyType)],
) -> Result<Vec<(String, DependencyType)>> {
use std::collections::{HashMap, HashSet};
let affected_set: HashSet<&String> = affected_modules.iter().map(|(name, _)| name).collect();
let mut parent_to_children: HashMap<String, Vec<String>> = HashMap::new();
for parent_module in graph.all_modules() {
let parent_path = &parent_module.canonical_path;
let children = graph
.get_dependencies_with_types(parent_module)
.unwrap_or_else(|_| Vec::new())
.into_iter()
.filter_map(|(child_path, dep_type)| {
if dep_type == DependencyType::Contains {
Some(child_path)
} else {
None
}
})
.collect::<Vec<_>>();
if !children.is_empty() {
parent_to_children.insert(parent_path.clone(), children);
}
}
let mut additional_parents = Vec::new();
for (parent_path, children) in parent_to_children {
if affected_set.contains(&parent_path) {
continue;
}
if !children.is_empty() && children.iter().all(|child| affected_set.contains(child)) {
additional_parents.push((parent_path, DependencyType::Imports));
}
}
Ok(additional_parents)
}
pub fn analyze_impact(graph: &DependencyGraph, module_name: &str) -> Result<ImpactAnalysisResult> {
let target_module = graph
.all_modules()
.find(|m| m.canonical_path == module_name)
.ok_or_else(|| anyhow::anyhow!("Module '{}' not found in dependency graph", module_name))?;
let (affected_modules, total_count) = get_impact_analysis(graph, target_module)?;
Ok(ImpactAnalysisResult {
target_module: target_module.canonical_path.clone(),
affected_modules,
total_affected_count: total_count,
})
}
pub mod formatters {
use super::ImpactAnalysisResult;
use crate::tools::common::formatters as common_formatters;
const NO_DEPENDENCIES_MSG: &str = "(no dependencies found)";
fn format_with_body(result: &ImpactAnalysisResult, body: String) -> String {
format!(
"Modules depending on '{}':\n{}Total: {} modules impacted by {}\n",
result.target_module, body, result.total_affected_count, result.target_module
)
}
pub fn format_text(result: &ImpactAnalysisResult) -> String {
let body = if result.affected_modules.is_empty() {
format!("{}\n", NO_DEPENDENCIES_MSG)
} else {
let mut output = String::new();
for (module, _dep_type, count) in &result.affected_modules {
if *count > 1 {
output.push_str(&format!("({} submodules) {}\n", count, module));
} else {
output.push_str(&format!("{}\n", module));
}
}
output
};
format_with_body(result, body)
}
pub fn format_text_grouped(result: &ImpactAnalysisResult) -> String {
let body = if result.affected_modules.is_empty() {
format!("{}\n", NO_DEPENDENCIES_MSG)
} else {
common_formatters::format_grouped_modules(&result.affected_modules)
};
format_with_body(result, body)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::DependencyGraph;
use crate::imports::{ModuleIdentifier, ModuleOrigin};
fn create_test_module_id(name: &str, origin: ModuleOrigin) -> ModuleIdentifier {
ModuleIdentifier {
origin,
canonical_path: name.to_string(),
}
}
#[test]
fn test_impact_analyzer_basic() {
let mut graph = DependencyGraph::new();
let main = create_test_module_id("main", ModuleOrigin::Internal);
let utils = create_test_module_id("utils", ModuleOrigin::Internal);
let tests = create_test_module_id("tests.test_utils", ModuleOrigin::Internal);
graph.add_module(main.clone());
graph.add_module(utils.clone());
graph.add_module(tests.clone());
graph
.add_dependency(&main, &utils, DependencyType::Imports)
.unwrap();
graph
.add_dependency(&tests, &utils, DependencyType::Imports)
.unwrap();
let result = analyze_impact(&graph, "utils").unwrap();
assert_eq!(result.target_module, "utils");
assert_eq!(result.affected_modules.len(), 3);
assert_eq!(result.total_affected_count, 3);
let affected_names: Vec<&String> = result
.affected_modules
.iter()
.map(|(name, _, _)| name)
.collect();
assert!(affected_names.contains(&&"utils".to_string()));
assert!(affected_names.contains(&&"main".to_string()));
assert!(affected_names.contains(&&"tests.test_utils".to_string()));
}
#[test]
fn test_format_text() {
let result = ImpactAnalysisResult {
target_module: "utils".to_string(),
affected_modules: vec![
("main".to_string(), DependencyType::Imports, 1),
("api".to_string(), DependencyType::Imports, 3),
],
total_affected_count: 4,
};
let formatted = formatters::format_text(&result);
assert!(formatted.contains("Modules depending on 'utils':"));
assert!(formatted.contains("main"));
assert!(formatted.contains("(3 submodules) api"));
assert!(formatted.contains("Total: 4 modules impacted by utils"));
}
}