1use std::collections::HashSet;
7use std::path::PathBuf;
8
9use crate::analyzer::LanguageAnalyzer;
10use crate::code_unit::CodeUnit;
11use crate::config::Config;
12use crate::error::Result;
13use crate::extractor::extract_sub_units;
14use crate::fingerprint::Fingerprint;
15use crate::grouper::compute_stats_with_sub;
16use crate::grouper::find_near_duplicates;
17use crate::grouper::group_exact_duplicates;
18use crate::grouper::{DuplicateGroup, DuplicationStats};
19use crate::node::NormalizedNode;
20use crate::suppression::baseline_filter::BaselineFilter;
21use crate::suppression::baseline_kind::BaselineKind;
22use crate::suppression::ignore_file::IgnoreFile;
23use std::fs;
24
25pub struct AnalysisResult {
27 pub stats: DuplicationStats,
28 pub exact_groups: Vec<DuplicateGroup>,
29 pub near_groups: Vec<DuplicateGroup>,
30 pub sub_exact_groups: Vec<DuplicateGroup>,
31 pub sub_near_groups: Vec<DuplicateGroup>,
32 pub warnings: Vec<String>,
33 pub all_fingerprints: HashSet<Fingerprint>,
36}
37
38pub fn analyze(
43 analyzer: &dyn LanguageAnalyzer,
44 files: &[PathBuf],
45 config: &Config,
46) -> Result<AnalysisResult> {
47 let analysis_config = config.analysis_config();
48 let mut units = Vec::new();
49 let mut warnings = Vec::new();
50
51 for path in files {
52 let source = match fs::read_to_string(path) {
53 Ok(s) => s,
54 Err(e) => {
55 warnings.push(format!("Failed to read {}: {}", path.display(), e));
56 continue;
57 }
58 };
59 match analyzer.parse_file(path, &source, &analysis_config) {
60 Ok(mut file_units) => {
61 if config.exclude_tests {
62 file_units.retain(|u| !analyzer.is_test_code(u));
63 }
64 units.extend(file_units);
65 }
66 Err(e) => warnings.push(e.to_string()),
67 }
68 }
69
70 analyze_units(&units, warnings, config)
71}
72
73pub fn analyze_units(
78 units: &[CodeUnit],
79 warnings: Vec<String>,
80 config: &Config,
81) -> Result<AnalysisResult> {
82 let exact_groups = group_exact_duplicates(units);
84
85 let exact_fps: Vec<_> = exact_groups.iter().map(|g| g.fingerprint).collect();
87 let near_groups =
88 find_near_duplicates(units, config.similarity_threshold.as_fraction(), &exact_fps);
89
90 let (sub_exact_groups, sub_near_groups) = if config.sub_function {
92 let sub_units: Vec<CodeUnit> = units
94 .iter()
95 .flat_map(|unit| {
96 let sub_units = extract_sub_units(&unit.body, config.min_sub_nodes);
97 sub_units.into_iter().map(|su| CodeUnit {
98 kind: su.kind,
99 name: su.description,
100 file: unit.file.clone(),
101 line_start: unit.line_start,
102 line_end: unit.line_end,
103 signature: NormalizedNode::leaf(crate::node::NodeKind::Opaque),
104 body: su.node.clone(),
105 fingerprint: Fingerprint::from_node(&su.node),
106 node_count: su.node_count,
107 parent_name: Some(unit.name.clone()),
108 is_test: unit.is_test,
109 })
110 })
111 .collect();
112
113 let sub_exact = group_exact_duplicates(&sub_units);
114 let sub_exact_fps: Vec<_> = sub_exact.iter().map(|g| g.fingerprint).collect();
115 let sub_near = find_near_duplicates(
116 &sub_units,
117 config.similarity_threshold.as_fraction(),
118 &sub_exact_fps,
119 );
120 (sub_exact, sub_near)
121 } else {
122 (Vec::new(), Vec::new())
123 };
124
125 let all_fingerprints: HashSet<Fingerprint> = exact_groups
127 .iter()
128 .chain(near_groups.iter())
129 .chain(sub_exact_groups.iter())
130 .chain(sub_near_groups.iter())
131 .map(|g| g.fingerprint)
132 .collect();
133
134 let ignore_file = IgnoreFile::load(&config.root);
136 let exact_groups = ignore_file.retain_unsuppressed(exact_groups);
137 let near_groups = ignore_file.retain_unsuppressed(near_groups);
138 let sub_exact_groups = ignore_file.retain_unsuppressed(sub_exact_groups);
139 let sub_near_groups = ignore_file.retain_unsuppressed(sub_near_groups);
140
141 let baseline = BaselineFilter::load(config)?;
143 let found =
144 exact_groups.len() + near_groups.len() + sub_exact_groups.len() + sub_near_groups.len();
145 let exact_groups = baseline.retain_new(BaselineKind::Exact, exact_groups);
146 let near_groups = baseline.retain_new(BaselineKind::Near, near_groups);
147 let sub_exact_groups = baseline.retain_new(BaselineKind::SubExact, sub_exact_groups);
148 let sub_near_groups = baseline.retain_new(BaselineKind::SubNear, sub_near_groups);
149 let reported =
150 exact_groups.len() + near_groups.len() + sub_exact_groups.len() + sub_near_groups.len();
151
152 let stats = compute_stats_with_sub(
154 units,
155 &exact_groups,
156 &near_groups,
157 &sub_exact_groups,
158 &sub_near_groups,
159 )
160 .with_baseline_suppressed(baseline.is_in_effect().then_some(found - reported));
161
162 Ok(AnalysisResult {
163 stats,
164 exact_groups,
165 near_groups,
166 sub_exact_groups,
167 sub_near_groups,
168 warnings,
169 all_fingerprints,
170 })
171}