1use std::collections::BTreeMap;
4use std::path::Path;
5
6use fallow_engine::duplicates::CloneFingerprintSet;
7use fallow_types::duplicates::{CloneGroup, DuplicationReport, DuplicationStats};
8use fallow_types::results::AnalysisResults;
9use rustc_hash::{FxHashMap, FxHashSet};
10
11use crate::{
12 AttributedCloneGroup, AttributedCloneGroupFinding, AttributedInstance, CloneFamilyFinding,
13 DuplicationGroup, DuplicationGrouping,
14};
15
16pub const UNOWNED_GROUP_LABEL: &str = "(unowned)";
18
19pub struct ResultGroup {
21 pub key: String,
23 pub owners: Option<Vec<String>>,
28 pub results: AnalysisResults,
30}
31
32#[must_use]
37pub fn group_analysis_results_with<F, O>(
38 results: &AnalysisResults,
39 mut key_for_path: F,
40 mut owners_for_path: O,
41 include_owners: bool,
42) -> Vec<ResultGroup>
43where
44 F: FnMut(&Path) -> String,
45 O: FnMut(&Path) -> Option<Vec<String>>,
46{
47 let mut group_owners: FxHashMap<String, Vec<String>> = FxHashMap::default();
48 let mut builder = GroupingBuilder::new(|path: &Path| {
49 let key = key_for_path(path);
50 if include_owners && !group_owners.contains_key(&key) {
51 let owners = owners_for_path(path).unwrap_or_default();
52 group_owners.insert(key.clone(), owners);
53 }
54 key
55 });
56 builder.group_symbol_issues(results);
57 builder.group_dependency_issues(results);
58 builder.group_relationship_issues(results);
59 builder.group_workspace_config_issues(results);
60
61 finalize_groups(builder.into_groups(), group_owners, include_owners)
62}
63
64struct GroupingBuilder<F> {
65 groups: FxHashMap<String, AnalysisResults>,
66 key_for: F,
67}
68
69impl<F> GroupingBuilder<F>
70where
71 F: FnMut(&Path) -> String,
72{
73 fn new(key_for: F) -> Self {
74 Self {
75 groups: FxHashMap::default(),
76 key_for,
77 }
78 }
79
80 fn entry_for_path(&mut self, path: &Path) -> &mut AnalysisResults {
81 let key = (self.key_for)(path);
82 self.groups.entry(key).or_default()
83 }
84
85 fn entry_for_key(&mut self, key: String) -> &mut AnalysisResults {
86 self.groups.entry(key).or_default()
87 }
88
89 fn into_groups(self) -> FxHashMap<String, AnalysisResults> {
90 self.groups
91 }
92
93 fn group_symbol_issues(&mut self, results: &AnalysisResults) {
94 for item in &results.unused_files {
95 self.entry_for_path(&item.file.path)
96 .unused_files
97 .push(item.clone());
98 }
99 for item in &results.unused_exports {
100 self.entry_for_path(&item.export.path)
101 .unused_exports
102 .push(item.clone());
103 }
104 for item in &results.unused_types {
105 self.entry_for_path(&item.export.path)
106 .unused_types
107 .push(item.clone());
108 }
109 for item in &results.private_type_leaks {
110 self.entry_for_path(&item.leak.path)
111 .private_type_leaks
112 .push(item.clone());
113 }
114 for item in &results.unused_enum_members {
115 self.entry_for_path(&item.member.path)
116 .unused_enum_members
117 .push(item.clone());
118 }
119 for item in &results.unused_class_members {
120 self.entry_for_path(&item.member.path)
121 .unused_class_members
122 .push(item.clone());
123 }
124 for item in &results.unused_store_members {
125 self.entry_for_path(&item.member.path)
126 .unused_store_members
127 .push(item.clone());
128 }
129 for item in &results.unresolved_imports {
130 self.entry_for_path(&item.import.path)
131 .unresolved_imports
132 .push(item.clone());
133 }
134 }
135
136 fn group_dependency_issues(&mut self, results: &AnalysisResults) {
137 for item in &results.unused_dependencies {
138 self.entry_for_path(&item.dep.path)
139 .unused_dependencies
140 .push(item.clone());
141 }
142 for item in &results.unused_dev_dependencies {
143 self.entry_for_path(&item.dep.path)
144 .unused_dev_dependencies
145 .push(item.clone());
146 }
147 for item in &results.unused_optional_dependencies {
148 self.entry_for_path(&item.dep.path)
149 .unused_optional_dependencies
150 .push(item.clone());
151 }
152 for item in &results.type_only_dependencies {
153 self.entry_for_path(&item.dep.path)
154 .type_only_dependencies
155 .push(item.clone());
156 }
157 for item in &results.test_only_dependencies {
158 self.entry_for_path(&item.dep.path)
159 .test_only_dependencies
160 .push(item.clone());
161 }
162
163 for item in &results.unlisted_dependencies {
164 let key = item.dep.imported_from.first().map_or_else(
165 || UNOWNED_GROUP_LABEL.to_string(),
166 |site| (self.key_for)(&site.path),
167 );
168 self.entry_for_key(key)
169 .unlisted_dependencies
170 .push(item.clone());
171 }
172 for item in &results.duplicate_exports {
173 let key = item.export.locations.first().map_or_else(
174 || UNOWNED_GROUP_LABEL.to_string(),
175 |loc| (self.key_for)(&loc.path),
176 );
177 self.entry_for_key(key).duplicate_exports.push(item.clone());
178 }
179 }
180
181 fn group_relationship_issues(&mut self, results: &AnalysisResults) {
182 self.group_structure_issues(results);
183 self.group_framework_boundary_issues(results);
184 self.group_component_contract_issues(results);
185 }
186
187 fn group_structure_issues(&mut self, results: &AnalysisResults) {
188 for item in &results.circular_dependencies {
189 let key = item
190 .cycle
191 .files
192 .first()
193 .map_or_else(|| UNOWNED_GROUP_LABEL.to_string(), |f| (self.key_for)(f));
194 self.entry_for_key(key)
195 .circular_dependencies
196 .push(item.clone());
197 }
198 for item in &results.boundary_violations {
199 self.entry_for_path(&item.violation.from_path)
200 .boundary_violations
201 .push(item.clone());
202 }
203 for item in &results.boundary_coverage_violations {
204 self.entry_for_path(&item.violation.path)
205 .boundary_coverage_violations
206 .push(item.clone());
207 }
208 for item in &results.boundary_call_violations {
209 self.entry_for_path(&item.violation.path)
210 .boundary_call_violations
211 .push(item.clone());
212 }
213 for item in &results.policy_violations {
214 self.entry_for_path(&item.violation.path)
215 .policy_violations
216 .push(item.clone());
217 }
218 }
219
220 fn group_framework_boundary_issues(&mut self, results: &AnalysisResults) {
221 for item in &results.invalid_client_exports {
222 self.entry_for_path(&item.export.path)
223 .invalid_client_exports
224 .push(item.clone());
225 }
226 for item in &results.mixed_client_server_barrels {
227 self.entry_for_path(&item.barrel.path)
228 .mixed_client_server_barrels
229 .push(item.clone());
230 }
231 for item in &results.misplaced_directives {
232 self.entry_for_path(&item.directive_site.path)
233 .misplaced_directives
234 .push(item.clone());
235 }
236 for item in &results.unprovided_injects {
237 self.entry_for_path(&item.inject.path)
238 .unprovided_injects
239 .push(item.clone());
240 }
241 for item in &results.unrendered_components {
242 self.entry_for_path(&item.component.path)
243 .unrendered_components
244 .push(item.clone());
245 }
246 }
247
248 fn group_component_contract_issues(&mut self, results: &AnalysisResults) {
249 for item in &results.unused_component_props {
250 self.entry_for_path(&item.prop.path)
251 .unused_component_props
252 .push(item.clone());
253 }
254 for item in &results.unused_component_emits {
255 self.entry_for_path(&item.emit.path)
256 .unused_component_emits
257 .push(item.clone());
258 }
259 for item in &results.unused_component_inputs {
260 self.entry_for_path(&item.input.path)
261 .unused_component_inputs
262 .push(item.clone());
263 }
264 for item in &results.unused_component_outputs {
265 self.entry_for_path(&item.output.path)
266 .unused_component_outputs
267 .push(item.clone());
268 }
269 for item in &results.unused_server_actions {
270 self.entry_for_path(&item.action.path)
271 .unused_server_actions
272 .push(item.clone());
273 }
274 for item in &results.unused_load_data_keys {
275 self.entry_for_path(&item.key.path)
276 .unused_load_data_keys
277 .push(item.clone());
278 }
279 for item in &results.stale_suppressions {
280 self.entry_for_path(&item.path)
281 .stale_suppressions
282 .push(item.clone());
283 }
284 }
285
286 fn group_workspace_config_issues(&mut self, results: &AnalysisResults) {
287 for item in &results.unused_catalog_entries {
288 self.entry_for_path(&item.entry.path)
289 .unused_catalog_entries
290 .push(item.clone());
291 }
292 for item in &results.empty_catalog_groups {
293 self.entry_for_path(&item.group.path)
294 .empty_catalog_groups
295 .push(item.clone());
296 }
297 for item in &results.unresolved_catalog_references {
298 self.entry_for_path(&item.reference.path)
299 .unresolved_catalog_references
300 .push(item.clone());
301 }
302 for item in &results.unused_dependency_overrides {
303 self.entry_for_path(&item.entry.path)
304 .unused_dependency_overrides
305 .push(item.clone());
306 }
307 for item in &results.misconfigured_dependency_overrides {
308 self.entry_for_path(&item.entry.path)
309 .misconfigured_dependency_overrides
310 .push(item.clone());
311 }
312 }
313}
314
315fn finalize_groups(
316 groups: FxHashMap<String, AnalysisResults>,
317 mut group_owners: FxHashMap<String, Vec<String>>,
318 include_owners: bool,
319) -> Vec<ResultGroup> {
320 let mut sorted: Vec<_> = groups
321 .into_iter()
322 .map(|(key, results)| {
323 let owners = if include_owners {
324 Some(group_owners.remove(&key).unwrap_or_default())
325 } else {
326 None
327 };
328 ResultGroup {
329 key,
330 owners,
331 results,
332 }
333 })
334 .collect();
335 sorted.sort_by(|a, b| {
336 let a_unowned = a.key == UNOWNED_GROUP_LABEL;
337 let b_unowned = b.key == UNOWNED_GROUP_LABEL;
338 match (a_unowned, b_unowned) {
339 (true, false) => std::cmp::Ordering::Greater,
340 (false, true) => std::cmp::Ordering::Less,
341 _ => b
342 .results
343 .total_issues()
344 .cmp(&a.results.total_issues())
345 .then_with(|| a.key.cmp(&b.key)),
346 }
347 });
348 sorted
349}
350
351#[must_use]
353pub fn largest_clone_group_owner_with<F>(group: &CloneGroup, mut key_for_path: F) -> String
354where
355 F: FnMut(&Path) -> String,
356{
357 let mut counts: BTreeMap<String, u32> = BTreeMap::new();
358 for instance in &group.instances {
359 let key = key_for_path(&instance.file);
360 *counts.entry(key).or_insert(0) += 1;
361 }
362 if counts.is_empty() {
363 return UNOWNED_GROUP_LABEL.to_string();
364 }
365 let mut best_key: Option<String> = None;
366 let mut best_count: u32 = 0;
367 for (key, count) in counts {
368 if best_key.is_none() || count > best_count {
369 best_count = count;
370 best_key = Some(key);
371 }
372 }
373 best_key.unwrap_or_else(|| UNOWNED_GROUP_LABEL.to_string())
374}
375
376#[must_use]
378pub fn build_duplication_grouping_with<F>(
379 report: &DuplicationReport,
380 mode: &'static str,
381 mut key_for_path: F,
382) -> DuplicationGrouping
383where
384 F: FnMut(&Path) -> String,
385{
386 let fingerprints = CloneFingerprintSet::from_groups(&report.clone_groups);
387 let buckets = build_attributed_clone_buckets(report, &mut key_for_path);
388 let mut groups: Vec<DuplicationGroup> = buckets
389 .into_iter()
390 .map(|(key, groups)| duplication_group(key, groups, report, &fingerprints))
391 .collect();
392 sort_duplication_groups(&mut groups);
393
394 DuplicationGrouping { mode, groups }
395}
396
397fn build_attributed_clone_buckets<F>(
398 report: &DuplicationReport,
399 key_for_path: &mut F,
400) -> BTreeMap<String, Vec<AttributedCloneGroup>>
401where
402 F: FnMut(&Path) -> String,
403{
404 let mut buckets: BTreeMap<String, Vec<AttributedCloneGroup>> = BTreeMap::new();
405 for group in &report.clone_groups {
406 let attributed = attributed_clone_group(group, key_for_path);
407 buckets
408 .entry(attributed.primary_owner.clone())
409 .or_default()
410 .push(attributed);
411 }
412 buckets
413}
414
415fn attributed_clone_group<F>(group: &CloneGroup, key_for_path: &mut F) -> AttributedCloneGroup
416where
417 F: FnMut(&Path) -> String,
418{
419 let primary_owner = largest_clone_group_owner_with(group, &mut *key_for_path);
420 let instances = group
421 .instances
422 .iter()
423 .map(|instance| AttributedInstance {
424 owner: key_for_path(&instance.file),
425 instance: instance.clone(),
426 })
427 .collect();
428 AttributedCloneGroup {
429 primary_owner,
430 token_count: group.token_count,
431 line_count: group.line_count,
432 instances,
433 }
434}
435
436fn duplication_group(
437 key: String,
438 attributed_groups: Vec<AttributedCloneGroup>,
439 report: &DuplicationReport,
440 fingerprints: &CloneFingerprintSet,
441) -> DuplicationGroup {
442 let mut subset = duplication_subset_report(&attributed_groups, report);
443 subset.stats = fallow_engine::duplicates::recompute_stats(&subset);
444 let clone_families = clone_families_for_bucket(&attributed_groups, report, fingerprints);
445 let clone_groups = attributed_groups
446 .into_iter()
447 .map(|group| {
448 let fingerprint = group.fingerprint(fingerprints);
449 AttributedCloneGroupFinding::with_fingerprint(group, fingerprint)
450 })
451 .collect();
452
453 DuplicationGroup {
454 key,
455 stats: subset.stats,
456 clone_groups,
457 clone_families,
458 }
459}
460
461fn duplication_subset_report(
462 attributed_groups: &[AttributedCloneGroup],
463 report: &DuplicationReport,
464) -> DuplicationReport {
465 DuplicationReport {
466 clone_groups: attributed_groups
467 .iter()
468 .map(|group| CloneGroup {
469 instances: group
470 .instances
471 .iter()
472 .map(|instance| instance.instance.clone())
473 .collect(),
474 token_count: group.token_count,
475 line_count: group.line_count,
476 })
477 .collect(),
478 clone_families: Vec::new(),
479 mirrored_directories: Vec::new(),
480 stats: DuplicationStats {
481 total_files: report.stats.total_files,
482 files_with_clones: 0,
483 total_lines: report.stats.total_lines,
484 duplicated_lines: 0,
485 total_tokens: report.stats.total_tokens,
486 duplicated_tokens: 0,
487 clone_groups: 0,
488 clone_instances: 0,
489 duplication_percentage: 0.0,
490 clone_groups_below_min_occurrences: report.stats.clone_groups_below_min_occurrences,
491 },
492 }
493}
494
495fn clone_families_for_bucket(
496 attributed_groups: &[AttributedCloneGroup],
497 report: &DuplicationReport,
498 fingerprints: &CloneFingerprintSet,
499) -> Vec<CloneFamilyFinding> {
500 let bucket_files: FxHashSet<&Path> = attributed_groups
501 .iter()
502 .flat_map(|group| group.instances.iter().map(|i| i.instance.file.as_path()))
503 .collect();
504
505 report
506 .clone_families
507 .iter()
508 .filter(|family| {
509 family
510 .files
511 .iter()
512 .any(|path| bucket_files.contains(path.as_path()))
513 })
514 .map(|family| CloneFamilyFinding::with_fingerprints(family.clone(), fingerprints))
515 .collect()
516}
517
518fn sort_duplication_groups(groups: &mut [DuplicationGroup]) {
519 groups.sort_by(|a, b| {
520 let a_unowned = a.key == UNOWNED_GROUP_LABEL;
521 let b_unowned = b.key == UNOWNED_GROUP_LABEL;
522 match (a_unowned, b_unowned) {
523 (true, false) => std::cmp::Ordering::Greater,
524 (false, true) => std::cmp::Ordering::Less,
525 _ => b
526 .clone_groups
527 .len()
528 .cmp(&a.clone_groups.len())
529 .then_with(|| a.key.cmp(&b.key)),
530 }
531 });
532}