use std::collections::HashSet;
use std::path::PathBuf;
use crate::analyzer::LanguageAnalyzer;
use crate::code_unit::CodeUnit;
use crate::config::Config;
use crate::error::Result;
use crate::extractor::extract_sub_units;
use crate::fingerprint::Fingerprint;
use crate::grouper::compute_stats_with_sub;
use crate::grouper::find_near_duplicates;
use crate::grouper::group_exact_duplicates;
use crate::grouper::{DuplicateGroup, DuplicationStats};
use crate::node::NormalizedNode;
use crate::suppression::baseline_filter::BaselineFilter;
use crate::suppression::baseline_kind::BaselineKind;
use crate::suppression::ignore_file::IgnoreFile;
use std::fs;
pub struct AnalysisResult {
pub stats: DuplicationStats,
pub exact_groups: Vec<DuplicateGroup>,
pub near_groups: Vec<DuplicateGroup>,
pub sub_exact_groups: Vec<DuplicateGroup>,
pub sub_near_groups: Vec<DuplicateGroup>,
pub warnings: Vec<String>,
pub all_fingerprints: HashSet<Fingerprint>,
}
pub fn analyze(
analyzer: &dyn LanguageAnalyzer,
files: &[PathBuf],
config: &Config,
) -> Result<AnalysisResult> {
let analysis_config = config.analysis_config();
let mut units = Vec::new();
let mut warnings = Vec::new();
for path in files {
let source = match fs::read_to_string(path) {
Ok(s) => s,
Err(e) => {
warnings.push(format!("Failed to read {}: {}", path.display(), e));
continue;
}
};
match analyzer.parse_file(path, &source, &analysis_config) {
Ok(mut file_units) => {
if config.exclude_tests {
file_units.retain(|u| !analyzer.is_test_code(u));
}
units.extend(file_units);
}
Err(e) => warnings.push(e.to_string()),
}
}
analyze_units(&units, warnings, config)
}
pub fn analyze_units(
units: &[CodeUnit],
warnings: Vec<String>,
config: &Config,
) -> Result<AnalysisResult> {
let exact_groups = group_exact_duplicates(units);
let exact_fps: Vec<_> = exact_groups.iter().map(|g| g.fingerprint).collect();
let near_groups =
find_near_duplicates(units, config.similarity_threshold.as_fraction(), &exact_fps);
let (sub_exact_groups, sub_near_groups) = if config.sub_function {
let sub_units: Vec<CodeUnit> = units
.iter()
.flat_map(|unit| {
let sub_units = extract_sub_units(&unit.body, config.min_sub_nodes);
sub_units.into_iter().map(|su| CodeUnit {
kind: su.kind,
name: su.description,
file: unit.file.clone(),
line_start: unit.line_start,
line_end: unit.line_end,
signature: NormalizedNode::leaf(crate::node::NodeKind::Opaque),
body: su.node.clone(),
fingerprint: Fingerprint::from_node(&su.node),
node_count: su.node_count,
parent_name: Some(unit.name.clone()),
is_test: unit.is_test,
})
})
.collect();
let sub_exact = group_exact_duplicates(&sub_units);
let sub_exact_fps: Vec<_> = sub_exact.iter().map(|g| g.fingerprint).collect();
let sub_near = find_near_duplicates(
&sub_units,
config.similarity_threshold.as_fraction(),
&sub_exact_fps,
);
(sub_exact, sub_near)
} else {
(Vec::new(), Vec::new())
};
let all_fingerprints: HashSet<Fingerprint> = exact_groups
.iter()
.chain(near_groups.iter())
.chain(sub_exact_groups.iter())
.chain(sub_near_groups.iter())
.map(|g| g.fingerprint)
.collect();
let ignore_file = IgnoreFile::load(&config.root);
let exact_groups = ignore_file.retain_unsuppressed(exact_groups);
let near_groups = ignore_file.retain_unsuppressed(near_groups);
let sub_exact_groups = ignore_file.retain_unsuppressed(sub_exact_groups);
let sub_near_groups = ignore_file.retain_unsuppressed(sub_near_groups);
let baseline = BaselineFilter::load(config)?;
let found =
exact_groups.len() + near_groups.len() + sub_exact_groups.len() + sub_near_groups.len();
let exact_groups = baseline.retain_new(BaselineKind::Exact, exact_groups);
let near_groups = baseline.retain_new(BaselineKind::Near, near_groups);
let sub_exact_groups = baseline.retain_new(BaselineKind::SubExact, sub_exact_groups);
let sub_near_groups = baseline.retain_new(BaselineKind::SubNear, sub_near_groups);
let reported =
exact_groups.len() + near_groups.len() + sub_exact_groups.len() + sub_near_groups.len();
let stats = compute_stats_with_sub(
units,
&exact_groups,
&near_groups,
&sub_exact_groups,
&sub_near_groups,
)
.with_baseline_suppressed(baseline.is_in_effect().then_some(found - reported));
Ok(AnalysisResult {
stats,
exact_groups,
near_groups,
sub_exact_groups,
sub_near_groups,
warnings,
all_fingerprints,
})
}