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