1use std::fmt;
8use std::path::{Path, PathBuf};
9
10#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
12pub struct FileId(pub u32);
13
14#[derive(Clone, Copy, PartialEq, Eq, Debug)]
16pub struct Span {
17 pub file: FileId,
18 pub start: u32,
19 pub end: u32,
20}
21
22impl Span {
23 pub fn new(file: FileId, start: u32, end: u32) -> Self {
24 Span { file, start, end }
25 }
26
27 pub fn to(self, other: Span) -> Span {
29 debug_assert_eq!(self.file, other.file);
30 Span {
31 file: self.file,
32 start: self.start.min(other.start),
33 end: self.end.max(other.end),
34 }
35 }
36}
37
38#[derive(Clone, Copy, PartialEq, Eq, Debug)]
40pub struct Spanned<T> {
41 pub node: T,
42 pub span: Span,
43}
44
45impl<T> Spanned<T> {
46 pub fn new(node: T, span: Span) -> Self {
47 Spanned { node, span }
48 }
49}
50
51pub struct SourceFile {
53 pub id: FileId,
54 pub path: PathBuf,
55 pub text: String,
56 line_starts: Vec<u32>,
58}
59
60impl SourceFile {
61 pub fn line_col(&self, offset: u32) -> (usize, usize) {
63 let line = match self.line_starts.binary_search(&offset) {
64 Ok(i) => i,
65 Err(i) => i - 1,
66 };
67 let line_start = self.line_starts[line] as usize;
68 let col = self.text[line_start..offset as usize].chars().count() + 1;
69 (line + 1, col)
70 }
71
72 pub fn line_text(&self, line: usize) -> &str {
74 let start = self.line_starts[line - 1] as usize;
75 let end = self
76 .line_starts
77 .get(line)
78 .map(|&e| e as usize)
79 .unwrap_or(self.text.len());
80 self.text[start..end].trim_end_matches(['\n', '\r'])
81 }
82}
83
84#[derive(Default)]
86pub struct SourceMap {
87 files: Vec<SourceFile>,
88}
89
90impl SourceMap {
91 pub fn new() -> Self {
92 SourceMap::default()
93 }
94
95 pub fn add(&mut self, path: impl Into<PathBuf>, text: impl Into<String>) -> FileId {
96 let id = FileId(self.files.len() as u32);
97 let text = text.into();
98 let mut line_starts = vec![0u32];
99 for (i, b) in text.bytes().enumerate() {
100 if b == b'\n' {
101 line_starts.push(i as u32 + 1);
102 }
103 }
104 self.files.push(SourceFile {
105 id,
106 path: path.into(),
107 text,
108 line_starts,
109 });
110 id
111 }
112
113 pub fn get(&self, id: FileId) -> &SourceFile {
114 &self.files[id.0 as usize]
115 }
116
117 pub fn path(&self, id: FileId) -> &Path {
118 &self.files[id.0 as usize].path
119 }
120
121 pub fn files(&self) -> impl Iterator<Item = &SourceFile> {
122 self.files.iter()
123 }
124}
125
126#[derive(Clone, Copy, PartialEq, Eq, Debug)]
135pub enum Severity {
136 Error,
137 Warning,
138 Note,
139}
140
141impl fmt::Display for Severity {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 match self {
144 Severity::Error => f.write_str("error"),
145 Severity::Warning => f.write_str("warning"),
146 Severity::Note => f.write_str("note"),
147 }
148 }
149}
150
151#[derive(Clone, Debug)]
153pub struct Label {
154 pub span: Span,
155 pub message: String,
156}
157
158#[derive(Clone, Debug)]
160pub struct Diagnostic {
161 pub severity: Severity,
162 pub code: String,
164 pub message: String,
165 pub primary: Option<Span>,
166 pub labels: Vec<Label>,
167 pub rule: Option<String>,
169 pub help: Option<String>,
171}
172
173impl Diagnostic {
174 pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
175 Diagnostic {
176 severity: Severity::Error,
177 code: code.into(),
178 message: message.into(),
179 primary: None,
180 labels: Vec::new(),
181 rule: None,
182 help: None,
183 }
184 }
185
186 pub fn warning(code: impl Into<String>, message: impl Into<String>) -> Self {
187 Diagnostic {
188 severity: Severity::Warning,
189 ..Diagnostic::error(code, message)
190 }
191 }
192
193 pub fn note(code: impl Into<String>, message: impl Into<String>) -> Self {
200 Diagnostic {
201 severity: Severity::Note,
202 ..Diagnostic::error(code, message)
203 }
204 }
205
206 pub fn at(mut self, span: Span) -> Self {
207 self.primary = Some(span);
208 self
209 }
210
211 pub fn label(mut self, span: Span, message: impl Into<String>) -> Self {
212 self.labels.push(Label {
213 span,
214 message: message.into(),
215 });
216 self
217 }
218
219 pub fn rule(mut self, rule: impl Into<String>) -> Self {
220 self.rule = Some(rule.into());
221 self
222 }
223
224 pub fn help(mut self, help: impl Into<String>) -> Self {
225 self.help = Some(help.into());
226 self
227 }
228}
229
230#[derive(Default)]
232pub struct Diagnostics {
233 items: Vec<Diagnostic>,
234}
235
236impl Diagnostics {
237 pub fn new() -> Self {
238 Diagnostics::default()
239 }
240
241 pub fn push(&mut self, diagnostic: Diagnostic) {
242 self.items.push(diagnostic);
243 }
244
245 pub fn extend(&mut self, other: impl IntoIterator<Item = Diagnostic>) {
246 self.items.extend(other);
247 }
248
249 pub fn has_errors(&self) -> bool {
250 self.items.iter().any(|d| d.severity == Severity::Error)
251 }
252
253 pub fn is_empty(&self) -> bool {
254 self.items.is_empty()
255 }
256
257 pub fn iter(&self) -> impl Iterator<Item = &Diagnostic> {
258 self.items.iter()
259 }
260
261 pub fn into_vec(self) -> Vec<Diagnostic> {
262 self.items
263 }
264}
265
266pub fn render(sources: &SourceMap, diagnostic: &Diagnostic) -> String {
268 let mut out = format!(
269 "{}[{}]: {}\n",
270 diagnostic.severity, diagnostic.code, diagnostic.message
271 );
272
273 if let Some(span) = diagnostic.primary {
274 out.push_str(&render_span(sources, span, None));
275 }
276 for label in &diagnostic.labels {
277 out.push_str(&render_span(sources, label.span, Some(&label.message)));
278 }
279 if let Some(rule) = &diagnostic.rule {
280 out.push_str(&format!(" rule: {rule}\n"));
281 }
282 if let Some(help) = &diagnostic.help {
283 for (i, line) in help.lines().enumerate() {
284 if i == 0 {
285 out.push_str(&format!(" help: {line}\n"));
286 } else {
287 out.push_str(&format!(" {line}\n"));
288 }
289 }
290 }
291 out
292}
293
294fn render_span(sources: &SourceMap, span: Span, message: Option<&str>) -> String {
295 let file = sources.get(span.file);
296 let (line, col) = file.line_col(span.start);
297 let text = file.line_text(line);
298 let gutter = line.to_string();
299 let pad = " ".repeat(gutter.len());
300 let width = {
301 let (end_line, end_col) = file.line_col(span.end);
302 if end_line == line {
303 (end_col - col).max(1)
304 } else {
305 text.chars().count().saturating_sub(col - 1).max(1)
306 }
307 };
308
309 let mut out = format!("{pad}--> {}:{line}:{col}\n", file.path.display());
310 out.push_str(&format!("{pad} |\n"));
311 out.push_str(&format!("{gutter} | {text}\n"));
312 out.push_str(&format!(
313 "{pad} | {}{}{}\n",
314 " ".repeat(col - 1),
315 "^".repeat(width),
316 match message {
317 Some(m) => format!(" {m}"),
318 None => String::new(),
319 }
320 ));
321 out
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn line_col_counts_from_one() {
330 let mut map = SourceMap::new();
331 let id = map.add("a.cove", "let x = 1\nlet y = 2\n");
332 let file = map.get(id);
333 assert_eq!(file.line_col(0), (1, 1));
334 assert_eq!(file.line_col(10), (2, 1));
335 assert_eq!(file.line_text(2), "let y = 2");
336 }
337
338 #[test]
339 fn render_points_at_the_span() {
340 let mut map = SourceMap::new();
341 let id = map.add("a.cove", "let x = 1\n");
342 let d = Diagnostic::error("cove::test::demo", "example")
343 .at(Span::new(id, 4, 5))
344 .rule("`let` creates a read-only place.");
345 let text = render(&map, &d);
346 assert!(text.contains("error[cove::test::demo]: example"));
347 assert!(text.contains("a.cove:1:5"));
348 assert!(text.contains("^"));
349 }
350}