Skip to main content

linguini_analyzer/
diagnostic.rs

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}