1use petgraph::algo::connected_components;
15use petgraph::graph::UnGraph;
16use petgraph::Graph;
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19
20use srcgraph_core::{ClassNode, EdgeKind, MethodConnectivity};
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct LcomScore {
25 pub class_id: String,
27 pub lcom4: Option<usize>,
31}
32
33pub fn build_method_graph(conn: &MethodConnectivity) -> UnGraph<String, ()> {
39 let mut g: UnGraph<String, ()> = UnGraph::new_undirected();
40 let mut idx: HashMap<&str, _> = HashMap::with_capacity(conn.methods.len());
41 for m in &conn.methods {
42 let id = g.add_node(m.clone());
43 idx.insert(m.as_str(), id);
44 }
45 for (a, b) in &conn.edges {
46 let ia = *idx
47 .entry(a.as_str())
48 .or_insert_with(|| g.add_node(a.clone()));
49 let ib = *idx
50 .entry(b.as_str())
51 .or_insert_with(|| g.add_node(b.clone()));
52 g.add_edge(ia, ib, ());
53 }
54 g
55}
56
57pub fn compute_lcom4(method_graph: &UnGraph<String, ()>) -> Option<usize> {
63 if method_graph.node_count() == 0 {
64 return None;
65 }
66 Some(connected_components(method_graph))
67}
68
69pub fn compute_lcom4_all<N, E>(graph: &Graph<N, E>) -> Vec<LcomScore>
75where
76 N: ClassNode,
77 E: EdgeKind,
78{
79 graph
80 .node_indices()
81 .map(|nx| {
82 let node = &graph[nx];
83 let lcom4 = node
84 .method_connectivity()
85 .and_then(|conn| compute_lcom4(&build_method_graph(conn)));
86 LcomScore {
87 class_id: node.id().to_owned(),
88 lcom4,
89 }
90 })
91 .collect()
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use srcgraph_core::{EdgeType, OwnedClassNode, OwnedGraph};
98 use petgraph::Graph;
99
100 fn class(id: &str, conn: Option<MethodConnectivity>) -> OwnedClassNode {
101 OwnedClassNode {
102 id: id.to_owned(),
103 name: id.split('.').last().unwrap_or(id).to_owned(),
104 namespace: "test".to_owned(),
105 line_count: 10,
106 method_count: conn.as_ref().map_or(0, |c| c.methods.len() as u32),
107 halstead_eta1: 0,
108 halstead_eta2: 0,
109 halstead_n1: 0,
110 halstead_n2: 0,
111 method_connectivity: conn,
112 method_fingerprints: None,
113 method_tokens: None,
114 call_sequences: None,
115 cyclomatic_complexity: None,
116 path_conditions: None,
117 invariants: None,
118 error_messages: None,
119 magic_numbers: None,
120 dead_code: None,
121 tenant_branches: None,
122 state_transitions: None,
123 }
124 }
125
126 fn conn(methods: &[&str], edges: &[(&str, &str)]) -> MethodConnectivity {
127 MethodConnectivity {
128 methods: methods.iter().map(|s| s.to_string()).collect(),
129 edges: edges
130 .iter()
131 .map(|(a, b)| (a.to_string(), b.to_string()))
132 .collect(),
133 }
134 }
135
136 #[test]
137 fn lcom4_single_cohesive_component() {
138 let mg = build_method_graph(&conn(
140 &["foo", "bar", "baz"],
141 &[("foo", "bar"), ("bar", "baz")],
142 ));
143 assert_eq!(compute_lcom4(&mg), Some(1));
144 }
145
146 #[test]
147 fn lcom4_two_disjoint_clusters() {
148 let mg = build_method_graph(&conn(
150 &["foo", "bar", "baz", "qux"],
151 &[("foo", "bar"), ("baz", "qux")],
152 ));
153 assert_eq!(compute_lcom4(&mg), Some(2));
154 }
155
156 #[test]
157 fn lcom4_isolated_methods_each_count() {
158 let mg = build_method_graph(&conn(&["foo", "bar", "baz"], &[]));
160 assert_eq!(compute_lcom4(&mg), Some(3));
161 }
162
163 #[test]
164 fn lcom4_empty_is_undefined() {
165 let mg = build_method_graph(&conn(&[], &[]));
166 assert_eq!(compute_lcom4(&mg), None);
167 }
168
169 #[test]
170 fn lcom4_edge_endpoint_outside_methods_list() {
171 let mg = build_method_graph(&conn(&["foo"], &[("foo", "ghost")]));
174 assert_eq!(compute_lcom4(&mg), Some(1));
175 assert_eq!(mg.node_count(), 2);
176 }
177
178 #[test]
179 fn lcom4_all_skips_classes_without_connectivity() {
180 let mut g: OwnedGraph = Graph::new();
181 let a = g.add_node(class(
182 "A",
183 Some(conn(&["m1", "m2"], &[("m1", "m2")])),
184 ));
185 let b = g.add_node(class("B", None));
186 let c = g.add_node(class(
187 "C",
188 Some(conn(&["x", "y", "z"], &[])),
189 ));
190 g.add_edge(a, b, EdgeType::MethodCall);
191 g.add_edge(b, c, EdgeType::Inheritance);
192
193 let scores = compute_lcom4_all(&g);
194 assert_eq!(scores.len(), 3);
195
196 let by_id: HashMap<_, _> = scores
197 .iter()
198 .map(|s| (s.class_id.as_str(), s.lcom4))
199 .collect();
200 assert_eq!(by_id["A"], Some(1));
201 assert_eq!(by_id["B"], None, "no methodConnectivity → None");
202 assert_eq!(by_id["C"], Some(3), "three isolated methods");
203 }
204}