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 /// Optional editor URL template for OSC 8 terminal hyperlinks on file paths.
58 ///
59 /// Supported placeholders: `%file%` (absolute path), `%line%`, `%column%`.
60 /// Example: `"phpstorm://open?file=%file%&line=%line%"`
61 pub editor_url: Option<String>,
62}
63
64/// Status information returned after reporting issues.
65///
66/// This struct provides detailed statistics about the reporting operation,
67/// including baseline filtering results and severity level information.
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub struct ReportStatus {
70 /// Indicates whether the baseline contains dead issues.
71 pub baseline_dead_issues: bool,
72
73 /// The number of issues that were filtered out by the baseline.
74 pub baseline_filtered_issues: usize,
75
76 /// The highest severity level among the reported issues.
77 pub highest_reported_level: Option<Level>,
78
79 /// The lowest severity level among the reported issues.
80 pub lowest_reported_level: Option<Level>,
81
82 /// The total number of issues reported.
83 pub total_reported_issues: usize,
84}
85
86/// The main reporter that handles formatting and outputting issues.
87///
88/// The reporter takes a collection of issues and outputs them according to
89/// the configured format and options. It can apply baseline filtering,
90/// severity filtering, and sorting before output.
91#[derive(Debug)]
92pub struct Reporter {
93 database: ReadDatabase,
94 config: ReporterConfig,
95}
96
97impl Reporter {
98 /// Create a new reporter with the given database and configuration.
99 #[must_use]
100 pub fn new(database: ReadDatabase, config: ReporterConfig) -> Self {
101 Self { database, config }
102 }
103
104 /// Report issues to the configured target.
105 ///
106 /// This method applies baseline filtering, severity filtering, and sorting
107 /// based on the reporter configuration, then formats and outputs the issues.
108 ///
109 /// # Errors
110 ///
111 /// Returns a [`ReportingError`] if formatting or writing the issues fails.
112 pub fn report(&self, issues: IssueCollection, baseline: Option<Baseline>) -> Result<ReportStatus, ReportingError> {
113 let mut writer = self.config.target.resolve();
114
115 let mut issues = issues;
116
117 // Apply baseline filtering
118 let mut baseline_has_dead_issues = false;
119 let mut baseline_filtered_issues = 0;
120 if let Some(baseline) = baseline {
121 let original_count = issues.len();
122 let comparison = baseline.compare_with_issues(&issues, &self.database);
123 let filtered_issues = baseline.filter_issues(issues, &self.database);
124
125 baseline_filtered_issues = original_count - filtered_issues.len();
126 baseline_has_dead_issues = comparison.removed_issues_count > 0;
127 issues = filtered_issues;
128 }
129
130 // Track reported issue stats before formatting
131 let total_reported_issues = issues.len();
132 let highest_reported_level = issues.get_highest_level();
133 let lowest_reported_level = issues.get_lowest_level();
134
135 // Early return if no issues to report
136 if total_reported_issues == 0 {
137 return Ok(ReportStatus {
138 baseline_dead_issues: baseline_has_dead_issues,
139 baseline_filtered_issues,
140 highest_reported_level: None,
141 lowest_reported_level: None,
142 total_reported_issues: 0,
143 });
144 }
145
146 // Build formatter config
147 let formatter_config = FormatterConfig {
148 color_choice: self.config.color_choice,
149 sort: self.config.sort,
150 minimum_level: self.config.minimum_report_level,
151 filter_fixable: self.config.filter_fixable,
152 editor_url: self.config.editor_url.clone(),
153 };
154
155 // Dispatch to the appropriate formatter
156 dispatch_format(self.config.format, &mut *writer, &issues, &self.database, &formatter_config)?;
157 // When writing to pipes, some formatters do not flush the last line of json
158 writer.flush()?;
159
160 Ok(ReportStatus {
161 baseline_dead_issues: baseline_has_dead_issues,
162 baseline_filtered_issues,
163 highest_reported_level,
164 lowest_reported_level,
165 total_reported_issues,
166 })
167 }
168
169 /// Report issues to a custom writer.
170 ///
171 /// This method allows writing to any `Write` implementation, making it useful
172 /// for testing, capturing output to strings, writing to files, or streaming
173 /// over network sockets.
174 ///
175 /// # Errors
176 ///
177 /// Returns a [`ReportingError`] if formatting or writing the issues fails.
178 ///
179 /// # Examples
180 ///
181 /// ```ignore
182 /// // Write to a buffer for testing
183 /// let mut buffer = Vec::new();
184 /// reporter.report_to(issues, None, &mut buffer)?;
185 /// let output = String::from_utf8(buffer)?;
186 ///
187 /// // Write to a file
188 /// let mut file = File::create("report.txt")?;
189 /// reporter.report_to(issues, None, &mut file)?;
190 /// ```
191 pub fn report_to<W: Write>(
192 &self,
193 issues: IssueCollection,
194 baseline: Option<Baseline>,
195 writer: &mut W,
196 ) -> Result<ReportStatus, ReportingError> {
197 let mut issues = issues;
198
199 // Apply baseline filtering
200 let mut baseline_has_dead_issues = false;
201 let mut baseline_filtered_issues = 0;
202 if let Some(baseline) = baseline {
203 let original_count = issues.len();
204 let comparison = baseline.compare_with_issues(&issues, &self.database);
205 let filtered_issues = baseline.filter_issues(issues, &self.database);
206
207 baseline_filtered_issues = original_count - filtered_issues.len();
208 baseline_has_dead_issues = comparison.removed_issues_count > 0;
209 issues = filtered_issues;
210 }
211
212 // Track reported issue stats before formatting
213 let total_reported_issues = issues.len();
214 let highest_reported_level = issues.get_highest_level();
215 let lowest_reported_level = issues.get_lowest_level();
216
217 // Early return if no issues to report
218 if total_reported_issues == 0 {
219 return Ok(ReportStatus {
220 baseline_dead_issues: baseline_has_dead_issues,
221 baseline_filtered_issues,
222 highest_reported_level: None,
223 lowest_reported_level: None,
224 total_reported_issues: 0,
225 });
226 }
227
228 // Build formatter config
229 let formatter_config = FormatterConfig {
230 color_choice: self.config.color_choice,
231 sort: self.config.sort,
232 minimum_level: self.config.minimum_report_level,
233 filter_fixable: self.config.filter_fixable,
234 editor_url: self.config.editor_url.clone(),
235 };
236
237 // Dispatch to the appropriate formatter
238 dispatch_format(self.config.format, writer, &issues, &self.database, &formatter_config)?;
239
240 Ok(ReportStatus {
241 baseline_dead_issues: baseline_has_dead_issues,
242 baseline_filtered_issues,
243 highest_reported_level,
244 lowest_reported_level,
245 total_reported_issues,
246 })
247 }
248}