Skip to main content

mago_reporting/
reporter.rs

1//! Issue reporter and output formatting.
2//!
3//! This module provides the core reporter functionality that formats and outputs
4//! issues in various formats. It supports multiple output targets (stdout/stderr),
5//! different formatting styles (rich, medium, short, JSON, etc.), and optional
6//! pagination for terminal output.
7//!
8//! The reporter can filter issues based on baseline files and severity levels,
9//! and can sort issues for better readability.
10
11use std::io::Write;
12
13use mago_database::ReadDatabase;
14
15use crate::IssueCollection;
16use crate::Level;
17use crate::baseline::Baseline;
18use crate::color::ColorChoice;
19use crate::error::ReportingError;
20use crate::formatter::FormatterConfig;
21use crate::formatter::ReportingFormat;
22use crate::formatter::dispatch_format;
23use crate::output::ReportingTarget;
24
25/// Configuration options for the reporter.
26///
27/// This struct controls how issues are formatted and displayed, including
28/// the output target, format style, color usage, and filtering options.
29#[derive(Debug)]
30pub struct ReporterConfig {
31    /// The target where the report will be sent.
32    pub target: ReportingTarget,
33
34    /// The format to use for the report output.
35    pub format: ReportingFormat,
36
37    /// Color choice for the report output.
38    pub color_choice: ColorChoice,
39
40    /// Filter the output to only show issues that can be automatically fixed.
41    ///
42    /// When enabled, only issues that have available automatic fixes will be displayed.
43    /// This is useful when you want to focus on issues that can be resolved immediately.
44    pub filter_fixable: bool,
45
46    /// Sort reported issues by severity level, rule code, and file location.
47    ///
48    /// By default, issues are reported in the order they appear in files.
49    /// This option provides a more organized view for reviewing large numbers of issues.
50    pub sort: bool,
51
52    /// the minimum issue severity to be shown in the report.
53    ///
54    /// Issues below this level will be completely ignored and not displayed.
55    pub minimum_report_level: Option<Level>,
56}
57
58/// Status information returned after reporting issues.
59///
60/// This struct provides detailed statistics about the reporting operation,
61/// including baseline filtering results and severity level information.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct ReportStatus {
64    /// Indicates whether the baseline contains dead issues.
65    pub baseline_dead_issues: bool,
66
67    /// The number of issues that were filtered out by the baseline.
68    pub baseline_filtered_issues: usize,
69
70    /// The highest severity level among the reported issues.
71    pub highest_reported_level: Option<Level>,
72
73    /// The lowest severity level among the reported issues.
74    pub lowest_reported_level: Option<Level>,
75
76    /// The total number of issues reported.
77    pub total_reported_issues: usize,
78}
79
80/// The main reporter that handles formatting and outputting issues.
81///
82/// The reporter takes a collection of issues and outputs them according to
83/// the configured format and options. It can apply baseline filtering,
84/// severity filtering, and sorting before output.
85#[derive(Debug)]
86pub struct Reporter {
87    database: ReadDatabase,
88    config: ReporterConfig,
89}
90
91impl Reporter {
92    /// Create a new reporter with the given database and configuration.
93    #[must_use]
94    pub fn new(database: ReadDatabase, config: ReporterConfig) -> Self {
95        Self { database, config }
96    }
97
98    /// Report issues to the configured target.
99    ///
100    /// This method applies baseline filtering, severity filtering, and sorting
101    /// based on the reporter configuration, then formats and outputs the issues.
102    ///
103    /// # Errors
104    ///
105    /// Returns a [`ReportingError`] if formatting or writing the issues fails.
106    pub fn report(&self, issues: IssueCollection, baseline: Option<Baseline>) -> Result<ReportStatus, ReportingError> {
107        let mut writer = self.config.target.resolve();
108
109        let mut issues = issues;
110
111        // Apply baseline filtering
112        let mut baseline_has_dead_issues = false;
113        let mut baseline_filtered_issues = 0;
114        if let Some(baseline) = baseline {
115            let original_count = issues.len();
116            let comparison = baseline.compare_with_issues(&issues, &self.database);
117            let filtered_issues = baseline.filter_issues(issues, &self.database);
118
119            baseline_filtered_issues = original_count - filtered_issues.len();
120            baseline_has_dead_issues = comparison.removed_issues_count > 0;
121            issues = filtered_issues;
122        }
123
124        // Track reported issue stats before formatting
125        let total_reported_issues = issues.len();
126        let highest_reported_level = issues.get_highest_level();
127        let lowest_reported_level = issues.get_lowest_level();
128
129        // Early return if no issues to report
130        if total_reported_issues == 0 {
131            return Ok(ReportStatus {
132                baseline_dead_issues: baseline_has_dead_issues,
133                baseline_filtered_issues,
134                highest_reported_level: None,
135                lowest_reported_level: None,
136                total_reported_issues: 0,
137            });
138        }
139
140        // Build formatter config
141        let formatter_config = FormatterConfig {
142            color_choice: self.config.color_choice,
143            sort: self.config.sort,
144            minimum_level: self.config.minimum_report_level,
145            filter_fixable: self.config.filter_fixable,
146        };
147
148        // Dispatch to the appropriate formatter
149        dispatch_format(self.config.format, &mut *writer, &issues, &self.database, &formatter_config)?;
150
151        Ok(ReportStatus {
152            baseline_dead_issues: baseline_has_dead_issues,
153            baseline_filtered_issues,
154            highest_reported_level,
155            lowest_reported_level,
156            total_reported_issues,
157        })
158    }
159
160    /// Report issues to a custom writer.
161    ///
162    /// This method allows writing to any `Write` implementation, making it useful
163    /// for testing, capturing output to strings, writing to files, or streaming
164    /// over network sockets.
165    ///
166    /// # Errors
167    ///
168    /// Returns a [`ReportingError`] if formatting or writing the issues fails.
169    ///
170    /// # Examples
171    ///
172    /// ```ignore
173    /// // Write to a buffer for testing
174    /// let mut buffer = Vec::new();
175    /// reporter.report_to(issues, None, &mut buffer)?;
176    /// let output = String::from_utf8(buffer)?;
177    ///
178    /// // Write to a file
179    /// let mut file = File::create("report.txt")?;
180    /// reporter.report_to(issues, None, &mut file)?;
181    /// ```
182    pub fn report_to<W: Write>(
183        &self,
184        issues: IssueCollection,
185        baseline: Option<Baseline>,
186        writer: &mut W,
187    ) -> Result<ReportStatus, ReportingError> {
188        let mut issues = issues;
189
190        // Apply baseline filtering
191        let mut baseline_has_dead_issues = false;
192        let mut baseline_filtered_issues = 0;
193        if let Some(baseline) = baseline {
194            let original_count = issues.len();
195            let comparison = baseline.compare_with_issues(&issues, &self.database);
196            let filtered_issues = baseline.filter_issues(issues, &self.database);
197
198            baseline_filtered_issues = original_count - filtered_issues.len();
199            baseline_has_dead_issues = comparison.removed_issues_count > 0;
200            issues = filtered_issues;
201        }
202
203        // Track reported issue stats before formatting
204        let total_reported_issues = issues.len();
205        let highest_reported_level = issues.get_highest_level();
206        let lowest_reported_level = issues.get_lowest_level();
207
208        // Early return if no issues to report
209        if total_reported_issues == 0 {
210            return Ok(ReportStatus {
211                baseline_dead_issues: baseline_has_dead_issues,
212                baseline_filtered_issues,
213                highest_reported_level: None,
214                lowest_reported_level: None,
215                total_reported_issues: 0,
216            });
217        }
218
219        // Build formatter config
220        let formatter_config = FormatterConfig {
221            color_choice: self.config.color_choice,
222            sort: self.config.sort,
223            minimum_level: self.config.minimum_report_level,
224            filter_fixable: self.config.filter_fixable,
225        };
226
227        // Dispatch to the appropriate formatter
228        dispatch_format(self.config.format, writer, &issues, &self.database, &formatter_config)?;
229
230        Ok(ReportStatus {
231            baseline_dead_issues: baseline_has_dead_issues,
232            baseline_filtered_issues,
233            highest_reported_level,
234            lowest_reported_level,
235            total_reported_issues,
236        })
237    }
238}