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).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).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.clone(),
81 severity: violation.severity.to_string(),
82 message: violation.message.clone(),
83 file: violation.file.clone(),
84 line,
85 column,
86 suggestion: violation.suggestion.clone(),
87 }
88 })
89 .collect();
90
91 let summary = Summary::from_violations(violations);
92 let output = JsonOutput {
93 violations: json_violations,
94 summary,
95 };
96
97 serde_json::to_string_pretty(&output).unwrap_or_default()
98 }
99}
100
101fn calculate_line_column(source: &str, offset: usize) -> (usize, usize) {
104 let mut line = 1;
105 let mut column = 1;
106
107 for (pos, ch) in source.char_indices() {
108 if pos >= offset {
109 break;
110 }
111 if ch == '\n' {
112 line += 1;
113 column = 1;
114 } else {
115 column += 1;
116 }
117 }
118
119 (line, column)
120}
121
122#[derive(Debug, Clone)]
123struct ViolationDiagnostic {
124 violation: Violation,
125 source_code: String,
126}
127
128impl fmt::Display for ViolationDiagnostic {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 write!(f, "{}", self.violation.message)
131 }
132}
133
134impl std::error::Error for ViolationDiagnostic {}
135
136impl Diagnostic for ViolationDiagnostic {
137 fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
138 Some(Box::new(format!(
139 "{}({})",
140 self.violation.severity, self.violation.rule_id
141 )))
142 }
143
144 fn severity(&self) -> Option<miette::Severity> {
145 Some(match self.violation.severity {
146 Severity::Error => miette::Severity::Error,
147 Severity::Warning => miette::Severity::Warning,
148 Severity::Info => miette::Severity::Advice,
149 })
150 }
151
152 fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
153 self.violation
154 .suggestion
155 .as_deref()
156 .map(|s| Box::new(s) as Box<dyn fmt::Display>)
157 }
158
159 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
160 let span = self.violation.to_source_span();
161 Some(Box::new(std::iter::once(LabeledSpan::new(
162 Some(self.violation.message.clone()),
163 span.offset(),
164 span.len(),
165 ))))
166 }
167
168 fn source_code(&self) -> Option<&dyn SourceCode> {
169 Some(&self.source_code as &dyn SourceCode)
170 }
171}
172
173#[derive(Serialize)]
174pub struct JsonOutput {
175 pub violations: Vec<JsonViolation>,
176 pub summary: Summary,
177}
178
179#[derive(Serialize)]
180pub struct JsonViolation {
181 pub rule_id: String,
182 pub severity: String,
183 pub message: String,
184 pub file: Option<String>,
185 pub line: usize,
186 pub column: usize,
187 pub suggestion: Option<String>,
188}
189
190#[derive(Serialize)]
191pub struct Summary {
192 pub errors: usize,
193 pub warnings: usize,
194 pub info: usize,
195 pub files_checked: usize,
196}
197
198impl Summary {
199 #[must_use]
200 pub fn from_violations(violations: &[Violation]) -> Self {
201 let (errors, warnings, info) =
202 violations
203 .iter()
204 .fold((0, 0, 0), |(e, w, i), v| match v.severity {
205 Severity::Error => (e + 1, w, i),
206 Severity::Warning => (e, w + 1, i),
207 Severity::Info => (e, w, i + 1),
208 });
209
210 Self {
211 errors,
212 warnings,
213 info,
214 files_checked: 1,
215 }
216 }
217}