use anyhow::Result;
use sqlitegraph::{GraphBackend, GraphEntity, SnapshotId};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use super::schema::{FileMetrics, SymbolMetrics};
use crate::graph::schema::SymbolNode;
pub struct V3MetricsCompute {
backend: Arc<dyn GraphBackend>,
}
impl V3MetricsCompute {
pub fn new(backend: Arc<dyn GraphBackend>) -> Self {
Self { backend }
}
pub fn compute_for_file<F, S>(
&self,
file_path: &str,
source: &[u8],
symbol_facts: &[SymbolNode],
store_fn: F,
store_symbol_fn: S,
) -> Result<()>
where
F: Fn(&FileMetrics) -> Result<()>,
S: Fn(&SymbolMetrics) -> Result<()>,
{
let symbol_count = symbol_facts.len() as i64;
let loc = source.iter().filter(|&&b| b == b'\n').count() as i64 + 1;
let estimated_loc = source.len() as f64 / 40.0;
let fan_in = self.compute_file_fan_in(file_path)?;
let fan_out = self.compute_file_fan_out(file_path)?;
let complexity_score = calculate_complexity(loc, fan_in, fan_out);
let file_metrics = FileMetrics {
file_path: file_path.to_string(),
symbol_count,
loc,
estimated_loc,
fan_in,
fan_out,
complexity_score,
last_updated: Self::now_timestamp(),
};
store_fn(&file_metrics)?;
for symbol in symbol_facts {
if let Err(e) =
self.compute_and_store_symbol_metrics(symbol, file_path, &store_symbol_fn)
{
let symbol_name = symbol.name.as_deref().unwrap_or("<unknown>");
eprintln!(
"Warning: Failed to compute metrics for symbol '{}': {}",
symbol_name, e
);
}
}
Ok(())
}
fn compute_file_fan_in(&self, file_path: &str) -> Result<i64> {
let snapshot = SnapshotId::current();
let entity_ids = self.backend.entity_ids()?;
let mut count = 0;
for entity_id in entity_ids {
let node = match self.backend.get_node(snapshot, entity_id) {
Ok(n) => n,
Err(_) => continue,
};
if !self.is_node_in_file(&node, file_path) {
continue;
}
let incoming = self.backend.neighbors(
snapshot,
entity_id,
sqlitegraph::NeighborQuery {
direction: sqlitegraph::BackendDirection::Incoming,
edge_type: None,
},
)?;
for source_id in incoming {
let source_node = match self.backend.get_node(snapshot, source_id) {
Ok(n) => n,
Err(_) => continue,
};
if !self.is_node_in_file(&source_node, file_path) {
count += 1;
}
}
}
Ok(count)
}
fn compute_file_fan_out(&self, file_path: &str) -> Result<i64> {
let snapshot = SnapshotId::current();
let entity_ids = self.backend.entity_ids()?;
let mut count = 0;
for entity_id in entity_ids {
let node = match self.backend.get_node(snapshot, entity_id) {
Ok(n) => n,
Err(_) => continue,
};
if !self.is_node_in_file(&node, file_path) {
continue;
}
let outgoing = self.backend.neighbors(
snapshot,
entity_id,
sqlitegraph::NeighborQuery {
direction: sqlitegraph::BackendDirection::Outgoing,
edge_type: None,
},
)?;
for target_id in outgoing {
let target_node = match self.backend.get_node(snapshot, target_id) {
Ok(n) => n,
Err(_) => continue,
};
if !self.is_node_in_file(&target_node, file_path) {
count += 1;
}
}
}
Ok(count)
}
fn is_node_in_file(&self, node: &GraphEntity, file_path: &str) -> bool {
match serde_json::from_value::<serde_json::Value>(node.data.clone()) {
Ok(data) => {
if let Some(fp) = data.get("file_path").and_then(|v| v.as_str()) {
return fp == file_path;
}
if let Some(f) = data.get("file").and_then(|v| v.as_str()) {
return f == file_path;
}
false
}
Err(_) => false,
}
}
fn compute_and_store_symbol_metrics<S>(
&self,
symbol: &SymbolNode,
file_path: &str,
store_fn: &S,
) -> Result<()>
where
S: Fn(&SymbolMetrics) -> Result<()>,
{
let fqn = symbol.fqn.as_deref().unwrap_or("");
if fqn.is_empty() {
return Ok(()); }
let symbol_id = match self.find_symbol_id(fqn)? {
Some(id) => id,
None => return Ok(()), };
let loc = if symbol.end_line > 0 && symbol.end_line >= symbol.start_line {
(symbol.end_line - symbol.start_line + 1) as i64
} else {
1
};
let byte_span = if symbol.byte_end > symbol.byte_start {
symbol.byte_end - symbol.byte_start
} else {
1
};
let estimated_loc = byte_span as f64 / 40.0;
let fan_in = self.compute_symbol_fan_in(symbol_id)?;
let fan_out = self.compute_symbol_fan_out(symbol_id)?;
let symbol_name = symbol.name.as_deref().unwrap_or("").to_string();
let metrics = SymbolMetrics {
symbol_id,
symbol_name,
kind: symbol.kind.clone(),
file_path: file_path.to_string(),
loc,
estimated_loc,
fan_in,
fan_out,
cyclomatic_complexity: 1, last_updated: Self::now_timestamp(),
};
store_fn(&metrics)?;
Ok(())
}
fn find_symbol_id(&self, fqn: &str) -> Result<Option<i64>> {
let snapshot = SnapshotId::current();
let entity_ids = self.backend.entity_ids()?;
for entity_id in entity_ids {
let node = match self.backend.get_node(snapshot, entity_id) {
Ok(n) => n,
Err(_) => continue,
};
if node.kind != "Symbol" {
continue;
}
match serde_json::from_value::<serde_json::Value>(node.data) {
Ok(data) => {
if let Some(node_fqn) = data.get("fqn").and_then(|v| v.as_str()) {
if node_fqn == fqn {
return Ok(Some(entity_id));
}
}
}
Err(_) => continue,
}
}
Ok(None)
}
fn compute_symbol_fan_in(&self, symbol_id: i64) -> Result<i64> {
let snapshot = SnapshotId::current();
let count = self.backend.neighbors(
snapshot,
symbol_id,
sqlitegraph::NeighborQuery {
direction: sqlitegraph::BackendDirection::Incoming,
edge_type: None,
},
)?;
Ok(count.len() as i64)
}
fn compute_symbol_fan_out(&self, symbol_id: i64) -> Result<i64> {
let snapshot = SnapshotId::current();
let count = self.backend.neighbors(
snapshot,
symbol_id,
sqlitegraph::NeighborQuery {
direction: sqlitegraph::BackendDirection::Outgoing,
edge_type: None,
},
)?;
Ok(count.len() as i64)
}
fn now_timestamp() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64
}
}
fn calculate_complexity(loc: i64, fan_in: i64, fan_out: i64) -> f64 {
let loc_weight = 0.1;
let fan_in_weight = 0.5;
let fan_out_weight = 0.3;
(loc as f64 * loc_weight) + (fan_in as f64 * fan_in_weight) + (fan_out as f64 * fan_out_weight)
}