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