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}