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