codehelion_core/structural/
reporting.rs1use super::{
2 BTreeMap, BTreeSet, BodyMateriality, Boilerplate, BuildVariant, CloneGroupFingerprint,
3 FileFeatures, FragmentFingerprint, GroupDetail, Lexeme, SourceTokenSpan, StructuralConfig,
4 SyntaxIrFile, Token, TokenKind, Unit, UnitEvidence, features, grouping, stable_id,
5 substitution, test_code, verify, view,
6};
7
8pub(super) fn group_detail(
12 group: &grouping::StructuralGroup,
13 units: &[Unit],
14 files: &[SyntaxIrFile],
15 feature_files: &[FileFeatures],
16 evidence: &UnitEvidence,
17 variant: &BuildVariant,
18 config: &StructuralConfig,
19) -> GroupDetail {
20 let medoid_view = view(group.canonical, units, files, feature_files, evidence);
21 let member_breakdowns = group
22 .members
23 .iter()
24 .map(|&member| {
25 verify::verify(
26 &medoid_view,
27 &view(member, units, files, feature_files, evidence),
28 &config.verify,
29 )
30 .breakdown
31 })
32 .collect();
33 let cohesion_breakdown = group
34 .members
35 .iter()
36 .enumerate()
37 .flat_map(|(left, &a)| group.members[left + 1..].iter().map(move |&b| (a, b)))
38 .map(|(a, b)| {
39 verify::verify(
40 &view(a, units, files, feature_files, evidence),
41 &view(b, units, files, feature_files, evidence),
42 &config.verify,
43 )
44 .breakdown
45 })
46 .min_by(|left, right| left.composite.total_cmp(&right.composite))
47 .unwrap_or_else(|| verify::verify(&medoid_view, &medoid_view, &config.verify).breakdown);
51
52 let fingerprint = group_fingerprint(group, units, variant);
53
54 GroupDetail {
55 fingerprint,
56 member_breakdowns,
57 cohesion_breakdown,
58 identifier_jaccard: group_identifier_jaccard(group, units, files),
59 body_materiality: group_body_materiality(group, units, feature_files),
60 boilerplate: dominant_boilerplate(group, units),
61 test_code: group.members.iter().all(|&member| units[member].test_code),
62 test_code_evidence: test_code::aggregate_evidence(
63 group
64 .members
65 .iter()
66 .map(|&member| units[member].test_code_evidence),
67 ),
68 width_family: written_once_per_width(group, units, files),
69 }
70}
71
72pub(super) fn group_fingerprint(
78 group: &grouping::StructuralGroup,
79 units: &[Unit],
80 variant: &BuildVariant,
81) -> CloneGroupFingerprint {
82 let member_contents: Vec<FragmentFingerprint> = group
83 .members
84 .iter()
85 .map(|&member| units[member].group_content(group.clone_type))
86 .collect();
87 stable_id::structural_clone_group_fingerprint(
88 variant,
89 group.clone_type,
90 &units[group.canonical].group_content(group.clone_type),
91 &member_contents,
92 )
93}
94
95pub(super) fn group_body_materiality(
97 group: &grouping::StructuralGroup,
98 units: &[Unit],
99 feature_files: &[FileFeatures],
100) -> BodyMateriality {
101 let members: Vec<&features::UnitFeatures> = group
102 .members
103 .iter()
104 .map(|&member| {
105 let unit = &units[member];
106 &feature_files[unit.file].units[unit.local]
107 })
108 .collect();
109 BodyMateriality {
110 has_loop: members
111 .iter()
112 .all(|features| features.cfg.max_loop_depth > 0),
113 has_dynamic_allocation: members
114 .iter()
115 .all(|features| features.api.names.iter().any(is_allocation_api)),
116 call_count: members
117 .iter()
118 .map(|features| u64::try_from(features.api.names.len()).unwrap_or(u64::MAX))
119 .min()
120 .unwrap_or(0),
121 }
122}
123
124pub(super) fn is_allocation_api(name: &Lexeme) -> bool {
130 matches!(
131 name.as_str(),
132 "aligned_alloc"
133 | "calloc"
134 | "make_shared"
135 | "make_unique"
136 | "malloc"
137 | "realloc"
138 | "reserve"
139 | "reserve_exact"
140 | "try_reserve"
141 | "try_reserve_exact"
142 | "with_capacity"
143 | "with_capacity_and_hasher"
144 )
145}
146
147#[must_use]
156pub fn span_identifier_jaccard(
157 files: &[SyntaxIrFile],
158 canonical: SourceTokenSpan,
159 corresponding: impl IntoIterator<Item = SourceTokenSpan>,
160) -> f64 {
161 let canonical = identifier_set(files, canonical);
162 corresponding
163 .into_iter()
164 .map(|span| set_jaccard(&canonical, &identifier_set(files, span)))
165 .min_by(f64::total_cmp)
166 .unwrap_or(1.0)
167}
168
169fn group_identifier_jaccard(
172 group: &grouping::StructuralGroup,
173 units: &[Unit],
174 files: &[SyntaxIrFile],
175) -> f64 {
176 span_identifier_jaccard(
177 files,
178 unit_token_span(&units[group.canonical]),
179 group
180 .members
181 .iter()
182 .filter(|&&member| member != group.canonical)
183 .map(|&member| unit_token_span(&units[member])),
184 )
185}
186
187const fn unit_token_span(unit: &Unit) -> SourceTokenSpan {
188 SourceTokenSpan::new(unit.file, unit.tokens.0, unit.tokens.1)
189}
190
191fn identifier_set(files: &[SyntaxIrFile], span: SourceTokenSpan) -> BTreeSet<&str> {
192 let tokens = files
193 .get(span.file)
194 .and_then(|file| file.tokens.get(span.token_start..span.token_end))
195 .unwrap_or(&[]);
196 tokens
197 .iter()
198 .filter(|token| matches!(token.kind, TokenKind::Identifier))
199 .map(|token| token.text.as_str())
200 .collect()
201}
202
203#[allow(clippy::cast_precision_loss)]
204pub(super) fn set_jaccard(left: &BTreeSet<&str>, right: &BTreeSet<&str>) -> f64 {
205 let union = left.union(right).count();
206 if union == 0 {
207 return 1.0;
208 }
209 left.intersection(right).count() as f64 / union as f64
213}
214
215pub(super) fn written_once_per_width(
227 group: &grouping::StructuralGroup,
228 units: &[Unit],
229 files: &[SyntaxIrFile],
230) -> bool {
231 written_once_per_width_members(group.canonical, &group.members, units, files)
232}
233
234pub(super) fn written_once_per_width_members(
236 canonical: usize,
237 members: &[usize],
238 units: &[Unit],
239 files: &[SyntaxIrFile],
240) -> bool {
241 let medoid = unit_tokens(&units[canonical], files);
242 let mut compared = 0usize;
243 for &member in members {
244 if member == canonical {
245 continue;
246 }
247 compared += 1;
248 let alike = substitution::witness(medoid, unit_tokens(&units[member], files))
249 .is_some_and(|witness| witness.written_once_per_width());
250 if !alike {
251 return false;
252 }
253 }
254 compared > 0
255}
256
257fn unit_tokens<'a>(unit: &Unit, files: &'a [SyntaxIrFile]) -> &'a [Token] {
259 &files[unit.file].tokens[unit.tokens.0..unit.tokens.1]
260}
261
262pub(super) fn dominant_boilerplate(
271 group: &grouping::StructuralGroup,
272 units: &[Unit],
273) -> Option<Boilerplate> {
274 dominant_boilerplate_members(&group.members, units)
275}
276
277pub(super) fn dominant_boilerplate_members(
279 members: &[usize],
280 units: &[Unit],
281) -> Option<Boilerplate> {
282 let mut counts = BTreeMap::new();
283 for &member in members {
284 if let Some(category) = units[member].boilerplate {
285 *counts.entry(category).or_insert(0usize) += 1;
286 }
287 }
288 let (category, count) = counts
289 .into_iter()
290 .max_by_key(|(category, count)| (*count, *category))?;
291 (count.saturating_mul(5) >= members.len().saturating_mul(4)).then_some(category)
292}