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.included_nodes().count() as f64;
65 if total_nodes > 0.0 {
66 let orphans = inputs
67 .degree
68 .nodes
69 .iter()
70 .filter(|n| n.in_degree == 0)
71 .count() as f64;
72 metrics.push(Metric {
73 name: "orphan_ratio".into(),
74 value: orphans / total_nodes,
75 kind: MetricKind::Ratio,
76 dimension: "connectivity".into(),
77 });
78
79 let islands = inputs
80 .connected_components
81 .components
82 .iter()
83 .filter(|c| c.members.len() == 1)
84 .count() as f64;
85 metrics.push(Metric {
86 name: "island_ratio".into(),
87 value: islands / total_nodes,
88 kind: MetricKind::Ratio,
89 dimension: "connectivity".into(),
90 });
91 }
92
93 metrics.push(Metric {
95 name: "component_count".into(),
96 value: inputs.connected_components.component_count as f64,
97 kind: MetricKind::Count,
98 dimension: "complexity".into(),
99 });
100 metrics.push(Metric {
101 name: "density".into(),
102 value: inputs.graph_stats.density,
103 kind: MetricKind::Ratio,
104 dimension: "complexity".into(),
105 });
106 let internal_edge_count = graph
107 .edges
108 .iter()
109 .filter(|e| graph.is_internal_edge(e))
110 .count();
111 metrics.push(Metric {
112 name: "cyclomatic_complexity".into(),
113 value: (internal_edge_count as f64 - total_nodes
114 + inputs.connected_components.components.len() as f64),
115 kind: MetricKind::Count,
116 dimension: "complexity".into(),
117 });
118 if let Some(d) = inputs.graph_stats.diameter {
119 metrics.push(Metric {
120 name: "diameter".into(),
121 value: d as f64,
122 kind: MetricKind::Count,
123 dimension: "complexity".into(),
124 });
125 }
126 if let Some(avg) = inputs.graph_stats.average_path_length {
127 metrics.push(Metric {
128 name: "average_path_length".into(),
129 value: avg,
130 kind: MetricKind::Score,
131 dimension: "complexity".into(),
132 });
133 }
134
135 let total_edges = internal_edge_count as f64;
137 if total_edges > 0.0 {
138 metrics.push(Metric {
139 name: "redundant_edge_ratio".into(),
140 value: inputs.transitive_reduction.redundant_edges.len() as f64 / total_edges,
141 kind: MetricKind::Ratio,
142 dimension: "conciseness".into(),
143 });
144 }
145
146 metrics.push(Metric {
148 name: "bridge_count".into(),
149 value: inputs.bridges.bridges.len() as f64,
150 kind: MetricKind::Count,
151 dimension: "resilience".into(),
152 });
153 metrics.push(Metric {
154 name: "cut_node_count".into(),
155 value: inputs.bridges.cut_vertices.len() as f64,
156 kind: MetricKind::Count,
157 dimension: "resilience".into(),
158 });
159
160 if inputs.change_propagation.has_lockfile {
162 metrics.push(Metric {
163 name: "directly_changed_count".into(),
164 value: inputs.change_propagation.directly_changed.len() as f64,
165 kind: MetricKind::Count,
166 dimension: "freshness".into(),
167 });
168 metrics.push(Metric {
169 name: "transitively_stale_count".into(),
170 value: inputs.change_propagation.transitively_stale.len() as f64,
171 kind: MetricKind::Count,
172 dimension: "freshness".into(),
173 });
174 if total_nodes > 0.0 {
175 let stale = (inputs.change_propagation.directly_changed.len()
176 + inputs.change_propagation.transitively_stale.len())
177 as f64;
178 metrics.push(Metric {
179 name: "stale_ratio".into(),
180 value: stale / total_nodes,
181 kind: MetricKind::Ratio,
182 dimension: "freshness".into(),
183 });
184 }
185 }
186
187 if !inputs.pagerank.nodes.is_empty() {
189 let max = inputs
190 .pagerank
191 .nodes
192 .iter()
193 .map(|n| n.score)
194 .fold(f64::NEG_INFINITY, f64::max);
195 metrics.push(Metric {
196 name: "max_pagerank".into(),
197 value: max,
198 kind: MetricKind::Score,
199 dimension: "complexity".into(),
200 });
201 }
202
203 metrics.push(Metric {
205 name: "cycle_count".into(),
206 value: inputs.scc.nontrivial_count as f64,
207 kind: MetricKind::Count,
208 dimension: "complexity".into(),
209 });
210
211 metrics
212}