Skip to main content

dry4rust/output/
text.rs

1// Copyright (c) 2026 Matjaz Domen Pecan
2// Copyright 2026 Umberto Gotti <umberto.gotti@umbertogotti.dev>
3// Licensed under the MIT License
4// SPDX-License-Identifier: MIT
5
6use 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    /// Base path for displaying relative paths.
30    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}