fallow_engine/
cross_reference.rs1use rustc_hash::FxHashSet;
4use serde::Serialize;
5
6use crate::duplicates::{CloneInstance, DuplicationReport};
7use crate::results::AnalysisResults;
8
9#[derive(Debug, Clone, Serialize)]
11pub struct CombinedFinding {
12 pub clone_instance: CloneInstance,
14 pub dead_code_kind: DeadCodeKind,
16 pub group_index: usize,
18}
19
20impl From<fallow_core::cross_reference::CombinedFinding> for CombinedFinding {
21 fn from(finding: fallow_core::cross_reference::CombinedFinding) -> Self {
22 Self {
23 clone_instance: finding.clone_instance,
24 dead_code_kind: finding.dead_code_kind.into(),
25 group_index: finding.group_index,
26 }
27 }
28}
29
30#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
32pub enum DeadCodeKind {
33 UnusedFile,
35 UnusedExport { export_name: String },
37 UnusedType { type_name: String },
39}
40
41impl From<fallow_core::cross_reference::DeadCodeKind> for DeadCodeKind {
42 fn from(kind: fallow_core::cross_reference::DeadCodeKind) -> Self {
43 match kind {
44 fallow_core::cross_reference::DeadCodeKind::UnusedFile => Self::UnusedFile,
45 fallow_core::cross_reference::DeadCodeKind::UnusedExport { export_name } => {
46 Self::UnusedExport { export_name }
47 }
48 fallow_core::cross_reference::DeadCodeKind::UnusedType { type_name } => {
49 Self::UnusedType { type_name }
50 }
51 }
52 }
53}
54
55#[derive(Debug, Clone, Serialize)]
57pub struct CrossReferenceResult {
58 pub combined_findings: Vec<CombinedFinding>,
60 pub clones_in_unused_files: usize,
62 pub clones_with_unused_exports: usize,
64}
65
66impl CrossReferenceResult {
67 #[must_use]
69 pub const fn total(&self) -> usize {
70 self.combined_findings.len()
71 }
72
73 #[must_use]
75 pub const fn has_findings(&self) -> bool {
76 !self.combined_findings.is_empty()
77 }
78
79 #[must_use]
81 pub fn affected_group_indices(&self) -> FxHashSet<usize> {
82 self.combined_findings
83 .iter()
84 .map(|finding| finding.group_index)
85 .collect()
86 }
87}
88
89impl From<fallow_core::cross_reference::CrossReferenceResult> for CrossReferenceResult {
90 fn from(result: fallow_core::cross_reference::CrossReferenceResult) -> Self {
91 Self {
92 combined_findings: result
93 .combined_findings
94 .into_iter()
95 .map(CombinedFinding::from)
96 .collect(),
97 clones_in_unused_files: result.clones_in_unused_files,
98 clones_with_unused_exports: result.clones_with_unused_exports,
99 }
100 }
101}
102
103#[must_use]
105pub fn cross_reference(
106 duplication: &DuplicationReport,
107 dead_code: &AnalysisResults,
108) -> CrossReferenceResult {
109 fallow_core::cross_reference::cross_reference(duplication, dead_code).into()
110}
111
112#[cfg(test)]
113mod tests {
114 use std::path::PathBuf;
115
116 use super::*;
117
118 fn clone_instance(file: &str, start_line: usize, end_line: usize) -> CloneInstance {
119 CloneInstance {
120 file: PathBuf::from(file),
121 start_line,
122 end_line,
123 start_col: 0,
124 end_col: 0,
125 fragment: String::new(),
126 }
127 }
128
129 #[test]
130 fn cross_reference_result_methods_use_engine_owned_findings() {
131 let result = CrossReferenceResult {
132 combined_findings: vec![
133 CombinedFinding {
134 clone_instance: clone_instance("src/a.ts", 1, 3),
135 dead_code_kind: DeadCodeKind::UnusedFile,
136 group_index: 2,
137 },
138 CombinedFinding {
139 clone_instance: clone_instance("src/b.ts", 4, 8),
140 dead_code_kind: DeadCodeKind::UnusedExport {
141 export_name: "unused".to_string(),
142 },
143 group_index: 4,
144 },
145 ],
146 clones_in_unused_files: 1,
147 clones_with_unused_exports: 1,
148 };
149
150 assert_eq!(result.total(), 2);
151 assert!(result.has_findings());
152 assert!(result.affected_group_indices().contains(&2));
153 assert!(result.affected_group_indices().contains(&4));
154 }
155
156 #[test]
157 fn cross_reference_result_converts_from_core_without_leaking_type() {
158 let result =
159 CrossReferenceResult::from(fallow_core::cross_reference::CrossReferenceResult {
160 combined_findings: vec![fallow_core::cross_reference::CombinedFinding {
161 clone_instance: clone_instance("src/a.ts", 1, 3),
162 dead_code_kind: fallow_core::cross_reference::DeadCodeKind::UnusedType {
163 type_name: "UnusedType".to_string(),
164 },
165 group_index: 7,
166 }],
167 clones_in_unused_files: 0,
168 clones_with_unused_exports: 1,
169 });
170
171 assert_eq!(result.total(), 1);
172 assert_eq!(result.clones_with_unused_exports, 1);
173 assert!(matches!(
174 result.combined_findings[0].dead_code_kind,
175 DeadCodeKind::UnusedType { ref type_name } if type_name == "UnusedType"
176 ));
177 }
178}