1use crate::analyses::{
2 bridges::BridgesResult, change_propagation::ChangePropagationResult,
3 connected_components::ConnectedComponentsResult, degree::DegreeResult,
4 graph_stats::GraphStatsResult, pagerank::PageRankResult, scc::SccResult,
5 transitive_reduction::TransitiveReductionResult,
6};
7use crate::graph::Graph;
8
9#[derive(Debug, Clone, serde::Serialize)]
11pub struct Metric {
12 pub name: String,
13 pub value: f64,
14 pub kind: MetricKind,
15 pub dimension: String,
16}
17
18#[derive(Debug, Clone, serde::Serialize)]
19#[serde(rename_all = "snake_case")]
20pub enum MetricKind {
21 Ratio,
22 Count,
23 Score,
24}
25
26pub fn all_metric_names() -> &'static [&'static str] {
28 &[
29 "average_path_length",
30 "bridge_count",
31 "component_count",
32 "cut_node_count",
33 "cycle_count",
34 "cyclomatic_complexity",
35 "density",
36 "diameter",
37 "directly_changed_count",
38 "island_ratio",
39 "max_pagerank",
40 "orphan_ratio",
41 "redundant_edge_ratio",
42 "stale_ratio",
43 "transitively_stale_count",
44 ]
45}
46
47pub struct AnalysisInputs<'a> {
49 pub degree: &'a DegreeResult,
50 pub scc: &'a SccResult,
51 pub connected_components: &'a ConnectedComponentsResult,
52 pub graph_stats: &'a GraphStatsResult,
53 pub bridges: &'a BridgesResult,
54 pub transitive_reduction: &'a TransitiveReductionResult,
55 pub change_propagation: &'a ChangePropagationResult,
56 pub pagerank: &'a PageRankResult,
57}
58
59pub fn compute_metrics(inputs: &AnalysisInputs, graph: &Graph) -> Vec<Metric> {
61 let mut metrics: Vec<Metric> = Vec::new();
62
63 let total_nodes = graph
65 .nodes
66 .values()
67 .filter(|n| graph.is_file_node(&n.path))
68 .count() as f64;
69 if total_nodes > 0.0 {
70 let orphans = inputs
71 .degree
72 .nodes
73 .iter()
74 .filter(|n| n.in_degree == 0)
75 .count() as f64;
76 metrics.push(Metric {
77 name: "orphan_ratio".into(),
78 value: orphans / total_nodes,
79 kind: MetricKind::Ratio,
80 dimension: "connectivity".into(),
81 });
82
83 let islands = inputs
84 .connected_components
85 .components
86 .iter()
87 .filter(|c| c.members.len() == 1)
88 .count() as f64;
89 metrics.push(Metric {
90 name: "island_ratio".into(),
91 value: islands / total_nodes,
92 kind: MetricKind::Ratio,
93 dimension: "connectivity".into(),
94 });
95 }
96
97 metrics.push(Metric {
99 name: "component_count".into(),
100 value: inputs.connected_components.component_count as f64,
101 kind: MetricKind::Count,
102 dimension: "complexity".into(),
103 });
104 metrics.push(Metric {
105 name: "density".into(),
106 value: inputs.graph_stats.density,
107 kind: MetricKind::Ratio,
108 dimension: "complexity".into(),
109 });
110 metrics.push(Metric {
111 name: "cyclomatic_complexity".into(),
112 value: (graph.edges.len() as f64 - graph.nodes.len() as f64
113 + inputs.connected_components.components.len() as f64),
114 kind: MetricKind::Count,
115 dimension: "complexity".into(),
116 });
117 if let Some(d) = inputs.graph_stats.diameter {
118 metrics.push(Metric {
119 name: "diameter".into(),
120 value: d as f64,
121 kind: MetricKind::Count,
122 dimension: "complexity".into(),
123 });
124 }
125 if let Some(avg) = inputs.graph_stats.average_path_length {
126 metrics.push(Metric {
127 name: "average_path_length".into(),
128 value: avg,
129 kind: MetricKind::Score,
130 dimension: "complexity".into(),
131 });
132 }
133
134 let total_edges = graph.edges.len() as f64;
136 if total_edges > 0.0 {
137 metrics.push(Metric {
138 name: "redundant_edge_ratio".into(),
139 value: inputs.transitive_reduction.redundant_edges.len() as f64 / total_edges,
140 kind: MetricKind::Ratio,
141 dimension: "conciseness".into(),
142 });
143 }
144
145 metrics.push(Metric {
147 name: "bridge_count".into(),
148 value: inputs.bridges.bridges.len() as f64,
149 kind: MetricKind::Count,
150 dimension: "resilience".into(),
151 });
152 metrics.push(Metric {
153 name: "cut_node_count".into(),
154 value: inputs.bridges.cut_vertices.len() as f64,
155 kind: MetricKind::Count,
156 dimension: "resilience".into(),
157 });
158
159 if inputs.change_propagation.has_lockfile {
161 metrics.push(Metric {
162 name: "directly_changed_count".into(),
163 value: inputs.change_propagation.directly_changed.len() as f64,
164 kind: MetricKind::Count,
165 dimension: "freshness".into(),
166 });
167 metrics.push(Metric {
168 name: "transitively_stale_count".into(),
169 value: inputs.change_propagation.transitively_stale.len() as f64,
170 kind: MetricKind::Count,
171 dimension: "freshness".into(),
172 });
173 if total_nodes > 0.0 {
174 let stale = (inputs.change_propagation.directly_changed.len()
175 + inputs.change_propagation.transitively_stale.len())
176 as f64;
177 metrics.push(Metric {
178 name: "stale_ratio".into(),
179 value: stale / total_nodes,
180 kind: MetricKind::Ratio,
181 dimension: "freshness".into(),
182 });
183 }
184 }
185
186 if !inputs.pagerank.nodes.is_empty() {
188 let max = inputs
189 .pagerank
190 .nodes
191 .iter()
192 .map(|n| n.score)
193 .fold(f64::NEG_INFINITY, f64::max);
194 metrics.push(Metric {
195 name: "max_pagerank".into(),
196 value: max,
197 kind: MetricKind::Score,
198 dimension: "complexity".into(),
199 });
200 }
201
202 metrics.push(Metric {
204 name: "cycle_count".into(),
205 value: inputs.scc.nontrivial_count as f64,
206 kind: MetricKind::Count,
207 dimension: "complexity".into(),
208 });
209
210 metrics
211}