use crate::edit::ResolvedEditChange;
use crate::graph::pdg::{NodeId, NodeType, TraversalConfig};
use crate::graph::ProgramDependenceGraph;
use crate::validation::ValidationError;
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Location {
pub line: usize,
pub column: usize,
}
impl std::fmt::Display for Location {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.line, self.column)
}
}
impl Location {
pub fn new(line: usize, column: usize) -> Self {
Self { line, column }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum RiskLevel {
Low = 0,
Medium = 1,
High = 2,
Critical = 3,
}
impl RiskLevel {
pub fn description(&self) -> &str {
match self {
Self::Low => "Local changes only",
Self::Medium => "Affects same module",
Self::High => "Affects multiple modules",
Self::Critical => "Affects public API",
}
}
pub fn color_code(&self) -> &str {
match self {
Self::Low => "\x1b[32m", Self::Medium => "\x1b[33m", Self::High => "\x1b[31m", Self::Critical => "\x1b[35m", }
}
}
impl std::fmt::Display for RiskLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.description())
}
}
#[derive(Debug, Clone)]
pub struct ImpactReport {
pub risk_level: RiskLevel,
pub affected_nodes: usize,
pub affected_files: Vec<PathBuf>,
pub affected_apis: Vec<String>,
pub description: String,
}
impl ImpactReport {
pub fn new(
risk_level: RiskLevel,
affected_nodes: usize,
affected_files: Vec<PathBuf>,
affected_apis: Vec<String>,
) -> Self {
let description = format!(
"Risk: {}, Affected nodes: {}, Files: {}, APIs: {}",
risk_level,
affected_nodes,
affected_files.len(),
affected_apis.len()
);
Self {
risk_level,
affected_nodes,
affected_files,
affected_apis,
description,
}
}
pub fn minimal() -> Self {
Self::new(RiskLevel::Low, 0, vec![], vec![])
}
}
#[derive(Clone)]
pub struct ImpactAnalyzer {
pdg: Arc<ProgramDependenceGraph>,
}
impl ImpactAnalyzer {
pub fn new(pdg: Arc<ProgramDependenceGraph>) -> Self {
Self { pdg }
}
pub fn analyze_impact(
&self,
changes: &[ResolvedEditChange],
) -> Result<ImpactReport, ValidationError> {
if changes.is_empty() {
return Ok(ImpactReport::minimal());
}
let mut affected_files = HashSet::new();
let mut affected_apis = HashSet::new();
let mut total_affected_nodes = 0;
for change in changes {
let file_path = change.file_path.to_string_lossy().to_string();
let nodes_in_file = self.pdg.nodes_in_file(&file_path);
let mut affected = HashSet::new();
for node_id in &nodes_in_file {
let forward = self
.pdg
.forward_impact(*node_id, &TraversalConfig::for_impact_analysis());
affected.extend(forward);
let backward = self
.pdg
.backward_impact(*node_id, &TraversalConfig::for_impact_analysis());
affected.extend(backward);
affected.insert(*node_id);
}
total_affected_nodes += affected.len();
for node_id in &affected {
if let Some(node) = self.pdg.get_node(*node_id) {
affected_files.insert(PathBuf::from(&*node.file_path));
if matches!(
node.node_type,
NodeType::Function | NodeType::Method | NodeType::Class
) {
affected_apis.insert(node.name.clone());
}
}
}
affected_files.insert(change.file_path.clone());
}
let risk_level =
self.calculate_risk_level(total_affected_nodes, &affected_files, &affected_apis);
Ok(ImpactReport::new(
risk_level,
total_affected_nodes,
affected_files.into_iter().collect(),
affected_apis.into_iter().collect(),
))
}
fn calculate_risk_level(
&self,
affected_nodes: usize,
affected_files: &HashSet<PathBuf>,
affected_apis: &HashSet<String>,
) -> RiskLevel {
if !affected_apis.is_empty() {
for api in affected_apis {
if self.is_public_api(api) {
return RiskLevel::Critical;
}
}
return RiskLevel::High;
}
if affected_files.len() > 3 {
return RiskLevel::High;
}
if affected_files.len() > 1 {
return RiskLevel::Medium;
}
if affected_nodes > 10 {
return RiskLevel::Medium;
}
RiskLevel::Low
}
fn is_public_api(&self, api_name: &str) -> bool {
if let Some(node_id) = self.pdg.find_by_symbol(api_name) {
if let Some(node) = self.pdg.get_node(node_id) {
if !node.file_path.contains("test") && !node.file_path.contains("spec") {
return !node.file_path.contains("internal")
&& !node.file_path.contains("private");
}
}
}
false
}
pub fn get_forward_impact(&self, node_id: NodeId) -> Vec<NodeId> {
self.pdg
.forward_impact(node_id, &TraversalConfig::for_impact_analysis())
}
pub fn get_backward_impact(&self, node_id: NodeId) -> Vec<NodeId> {
self.pdg
.backward_impact(node_id, &TraversalConfig::for_impact_analysis())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::pdg::{EdgeMetadata, EdgeType};
use crate::graph::Node;
#[test]
fn test_location_new() {
let loc = Location::new(10, 5);
assert_eq!(loc.line, 10);
assert_eq!(loc.column, 5);
}
#[test]
fn test_location_display() {
let loc = Location::new(10, 5);
assert_eq!(loc.to_string(), "10:5");
}
#[test]
fn test_risk_level_ordering() {
assert!(RiskLevel::Low < RiskLevel::Medium);
assert!(RiskLevel::Medium < RiskLevel::High);
assert!(RiskLevel::High < RiskLevel::Critical);
}
#[test]
fn test_risk_level_description() {
assert_eq!(RiskLevel::Low.description(), "Local changes only");
assert_eq!(RiskLevel::Medium.description(), "Affects same module");
assert_eq!(RiskLevel::High.description(), "Affects multiple modules");
assert_eq!(RiskLevel::Critical.description(), "Affects public API");
}
#[test]
fn test_risk_level_display() {
assert_eq!(RiskLevel::Low.to_string(), "Local changes only");
assert_eq!(RiskLevel::Critical.to_string(), "Affects public API");
}
#[test]
fn test_risk_level_equality() {
assert_eq!(RiskLevel::Low, RiskLevel::Low);
assert_ne!(RiskLevel::Low, RiskLevel::High);
}
#[test]
fn test_impact_report_new() {
let report = ImpactReport::new(
RiskLevel::Low,
5,
vec![PathBuf::from("test.py")],
vec!["my_func".to_string()],
);
assert_eq!(report.risk_level, RiskLevel::Low);
assert_eq!(report.affected_nodes, 5);
assert_eq!(report.affected_files.len(), 1);
assert_eq!(report.affected_apis.len(), 1);
}
#[test]
fn test_impact_report_minimal() {
let report = ImpactReport::minimal();
assert_eq!(report.risk_level, RiskLevel::Low);
assert_eq!(report.affected_nodes, 0);
assert!(report.affected_files.is_empty());
assert!(report.affected_apis.is_empty());
}
#[test]
fn test_impact_analyzer_new() {
let pdg = Arc::new(ProgramDependenceGraph::new());
let _analyzer = ImpactAnalyzer::new(pdg);
assert!(true);
}
#[test]
fn test_analyze_impact_empty_changes() {
let pdg = Arc::new(ProgramDependenceGraph::new());
let analyzer = ImpactAnalyzer::new(pdg);
let changes: &[ResolvedEditChange] = &[];
let report = analyzer.analyze_impact(changes).unwrap();
assert_eq!(report.risk_level, RiskLevel::Low);
assert_eq!(report.affected_nodes, 0);
}
#[test]
fn test_analyze_impact_single_change() {
let mut pdg = ProgramDependenceGraph::new();
let node = Node {
id: "my_func".to_string(),
node_type: NodeType::Function,
name: "my_func".to_string(),
file_path: Arc::from("test.py"),
byte_range: (0, 100),
complexity: 1,
language: "python".to_string(),
};
pdg.add_node(node);
let analyzer = ImpactAnalyzer::new(Arc::new(pdg));
let change = ResolvedEditChange::new(
PathBuf::from("test.py"),
"old content".to_string(),
"new content".to_string(),
);
let report = analyzer.analyze_impact(&[change]).unwrap();
assert!(!report.affected_files.is_empty());
}
#[test]
fn test_analyze_impact_with_dependencies() {
let mut pdg = ProgramDependenceGraph::new();
let node_a = pdg.add_node(Node {
id: "func_a".to_string(),
node_type: NodeType::Function,
name: "func_a".to_string(),
file_path: Arc::from("a.py"),
byte_range: (0, 100),
complexity: 1,
language: "python".to_string(),
});
let node_b = pdg.add_node(Node {
id: "func_b".to_string(),
node_type: NodeType::Function,
name: "func_b".to_string(),
file_path: Arc::from("b.py"),
byte_range: (0, 100),
complexity: 1,
language: "python".to_string(),
});
let node_c = pdg.add_node(Node {
id: "func_c".to_string(),
node_type: NodeType::Function,
name: "func_c".to_string(),
file_path: Arc::from("c.py"),
byte_range: (0, 100),
complexity: 1,
language: "python".to_string(),
});
pdg.add_edge(
node_a,
node_b,
crate::graph::Edge {
edge_type: EdgeType::Call,
metadata: EdgeMetadata {
call_count: None,
confidence: None,
variable_name: None,
},
},
);
pdg.add_edge(
node_b,
node_c,
crate::graph::Edge {
edge_type: EdgeType::Call,
metadata: EdgeMetadata {
call_count: None,
confidence: None,
variable_name: None,
},
},
);
let analyzer = ImpactAnalyzer::new(Arc::new(pdg));
let change =
ResolvedEditChange::new(PathBuf::from("c.py"), "old".to_string(), "new".to_string());
let report = analyzer.analyze_impact(&[change]).unwrap();
assert!(report.affected_nodes >= 1);
}
#[test]
fn test_is_public_api() {
let mut pdg = ProgramDependenceGraph::new();
let node = Node {
id: "public_func".to_string(),
node_type: NodeType::Function,
name: "public_func".to_string(),
file_path: Arc::from("src/lib.rs"),
byte_range: (0, 100),
complexity: 1,
language: "rust".to_string(),
};
pdg.add_node(node);
let analyzer = ImpactAnalyzer::new(Arc::new(pdg));
assert!(analyzer.is_public_api("public_func"));
}
#[test]
fn test_is_public_api_test_file() {
let mut pdg = ProgramDependenceGraph::new();
let node = Node {
id: "test_func".to_string(),
node_type: NodeType::Function,
name: "test_func".to_string(),
file_path: Arc::from("tests/test_lib.rs"),
byte_range: (0, 100),
complexity: 1,
language: "rust".to_string(),
};
pdg.add_node(node);
let analyzer = ImpactAnalyzer::new(Arc::new(pdg));
assert!(!analyzer.is_public_api("test_func"));
}
#[test]
fn test_is_public_api_internal_module() {
let mut pdg = ProgramDependenceGraph::new();
let node = Node {
id: "internal_func".to_string(),
node_type: NodeType::Function,
name: "internal_func".to_string(),
file_path: Arc::from("src/internal/mod.rs"),
byte_range: (0, 100),
complexity: 1,
language: "rust".to_string(),
};
pdg.add_node(node);
let analyzer = ImpactAnalyzer::new(Arc::new(pdg));
assert!(!analyzer.is_public_api("internal_func"));
}
}