use std::path::PathBuf;
use serde::Serialize;
use crate::serde_path;
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct CloneInstance {
#[serde(serialize_with = "serde_path::serialize")]
pub file: PathBuf,
pub start_line: usize,
pub end_line: usize,
pub start_col: usize,
pub end_col: usize,
pub fragment: String,
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct CloneGroup {
pub instances: Vec<CloneInstance>,
pub token_count: usize,
pub line_count: usize,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum RefactoringKind {
ExtractFunction,
ExtractModule,
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RefactoringSuggestion {
pub kind: RefactoringKind,
pub description: String,
pub estimated_savings: usize,
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct CloneFamily {
#[serde(serialize_with = "serde_path::serialize_vec")]
pub files: Vec<PathBuf>,
pub groups: Vec<CloneGroup>,
pub total_duplicated_lines: usize,
pub total_duplicated_tokens: usize,
pub suggestions: Vec<RefactoringSuggestion>,
}
#[derive(Debug, Clone, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct MirroredDirectory {
pub dir_a: String,
pub dir_b: String,
pub shared_files: Vec<String>,
pub total_lines: usize,
}
#[derive(Debug, Clone, Default)]
pub struct DefaultIgnoreSkipCount {
pub pattern: &'static str,
pub count: usize,
}
#[derive(Debug, Clone, Default)]
pub struct DefaultIgnoreSkips {
pub total: usize,
pub by_pattern: Vec<DefaultIgnoreSkipCount>,
}
#[derive(Debug, Clone, Default, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct DuplicationReport {
pub clone_groups: Vec<CloneGroup>,
pub clone_families: Vec<CloneFamily>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mirrored_directories: Vec<MirroredDirectory>,
pub stats: DuplicationStats,
}
impl DuplicationReport {
pub fn sort(&mut self) {
for group in &mut self.clone_groups {
group
.instances
.sort_by(|a, b| a.file.cmp(&b.file).then(a.start_line.cmp(&b.start_line)));
}
self.clone_groups
.sort_by(|a, b| match (a.instances.first(), b.instances.first()) {
(Some(ai), Some(bi)) => ai
.file
.cmp(&bi.file)
.then(ai.start_line.cmp(&bi.start_line)),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => std::cmp::Ordering::Equal,
});
for family in &mut self.clone_families {
for group in &mut family.groups {
group
.instances
.sort_by(|a, b| a.file.cmp(&b.file).then(a.start_line.cmp(&b.start_line)));
}
family
.groups
.sort_by(|a, b| match (a.instances.first(), b.instances.first()) {
(Some(ai), Some(bi)) => ai
.file
.cmp(&bi.file)
.then(ai.start_line.cmp(&bi.start_line)),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => std::cmp::Ordering::Equal,
});
}
self.clone_families.sort_by(|a, b| a.files.cmp(&b.files));
}
}
#[derive(Debug, Clone, Default, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct DuplicationStats {
pub total_files: usize,
pub files_with_clones: usize,
pub total_lines: usize,
pub duplicated_lines: usize,
pub total_tokens: usize,
pub duplicated_tokens: usize,
pub clone_groups: usize,
pub clone_instances: usize,
pub duplication_percentage: f64,
#[serde(default, skip_serializing_if = "is_zero_usize")]
pub clone_groups_below_min_occurrences: usize,
}
#[expect(
clippy::trivially_copy_pass_by_ref,
reason = "serde skip_serializing_if requires &T signature"
)]
const fn is_zero_usize(value: &usize) -> bool {
*value == 0
}