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::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    /// Base path for displaying relative paths.
29    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}