1use std::{fmt, fmt::Write};
2
3use miette::{Diagnostic, LabeledSpan, Report, SourceCode};
4use serde::Serialize;
5
6use crate::lint::{Severity, Violation};
7#[derive(Debug, Clone, Copy)]
8pub enum OutputFormat {
9 Text,
10 Json,
11 Github,
12}
13
14pub trait OutputFormatter {
15 fn format(&self, violations: &[Violation], source: &str) -> String;
16}
17
18#[derive(Debug, Default)]
19pub struct TextFormatter;
20
21impl OutputFormatter for TextFormatter {
22 fn format(&self, violations: &[Violation], _source: &str) -> String {
23 if violations.is_empty() {
24 return String::from("No violations found!");
25 }
26
27 let mut output = String::new();
28
29 for violation in violations {
30 let source_code = violation
31 .file
32 .as_ref()
33 .and_then(|path| std::fs::read_to_string(path.as_ref()).ok())
34 .unwrap_or_default();
35
36 let (line, column) = calculate_line_column(&source_code, violation.span.start);
37
38 if let Some(file_path) = &violation.file {
39 writeln!(output, "\x1b[1m{file_path}:{line}:{column}\x1b[0m").unwrap();
40 }
41
42 let diagnostic = ViolationDiagnostic {
43 violation: violation.clone(),
44 source_code,
45 };
46
47 let report = Report::new(diagnostic);
48 writeln!(output, "{report:?}").unwrap();
49 }
50
51 let summary = Summary::from_violations(violations);
52 writeln!(
53 output,
54 "\n{} error(s), {} warning(s), {} info",
55 summary.errors, summary.warnings, summary.info
56 )
57 .unwrap();
58
59 output
60 }
61}
62
63#[derive(Debug, Default)]
64pub struct JsonFormatter;
65
66impl OutputFormatter for JsonFormatter {
67 fn format(&self, violations: &[Violation], _source: &str) -> String {
68 let json_violations: Vec<JsonViolation> = violations
69 .iter()
70 .map(|violation| {
71 let source_code = violation
72 .file
73 .as_ref()
74 .and_then(|path| std::fs::read_to_string(path.as_ref()).ok())
75 .unwrap_or_default();
76
77 let (line, column) = calculate_line_column(&source_code, violation.span.start);
78
79 JsonViolation {
80 rule_id: violation.rule_id.to_string(),
81 severity: violation.severity.to_string(),
82 message: violation.message.to_string(),
83 file: violation
84 .file
85 .as_ref()
86 .map(std::string::ToString::to_string),
87 line,
88 column,
89 suggestion: violation
90 .suggestion
91 .as_ref()
92 .map(std::string::ToString::to_string),
93 }
94 })
95 .collect();
96
97 let summary = Summary::from_violations(violations);
98 let output = JsonOutput {
99 violations: json_violations,
100 summary,
101 };
102
103 serde_json::to_string_pretty(&output).unwrap_or_default()
104 }
105}
106
107fn calculate_line_column(source: &str, offset: usize) -> (usize, usize) {
110 let mut line = 1;
111 let mut column = 1;
112
113 for (pos, ch) in source.char_indices() {
114 if pos >= offset {
115 break;
116 }
117 if ch == '\n' {
118 line += 1;
119 column = 1;
120 } else {
121 column += 1;
122 }
123 }
124
125 (line, column)
126}
127
128#[derive(Debug, Clone)]
129struct ViolationDiagnostic {
130 violation: Violation,
131 source_code: String,
132}
133
134impl fmt::Display for ViolationDiagnostic {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 write!(f, "{}", self.violation.message)
137 }
138}
139
140impl std::error::Error for ViolationDiagnostic {}
141
142impl Diagnostic for ViolationDiagnostic {
143 fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
144 Some(Box::new(format!(
145 "{}({})",
146 self.violation.severity, self.violation.rule_id
147 )))
148 }
149
150 fn severity(&self) -> Option<miette::Severity> {
151 Some(match self.violation.severity {
152 Severity::Error => miette::Severity::Error,
153 Severity::Warning => miette::Severity::Warning,
154 Severity::Info => miette::Severity::Advice,
155 })
156 }
157
158 fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
159 self.violation
160 .suggestion
161 .as_deref()
162 .map(|s| Box::new(s) as Box<dyn fmt::Display>)
163 }
164
165 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
166 let span = self.violation.to_source_span();
167 Some(Box::new(std::iter::once(LabeledSpan::new(
168 Some(self.violation.message.to_string()),
169 span.offset(),
170 span.len(),
171 ))))
172 }
173
174 fn source_code(&self) -> Option<&dyn SourceCode> {
175 Some(&self.source_code as &dyn SourceCode)
176 }
177}
178
179#[derive(Serialize)]
180pub struct JsonOutput {
181 pub violations: Vec<JsonViolation>,
182 pub summary: Summary,
183}
184
185#[derive(Serialize)]
186pub struct JsonViolation {
187 pub rule_id: String,
188 pub severity: String,
189 pub message: String,
190 pub file: Option<String>,
191 pub line: usize,
192 pub column: usize,
193 pub suggestion: Option<String>,
194}
195
196#[derive(Serialize)]
197pub struct Summary {
198 pub errors: usize,
199 pub warnings: usize,
200 pub info: usize,
201 pub files_checked: usize,
202}
203
204impl Summary {
205 #[must_use]
206 pub fn from_violations(violations: &[Violation]) -> Self {
207 let (errors, warnings, info) =
208 violations
209 .iter()
210 .fold((0, 0, 0), |(e, w, i), v| match v.severity {
211 Severity::Error => (e + 1, w, i),
212 Severity::Warning => (e, w + 1, i),
213 Severity::Info => (e, w, i + 1),
214 });
215
216 Self {
217 errors,
218 warnings,
219 info,
220 files_checked: 1,
221 }
222 }
223}