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_included_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 let internal_edge_count = graph
111 .edges
112 .iter()
113 .filter(|e| graph.is_internal_edge(e))
114 .count();
115 metrics.push(Metric {
116 name: "cyclomatic_complexity".into(),
117 value: (internal_edge_count as f64 - total_nodes
118 + inputs.connected_components.components.len() as f64),
119 kind: MetricKind::Count,
120 dimension: "complexity".into(),
121 });
122 if let Some(d) = inputs.graph_stats.diameter {
123 metrics.push(Metric {
124 name: "diameter".into(),
125 value: d as f64,
126 kind: MetricKind::Count,
127 dimension: "complexity".into(),
128 });
129 }
130 if let Some(avg) = inputs.graph_stats.average_path_length {
131 metrics.push(Metric {
132 name: "average_path_length".into(),
133 value: avg,
134 kind: MetricKind::Score,
135 dimension: "complexity".into(),
136 });
137 }
138
139 let total_edges = internal_edge_count as f64;
141 if total_edges > 0.0 {
142 metrics.push(Metric {
143 name: "redundant_edge_ratio".into(),
144 value: inputs.transitive_reduction.redundant_edges.len() as f64 / total_edges,
145 kind: MetricKind::Ratio,
146 dimension: "conciseness".into(),
147 });
148 }
149
150 metrics.push(Metric {
152 name: "bridge_count".into(),
153 value: inputs.bridges.bridges.len() as f64,
154 kind: MetricKind::Count,
155 dimension: "resilience".into(),
156 });
157 metrics.push(Metric {
158 name: "cut_node_count".into(),
159 value: inputs.bridges.cut_vertices.len() as f64,
160 kind: MetricKind::Count,
161 dimension: "resilience".into(),
162 });
163
164 if inputs.change_propagation.has_lockfile {
166 metrics.push(Metric {
167 name: "directly_changed_count".into(),
168 value: inputs.change_propagation.directly_changed.len() as f64,
169 kind: MetricKind::Count,
170 dimension: "freshness".into(),
171 });
172 metrics.push(Metric {
173 name: "transitively_stale_count".into(),
174 value: inputs.change_propagation.transitively_stale.len() as f64,
175 kind: MetricKind::Count,
176 dimension: "freshness".into(),
177 });
178 if total_nodes > 0.0 {
179 let stale = (inputs.change_propagation.directly_changed.len()
180 + inputs.change_propagation.transitively_stale.len())
181 as f64;
182 metrics.push(Metric {
183 name: "stale_ratio".into(),
184 value: stale / total_nodes,
185 kind: MetricKind::Ratio,
186 dimension: "freshness".into(),
187 });
188 }
189 }
190
191 if !inputs.pagerank.nodes.is_empty() {
193 let max = inputs
194 .pagerank
195 .nodes
196 .iter()
197 .map(|n| n.score)
198 .fold(f64::NEG_INFINITY, f64::max);
199 metrics.push(Metric {
200 name: "max_pagerank".into(),
201 value: max,
202 kind: MetricKind::Score,
203 dimension: "complexity".into(),
204 });
205 }
206
207 metrics.push(Metric {
209 name: "cycle_count".into(),
210 value: inputs.scc.nontrivial_count as f64,
211 kind: MetricKind::Count,
212 dimension: "complexity".into(),
213 });
214
215 metrics
216}