Skip to main content

dry4rust/cli/
check_command.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::Write;
7
8use crate::analysis::AnalysisResult;
9use crate::cli::checking::ceiling::Ceiling;
10use crate::cli::checking::check_thresholds::CheckThresholds;
11use crate::cli::cli_error::CliError;
12use crate::cli::cli_error::CliResult;
13use crate::config::Config;
14use crate::grouper::DuplicateGroup;
15use crate::output::check_breach::CheckBreach;
16use crate::output::reporter::Reporter;
17
18/// `check`: the summary, then a non-zero exit if any ceiling is breached.
19///
20/// A ceiling given on the command line wins over the same ceiling in the
21/// config file, and one set in neither place cannot be breached.
22pub struct CheckCommand<'a> {
23    config: &'a Config,
24    result: &'a AnalysisResult,
25    reporter: &'a dyn Reporter,
26    thresholds: &'a CheckThresholds,
27}
28
29impl<'a> CheckCommand<'a> {
30    #[must_use]
31    pub const fn new(
32        config: &'a Config,
33        result: &'a AnalysisResult,
34        reporter: &'a dyn Reporter,
35        thresholds: &'a CheckThresholds,
36    ) -> Self {
37        Self {
38            config,
39            result,
40            reporter,
41            thresholds,
42        }
43    }
44
45    /// Report, then measure against every ceiling.
46    ///
47    /// Every breach is reported, not just the first, so one run tells the
48    /// whole story.
49    ///
50    /// # Errors
51    ///
52    /// Returns [`CliError::CheckFailed`] if any ceiling was breached, and
53    /// [`CliError::Io`] if the writer fails.
54    pub fn run(&self, writer: &mut impl Write) -> CliResult {
55        let breaches = self.breaches();
56        self.reporter
57            .report_check(&self.result.stats, &breaches, writer)?;
58        if breaches.is_empty() {
59            Ok(())
60        } else {
61            Err(CliError::CheckFailed)
62        }
63    }
64
65    fn breaches(&self) -> Vec<CheckBreach<'a>> {
66        self.ceilings()
67            .into_iter()
68            .filter_map(|(ceiling, of_exact)| {
69                ceiling
70                    .breach()
71                    .map(|message| CheckBreach::new(message, self.offenders(of_exact), of_exact))
72            })
73            .collect()
74    }
75
76    const fn offenders(&self, of_exact: bool) -> &'a [DuplicateGroup] {
77        if of_exact {
78            self.result.exact_groups.as_slice()
79        } else {
80            self.result.near_groups.as_slice()
81        }
82    }
83
84    fn ceilings(&self) -> [(Ceiling, bool); 4] {
85        let stats = &self.result.stats;
86        [
87            (
88                Ceiling::count(
89                    self.thresholds
90                        .max_exact
91                        .or(self.config.max_exact_duplicates),
92                    stats.exact_duplicate_groups,
93                    "exact duplicate groups",
94                ),
95                true,
96            ),
97            (
98                Ceiling::count(
99                    self.thresholds.max_near.or(self.config.max_near_duplicates),
100                    stats.near_duplicate_groups,
101                    "near duplicate groups",
102                ),
103                false,
104            ),
105            (
106                Ceiling::percent(
107                    self.thresholds
108                        .max_exact_percent
109                        .or(self.config.max_exact_percent),
110                    stats.exact_duplicate_percent(),
111                    "exact duplicate lines",
112                ),
113                true,
114            ),
115            (
116                Ceiling::percent(
117                    self.thresholds
118                        .max_near_percent
119                        .or(self.config.max_near_percent),
120                    stats.near_duplicate_percent(),
121                    "near duplicate lines",
122                ),
123                false,
124            ),
125        ]
126    }
127}