1use std::fmt::Write as _;
7use std::io;
8
9use crate::grouper::{DuplicateGroup, DuplicationStats};
10use crate::output::check_breach::CheckBreach;
11use crate::output::group_section::GroupSection;
12use crate::output::report::Report;
13use crate::output::reporter::Reporter;
14use std::path::PathBuf;
15
16fn format_with_commas(n: usize) -> String {
17 let s = n.to_string();
18 let mut result = String::with_capacity(s.len() + s.len() / 3);
19 for (i, c) in s.chars().enumerate() {
20 if i > 0 && (s.len() - i).is_multiple_of(3) {
21 result.push(',');
22 }
23 result.push(c);
24 }
25 result
26}
27
28pub struct TextReporter {
29 pub base_path: Option<PathBuf>,
31}
32
33impl TextReporter {
34 #[must_use]
35 pub const fn new(base_path: Option<PathBuf>) -> Self {
36 Self { base_path }
37 }
38
39 fn section(&self, section: &GroupSection, groups: &[DuplicateGroup]) -> String {
40 section.write(groups, self.base_path.as_deref())
41 }
42}
43
44impl Reporter for TextReporter {
45 fn report(&self, report: &Report<'_>) -> io::Result<String> {
46 let mut out = self.report_stats(report.stats)?;
47 let _ = writeln!(out);
48 out.push_str(&self.report_exact(report.exact)?);
49 if !report.near.is_empty() {
50 out.push_str(&self.report_near(report.near)?);
51 }
52 if !report.sub_exact.is_empty() {
53 out.push_str(&self.report_sub_exact(report.sub_exact)?);
54 }
55 if !report.sub_near.is_empty() {
56 out.push_str(&self.report_sub_near(report.sub_near)?);
57 }
58 Ok(out)
59 }
60
61 fn report_check(
62 &self,
63 stats: &DuplicationStats,
64 breaches: &[CheckBreach<'_>],
65 ) -> io::Result<String> {
66 let mut out = self.report_stats(stats)?;
67 for breach in breaches {
68 let _ = writeln!(out, "\nCheck FAILED: {}", breach.message());
69 if breach.is_of_exact() {
70 out.push_str(&self.report_exact(breach.groups())?);
71 } else {
72 out.push_str(&self.report_near(breach.groups())?);
73 }
74 }
75 if breaches.is_empty() {
76 let _ = writeln!(out, "\nCheck passed.");
77 }
78 Ok(out)
79 }
80
81 fn report_stats(&self, stats: &DuplicationStats) -> io::Result<String> {
82 let mut out = String::new();
83 let _ = writeln!(out, "Duplication Statistics");
84 let _ = writeln!(out, "=====================");
85 let _ = writeln!(out, "Total code units analyzed: {}", stats.total_code_units);
86 let _ = writeln!(out);
87 let _ = writeln!(
88 out,
89 "Exact duplicates: {} groups ({} code units)",
90 stats.exact_duplicate_groups, stats.exact_duplicate_units
91 );
92 let _ = writeln!(
93 out,
94 "Near duplicates: {} groups ({} code units)",
95 stats.near_duplicate_groups, stats.near_duplicate_units
96 );
97 let _ = writeln!(out);
98 let _ = writeln!(
99 out,
100 "Duplicated lines (exact): {}",
101 stats.exact_duplicate_lines
102 );
103 let _ = writeln!(
104 out,
105 "Duplicated lines (near): {}",
106 stats.near_duplicate_lines
107 );
108 let _ = writeln!(
109 out,
110 "Duplication: {:.1}% exact, {:.1}% near (of {} total lines)",
111 stats.exact_duplicate_percent(),
112 stats.near_duplicate_percent(),
113 format_with_commas(stats.total_lines),
114 );
115 if let Some(suppressed) = stats.baseline_suppressed {
116 let _ = writeln!(out, "Baseline: {suppressed} groups suppressed");
117 }
118 if stats.sub_exact_groups > 0 || stats.sub_near_groups > 0 {
119 let _ = writeln!(out);
120 let _ = writeln!(
121 out,
122 "Sub-function exact: {} groups ({} units)",
123 stats.sub_exact_groups, stats.sub_exact_units
124 );
125 let _ = writeln!(
126 out,
127 "Sub-function near: {} groups ({} units)",
128 stats.sub_near_groups, stats.sub_near_units
129 );
130 }
131 Ok(out)
132 }
133
134 fn report_exact(&self, groups: &[DuplicateGroup]) -> io::Result<String> {
135 Ok(self.section(&GroupSection::exact(), groups))
136 }
137
138 fn report_near(&self, groups: &[DuplicateGroup]) -> io::Result<String> {
139 Ok(self.section(&GroupSection::near(), groups))
140 }
141
142 fn report_sub_exact(&self, groups: &[DuplicateGroup]) -> io::Result<String> {
143 Ok(self.section(&GroupSection::sub_exact(), groups))
144 }
145
146 fn report_sub_near(&self, groups: &[DuplicateGroup]) -> io::Result<String> {
147 Ok(self.section(&GroupSection::sub_near(), groups))
148 }
149}