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