1use ariadne::{CharSet, Color, Config, Fmt, IndexType, Label, Report, ReportKind, Source};
2use linguini_syntax::Span;
3use std::fmt;
4use std::io;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum DiagnosticSeverity {
8 Error,
9 Warning,
10 Advice,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct RelatedSpan {
15 pub span: Span,
16 pub message: String,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct QuickFix {
21 pub title: String,
22 pub id: Option<String>,
23 pub replacement: Option<Replacement>,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct Replacement {
28 pub span: Span,
29 pub text: String,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct Diagnostic {
34 pub severity: DiagnosticSeverity,
35 pub message: String,
36 pub span: Span,
37 pub note: Option<String>,
38 pub related: Vec<RelatedSpan>,
39 pub quick_fixes: Vec<QuickFix>,
40 pub show_source: bool,
41}
42
43#[derive(Debug)]
44pub struct RenderError {
45 source: io::Error,
46}
47
48impl fmt::Display for RenderError {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 write!(f, "failed to render diagnostic: {}", self.source)
51 }
52}
53
54impl std::error::Error for RenderError {}
55
56impl Diagnostic {
57 pub fn error(message: impl Into<String>, span: Span) -> Self {
58 Self {
59 severity: DiagnosticSeverity::Error,
60 message: message.into(),
61 span,
62 note: None,
63 related: Vec::new(),
64 quick_fixes: Vec::new(),
65 show_source: true,
66 }
67 }
68
69 pub fn warning(message: impl Into<String>, span: Span) -> Self {
70 Self {
71 severity: DiagnosticSeverity::Warning,
72 message: message.into(),
73 span,
74 note: None,
75 related: Vec::new(),
76 quick_fixes: Vec::new(),
77 show_source: true,
78 }
79 }
80
81 pub fn advice(message: impl Into<String>, span: Span) -> Self {
82 Self {
83 severity: DiagnosticSeverity::Advice,
84 message: message.into(),
85 span,
86 note: None,
87 related: Vec::new(),
88 quick_fixes: Vec::new(),
89 show_source: true,
90 }
91 }
92
93 pub fn with_note(mut self, note: impl Into<String>) -> Self {
94 self.note = Some(note.into());
95 self
96 }
97
98 pub fn with_related(mut self, span: Span, message: impl Into<String>) -> Self {
99 self.related.push(RelatedSpan {
100 span,
101 message: message.into(),
102 });
103 self
104 }
105
106 pub fn with_quick_fix(mut self, quick_fix: QuickFix) -> Self {
107 self.quick_fixes.push(quick_fix);
108 self
109 }
110
111 pub fn without_source(mut self) -> Self {
112 self.show_source = false;
113 self
114 }
115}
116
117impl QuickFix {
118 pub fn hint(title: impl Into<String>) -> Self {
119 Self {
120 title: title.into(),
121 id: None,
122 replacement: None,
123 }
124 }
125
126 pub fn command(id: impl Into<String>, title: impl Into<String>) -> Self {
127 Self {
128 title: title.into(),
129 id: Some(id.into()),
130 replacement: None,
131 }
132 }
133
134 pub fn replacement(title: impl Into<String>, replacement: Replacement) -> Self {
135 Self {
136 title: title.into(),
137 id: None,
138 replacement: Some(replacement),
139 }
140 }
141
142 pub fn replacement_with_id(
143 id: impl Into<String>,
144 title: impl Into<String>,
145 replacement: Replacement,
146 ) -> Self {
147 Self {
148 title: title.into(),
149 id: Some(id.into()),
150 replacement: Some(replacement),
151 }
152 }
153
154 pub fn with_id(mut self, id: impl Into<String>) -> Self {
155 self.id = Some(id.into());
156 self
157 }
158}
159
160pub fn render_diagnostics(
161 path: &str,
162 source: &str,
163 diagnostics: &[Diagnostic],
164) -> Result<String, RenderError> {
165 render_diagnostics_with_color(path, source, diagnostics, false)
166}
167
168pub fn render_diagnostics_with_color(
169 path: &str,
170 source: &str,
171 diagnostics: &[Diagnostic],
172 color: bool,
173) -> Result<String, RenderError> {
174 let source = Source::from(source);
175 let config = Config::default()
176 .with_color(color)
177 .with_char_set(CharSet::Ascii)
178 .with_index_type(IndexType::Byte);
179 let mut output = Vec::new();
180
181 for diagnostic in diagnostics {
182 if !diagnostic.show_source {
183 render_summary_diagnostic(path, &mut output, diagnostic, color);
184 continue;
185 }
186
187 let mut builder = Report::build(
188 report_kind(diagnostic.severity),
189 (path.to_string(), span_range(diagnostic.span)),
190 )
191 .with_config(config)
192 .with_message(&diagnostic.message)
193 .with_label(
194 Label::new((path.to_string(), span_range(diagnostic.span)))
195 .with_color(label_color(diagnostic.severity))
196 .with_message(&diagnostic.message),
197 );
198
199 for related in &diagnostic.related {
200 builder = builder.with_label(
201 Label::new((path.to_string(), span_range(related.span)))
202 .with_color(Color::Cyan)
203 .with_message(&related.message),
204 );
205 }
206
207 if let Some(note) = &diagnostic.note {
208 builder = builder.with_note(note);
209 }
210
211 for quick_fix in &diagnostic.quick_fixes {
212 builder = builder.with_help(quick_fix_description(quick_fix));
213 }
214
215 builder
216 .finish()
217 .write((path.to_string(), &source), &mut output)
218 .map_err(|source| RenderError { source })?;
219 }
220
221 String::from_utf8(output).map_err(|source| RenderError {
222 source: io::Error::new(io::ErrorKind::InvalidData, source),
223 })
224}
225
226fn render_summary_diagnostic(
227 path: &str,
228 output: &mut Vec<u8>,
229 diagnostic: &Diagnostic,
230 color: bool,
231) {
232 let label = severity_label(diagnostic.severity);
233 let rendered_label = if color {
234 format!("{}", label.fg(label_color(diagnostic.severity)))
235 } else {
236 label.to_owned()
237 };
238
239 push_line(output, &format!("{rendered_label}: {}", diagnostic.message));
240 push_line(output, &format!(" in {path}"));
241
242 for quick_fix in &diagnostic.quick_fixes {
243 push_line(
244 output,
245 &format!(" Fix: {}", quick_fix_description(quick_fix)),
246 );
247 }
248
249 if let Some(note) = &diagnostic.note {
250 push_line(output, &format!(" Note: {note}"));
251 }
252
253 output.push(b'\n');
254}
255
256fn push_line(output: &mut Vec<u8>, line: &str) {
257 output.extend_from_slice(line.as_bytes());
258 output.push(b'\n');
259}
260
261fn quick_fix_description(quick_fix: &QuickFix) -> String {
262 match &quick_fix.id {
263 Some(id) => format!(
264 "{} (run `linguini fix {}` or `linguini fix --all`)",
265 quick_fix.title, id
266 ),
267 None => format!("quick fix: {}", quick_fix.title),
268 }
269}
270
271fn severity_label(severity: DiagnosticSeverity) -> &'static str {
272 match severity {
273 DiagnosticSeverity::Error => "Error",
274 DiagnosticSeverity::Warning => "Warning",
275 DiagnosticSeverity::Advice => "Advice",
276 }
277}
278
279fn report_kind(severity: DiagnosticSeverity) -> ReportKind<'static> {
280 match severity {
281 DiagnosticSeverity::Error => ReportKind::Error,
282 DiagnosticSeverity::Warning => ReportKind::Warning,
283 DiagnosticSeverity::Advice => ReportKind::Advice,
284 }
285}
286
287fn label_color(severity: DiagnosticSeverity) -> Color {
288 match severity {
289 DiagnosticSeverity::Error => Color::Red,
290 DiagnosticSeverity::Warning => Color::Yellow,
291 DiagnosticSeverity::Advice => Color::Blue,
292 }
293}
294
295fn span_range(span: Span) -> std::ops::Range<usize> {
296 span.start..span.end
297}