use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CfgNode {
Entry,
Exit,
BasicBlock {
id: usize,
label: String,
start_line: usize,
end_line: usize,
},
Conditional {
id: usize,
label: String,
line: usize,
},
LoopHeader {
id: usize,
label: String,
line: usize,
},
FunctionEntry {
id: usize,
name: String,
line: usize,
},
SubshellEntry { id: usize, line: usize },
}
impl CfgNode {
pub fn id(&self) -> usize {
match self {
CfgNode::Entry => 0,
CfgNode::Exit => usize::MAX,
CfgNode::BasicBlock { id, .. }
| CfgNode::Conditional { id, .. }
| CfgNode::LoopHeader { id, .. }
| CfgNode::FunctionEntry { id, .. }
| CfgNode::SubshellEntry { id, .. } => *id,
}
}
pub fn label(&self) -> String {
match self {
CfgNode::Entry => "ENTRY".to_string(),
CfgNode::Exit => "EXIT".to_string(),
CfgNode::BasicBlock { label, .. } => label.clone(),
CfgNode::Conditional { label, .. } => label.clone(),
CfgNode::LoopHeader { label, .. } => label.clone(),
CfgNode::FunctionEntry { name, .. } => format!("fn {}", name),
CfgNode::SubshellEntry { .. } => "subshell".to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CfgEdge {
pub from: usize,
pub to: usize,
pub label: Option<String>,
pub is_back_edge: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ControlFlowGraph {
pub nodes: Vec<CfgNode>,
pub edges: Vec<CfgEdge>,
pub entry_id: usize,
pub exit_id: usize,
}
impl ControlFlowGraph {
pub fn new() -> Self {
Self {
nodes: vec![CfgNode::Entry, CfgNode::Exit],
edges: Vec::new(),
entry_id: 0,
exit_id: usize::MAX,
}
}
pub fn add_node(&mut self, node: CfgNode) -> usize {
let id = self.nodes.len();
self.nodes.push(node);
id
}
pub fn add_edge(&mut self, from: usize, to: usize, label: Option<String>) {
self.edges.push(CfgEdge {
from,
to,
label,
is_back_edge: false,
});
}
pub fn node_count(&self) -> usize {
self.nodes.len().saturating_sub(2)
}
pub fn edge_count(&self) -> usize {
self.edges.len()
}
pub fn successors(&self, node_id: usize) -> Vec<usize> {
self.edges
.iter()
.filter(|e| e.from == node_id)
.map(|e| e.to)
.collect()
}
pub fn predecessors(&self, node_id: usize) -> Vec<usize> {
self.edges
.iter()
.filter(|e| e.to == node_id)
.map(|e| e.from)
.collect()
}
pub fn mark_back_edges(&mut self) {
let mut visited = HashSet::new();
let mut stack = HashSet::new();
let mut back_edges = HashSet::new();
self.dfs_back_edges(0, &mut visited, &mut stack, &mut back_edges);
for edge in &mut self.edges {
if back_edges.contains(&(edge.from, edge.to)) {
edge.is_back_edge = true;
}
}
}
fn dfs_back_edges(
&self,
node: usize,
visited: &mut HashSet<usize>,
stack: &mut HashSet<usize>,
back_edges: &mut HashSet<(usize, usize)>,
) {
visited.insert(node);
stack.insert(node);
for succ in self.successors(node) {
if !visited.contains(&succ) {
self.dfs_back_edges(succ, visited, stack, back_edges);
} else if stack.contains(&succ) {
back_edges.insert((node, succ));
}
}
stack.remove(&node);
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ComplexityMetrics {
pub cyclomatic: usize,
pub essential: usize,
pub cognitive: usize,
pub max_depth: usize,
pub decision_points: usize,
pub loop_count: usize,
pub halstead_volume: Option<f64>,
}
impl ComplexityMetrics {
pub fn from_cfg(cfg: &ControlFlowGraph) -> Self {
let n = cfg.node_count() + 2; let e = cfg.edge_count();
let p = 1;
let cyclomatic = e.saturating_sub(n) + 2 * p;
let decision_points = cfg
.nodes
.iter()
.filter(|n| matches!(n, CfgNode::Conditional { .. }))
.count();
let loop_count = cfg
.nodes
.iter()
.filter(|n| matches!(n, CfgNode::LoopHeader { .. }))
.count();
let essential = cfg.edges.iter().filter(|e| e.is_back_edge).count();
let max_depth = Self::compute_max_depth(cfg);
let cognitive = decision_points + 2 * loop_count + max_depth;
Self {
cyclomatic,
essential,
cognitive,
max_depth,
decision_points,
loop_count,
halstead_volume: None,
}
}
fn compute_max_depth(cfg: &ControlFlowGraph) -> usize {
let mut max_depth = 0;
let mut visited = HashSet::new();
Self::dfs_depth(cfg, 0, 0, &mut max_depth, &mut visited);
max_depth
}
fn dfs_depth(
cfg: &ControlFlowGraph,
node: usize,
current_depth: usize,
max_depth: &mut usize,
visited: &mut HashSet<usize>,
) {
if visited.contains(&node) {
return;
}
visited.insert(node);
*max_depth = (*max_depth).max(current_depth);
let new_depth = match cfg.nodes.get(node) {
Some(CfgNode::Conditional { .. } | CfgNode::LoopHeader { .. }) => current_depth + 1,
_ => current_depth,
};
for succ in cfg.successors(node) {
Self::dfs_depth(cfg, succ, new_depth, max_depth, visited);
}
}
pub fn exceeds_threshold(&self) -> bool {
self.cyclomatic > 10
}
pub fn grade(&self) -> ComplexityGrade {
match self.cyclomatic {
0..=5 => ComplexityGrade::Simple,
6..=10 => ComplexityGrade::Moderate,
11..=20 => ComplexityGrade::Complex,
21..=50 => ComplexityGrade::VeryComplex,
_ => ComplexityGrade::Untestable,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ComplexityGrade {
Simple,
Moderate,
Complex,
VeryComplex,
Untestable,
}
impl ComplexityGrade {
pub fn description(&self) -> &'static str {
match self {
Self::Simple => "Simple, low risk",
Self::Moderate => "Moderate complexity, acceptable",
Self::Complex => "Complex, needs attention",
Self::VeryComplex => "Very complex, high risk",
Self::Untestable => "Untestable, must refactor",
}
}
pub fn needs_refactoring(&self) -> bool {
matches!(self, Self::Complex | Self::VeryComplex | Self::Untestable)
}
}
pub struct CfgBuilder {
cfg: ControlFlowGraph,
next_id: usize,
current_block: Option<usize>,
}
impl CfgBuilder {
pub fn new() -> Self {
Self {
cfg: ControlFlowGraph::new(),
next_id: 1, current_block: Some(0), }
}
pub fn add_block(&mut self, label: &str, start_line: usize, end_line: usize) -> usize {
let id = self.next_id;
self.next_id += 1;
let node = CfgNode::BasicBlock {
id,
label: label.to_string(),
start_line,
end_line,
};
self.cfg.nodes.push(node);
if let Some(prev) = self.current_block {
self.cfg.add_edge(prev, id, None);
}
self.current_block = Some(id);
id
}
pub fn add_conditional(&mut self, label: &str, line: usize) -> usize {
let id = self.next_id;
self.next_id += 1;
let node = CfgNode::Conditional {
id,
label: label.to_string(),
line,
};
self.cfg.nodes.push(node);
if let Some(prev) = self.current_block {
self.cfg.add_edge(prev, id, None);
}
self.current_block = Some(id);
id
}
pub fn add_loop(&mut self, label: &str, line: usize) -> usize {
let id = self.next_id;
self.next_id += 1;
let node = CfgNode::LoopHeader {
id,
label: label.to_string(),
line,
};
self.cfg.nodes.push(node);
if let Some(prev) = self.current_block {
self.cfg.add_edge(prev, id, None);
}
self.current_block = Some(id);
id
}
pub fn add_function(&mut self, name: &str, line: usize) -> usize {
let id = self.next_id;
self.next_id += 1;
let node = CfgNode::FunctionEntry {
id,
name: name.to_string(),
line,
};
self.cfg.nodes.push(node);
if let Some(prev) = self.current_block {
self.cfg.add_edge(prev, id, None);
}
self.current_block = Some(id);
id
}
pub fn add_edge(&mut self, from: usize, to: usize, label: Option<&str>) {
self.cfg.add_edge(from, to, label.map(String::from));
}
pub fn connect_to_exit(&mut self) {
if let Some(current) = self.current_block {
self.cfg.add_edge(current, usize::MAX, None);
}
}
pub fn set_current(&mut self, id: usize) {
self.current_block = Some(id);
}
pub fn build(mut self) -> ControlFlowGraph {
self.connect_to_exit();
self.cfg.mark_back_edges();
self.cfg
}
}
include!("cfg_default.rs");