1use ariadne::{Color, Label, Report, ReportKind, Source};
4use serde::Serialize;
5use std::ops::Range;
6
7#[derive(Debug, Clone)]
8pub struct Diag {
9 pub kind: DiagKind,
10 pub message: String,
11 pub label: String,
12 pub span: Range<usize>,
13 pub note: Option<String>,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum DiagKind {
18 Error,
19 Warning,
20}
21
22#[derive(Debug, Clone, Serialize)]
23pub struct DiagnosticReport {
24 pub ok: bool,
25 pub error_count: usize,
26 pub warning_count: usize,
27 pub endpoint_count: Option<usize>,
28 pub diagnostics: Vec<JsonDiag>,
29}
30
31#[derive(Debug, Clone, Serialize)]
32pub struct JsonDiag {
33 pub kind: &'static str,
34 pub message: String,
35 pub label: String,
36 pub note: Option<String>,
37 pub span: JsonSpan,
38}
39
40#[derive(Debug, Clone, Serialize)]
41pub struct JsonSpan {
42 pub start: usize,
43 pub end: usize,
44 pub start_line: usize,
45 pub start_column: usize,
46 pub end_line: usize,
47 pub end_column: usize,
48}
49
50impl Diag {
51 pub fn error(message: impl Into<String>, span: Range<usize>, label: impl Into<String>) -> Self {
52 Self {
53 kind: DiagKind::Error,
54 message: message.into(),
55 label: label.into(),
56 span,
57 note: None,
58 }
59 }
60
61 pub fn warning(
62 message: impl Into<String>,
63 span: Range<usize>,
64 label: impl Into<String>,
65 ) -> Self {
66 Self {
67 kind: DiagKind::Warning,
68 message: message.into(),
69 label: label.into(),
70 span,
71 note: None,
72 }
73 }
74
75 pub fn with_note(mut self, note: impl Into<String>) -> Self {
76 self.note = Some(note.into());
77 self
78 }
79
80 pub fn emit(&self, file_name: &str, source: &str) {
81 let kind = match self.kind {
82 DiagKind::Error => ReportKind::Error,
83 DiagKind::Warning => ReportKind::Warning,
84 };
85 let span = clamp_span(&self.span, source.len());
86 let mut builder = Report::build(kind, (file_name, span.clone()))
87 .with_message(&self.message)
88 .with_label(
89 Label::new((file_name, span))
90 .with_message(&self.label)
91 .with_color(match self.kind {
92 DiagKind::Error => Color::Red,
93 DiagKind::Warning => Color::Yellow,
94 }),
95 );
96 if let Some(note) = &self.note {
97 builder = builder.with_note(note);
98 }
99 let _ = builder.finish().eprint((file_name, Source::from(source)));
100 }
101}
102
103fn clamp_span(span: &Range<usize>, max: usize) -> Range<usize> {
104 let start = span.start.min(max);
105 let end = span.end.min(max).max(start);
106 start..end
107}
108
109pub fn emit_all(diags: &[Diag], file_name: &str, source: &str) {
110 for d in diags {
111 d.emit(file_name, source);
112 }
113}
114
115pub fn report(diags: &[Diag], source: &str, endpoint_count: Option<usize>) -> DiagnosticReport {
116 let diagnostics: Vec<_> = diags
117 .iter()
118 .map(|d| JsonDiag {
119 kind: match d.kind {
120 DiagKind::Error => "error",
121 DiagKind::Warning => "warning",
122 },
123 message: d.message.clone(),
124 label: d.label.clone(),
125 note: d.note.clone(),
126 span: json_span(&d.span, source),
127 })
128 .collect();
129 let error_count = diagnostics.iter().filter(|d| d.kind == "error").count();
130 let warning_count = diagnostics.iter().filter(|d| d.kind == "warning").count();
131 DiagnosticReport {
132 ok: error_count == 0,
133 error_count,
134 warning_count,
135 endpoint_count,
136 diagnostics,
137 }
138}
139
140fn json_span(span: &Range<usize>, source: &str) -> JsonSpan {
141 let span = clamp_span(span, source.len());
142 let (start_line, start_column) = line_column(source, span.start);
143 let (end_line, end_column) = line_column(source, span.end);
144 JsonSpan {
145 start: span.start,
146 end: span.end,
147 start_line,
148 start_column,
149 end_line,
150 end_column,
151 }
152}
153
154fn line_column(source: &str, byte_offset: usize) -> (usize, usize) {
155 let byte_offset = previous_char_boundary(source, byte_offset.min(source.len()));
156 let mut line = 0usize;
157 let mut line_start = 0usize;
158 for (idx, ch) in source.char_indices() {
159 if idx >= byte_offset {
160 break;
161 }
162 if ch == '\n' {
163 line += 1;
164 line_start = idx + ch.len_utf8();
165 }
166 }
167 let column = source[line_start..byte_offset].chars().count();
168 (line, column)
169}
170
171fn previous_char_boundary(source: &str, mut byte_offset: usize) -> usize {
172 while byte_offset > 0 && !source.is_char_boundary(byte_offset) {
173 byte_offset -= 1;
174 }
175 byte_offset
176}