Skip to main content

fallow_engine/
cross_reference.rs

1//! Cross-reference helpers exposed through the engine boundary.
2
3use rustc_hash::FxHashSet;
4use serde::Serialize;
5
6use crate::core_backend;
7use crate::duplicates::{CloneInstance, DuplicationReport};
8use crate::results::AnalysisResults;
9
10/// A combined finding where a clone instance overlaps with a dead-code issue.
11#[derive(Debug, Clone, Serialize)]
12pub struct CombinedFinding {
13    /// The clone instance that is also unused.
14    pub clone_instance: CloneInstance,
15    /// What kind of dead code overlaps with this clone.
16    pub dead_code_kind: DeadCodeKind,
17    /// Clone group index for associating with the parent group.
18    pub group_index: usize,
19}
20
21/// The type of dead code that overlaps with a clone instance.
22#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
23pub enum DeadCodeKind {
24    /// The entire file containing the clone is unused.
25    UnusedFile,
26    /// A specific unused export overlaps with the clone's line range.
27    UnusedExport { export_name: String },
28    /// A specific unused type overlaps with the clone's line range.
29    UnusedType { type_name: String },
30}
31
32/// Result of cross-referencing duplication with dead-code analysis.
33#[derive(Debug, Clone, Serialize)]
34pub struct CrossReferenceResult {
35    /// Clone instances that are also dead code.
36    pub combined_findings: Vec<CombinedFinding>,
37    /// Number of clone instances in unused files.
38    pub clones_in_unused_files: usize,
39    /// Number of clone instances overlapping unused exports.
40    pub clones_with_unused_exports: usize,
41}
42
43impl CrossReferenceResult {
44    /// Total number of combined findings.
45    #[must_use]
46    pub const fn total(&self) -> usize {
47        self.combined_findings.len()
48    }
49
50    /// Whether any combined findings exist.
51    #[must_use]
52    pub const fn has_findings(&self) -> bool {
53        !self.combined_findings.is_empty()
54    }
55
56    /// Get clone groups that have at least one combined finding.
57    #[must_use]
58    pub fn affected_group_indices(&self) -> FxHashSet<usize> {
59        self.combined_findings
60            .iter()
61            .map(|finding| finding.group_index)
62            .collect()
63    }
64}
65
66/// Cross-reference duplication findings with dead-code analysis results.
67#[must_use]
68pub fn cross_reference(
69    duplication: &DuplicationReport,
70    dead_code: &AnalysisResults,
71) -> CrossReferenceResult {
72    core_backend::cross_reference(duplication, dead_code)
73}
74
75#[cfg(test)]
76mod tests {
77    use std::path::PathBuf;
78
79    use super::*;
80
81    fn clone_instance(file: &str, start_line: usize, end_line: usize) -> CloneInstance {
82        CloneInstance {
83            file: PathBuf::from(file),
84            start_line,
85            end_line,
86            start_col: 0,
87            end_col: 0,
88            fragment: String::new(),
89        }
90    }
91
92    #[test]
93    fn cross_reference_result_methods_use_engine_owned_findings() {
94        let result = CrossReferenceResult {
95            combined_findings: vec![
96                CombinedFinding {
97                    clone_instance: clone_instance("src/a.ts", 1, 3),
98                    dead_code_kind: DeadCodeKind::UnusedFile,
99                    group_index: 2,
100                },
101                CombinedFinding {
102                    clone_instance: clone_instance("src/b.ts", 4, 8),
103                    dead_code_kind: DeadCodeKind::UnusedExport {
104                        export_name: "unused".to_string(),
105                    },
106                    group_index: 4,
107                },
108            ],
109            clones_in_unused_files: 1,
110            clones_with_unused_exports: 1,
111        };
112
113        assert_eq!(result.total(), 2);
114        assert!(result.has_findings());
115        assert!(result.affected_group_indices().contains(&2));
116        assert!(result.affected_group_indices().contains(&4));
117    }
118}