1pub mod files;
8pub mod line_index;
9pub mod lines;
10pub mod render;
11pub mod suggest;
12
13pub use files::{FileDb, FileId};
14pub use line_index::LineIndex;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct Span {
21 pub start: u32,
22 pub end: u32,
23 pub file: FileId,
24}
25
26impl Span {
27 pub fn new(start: u32, end: u32, file: FileId) -> Self {
28 Self { start, end, file }
29 }
30
31 pub fn point(offset: u32, file: FileId) -> Self {
33 Self {
34 start: offset,
35 end: offset,
36 file,
37 }
38 }
39
40 pub fn merge(self, other: Span) -> Self {
42 debug_assert_eq!(
43 self.file, other.file,
44 "cannot merge spans from different files"
45 );
46 Self {
47 start: self.start.min(other.start),
48 end: self.end.max(other.end),
49 file: self.file,
50 }
51 }
52
53 pub fn as_range(self) -> std::ops::Range<usize> {
54 self.start as usize..self.end as usize
55 }
56}
57
58#[derive(Debug, Clone)]
60pub struct Spanned<T> {
61 pub value: T,
62 pub span: Span,
63}
64
65impl<T> Spanned<T> {
66 pub fn new(value: T, span: Span) -> Self {
67 Self { value, span }
68 }
69
70 pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Spanned<U> {
71 Spanned {
72 value: f(self.value),
73 span: self.span,
74 }
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum Severity {
81 Error,
82 Warning,
83 Note,
84 Help,
85}
86
87#[derive(Debug, Clone)]
92pub struct Diagnostic {
93 pub severity: Severity,
94 pub code: String,
95 pub message: String,
96 pub labels: Vec<Label>,
97 pub notes: Vec<String>,
98 pub fixes: Vec<Fix>,
101}
102
103#[derive(Debug, Clone)]
105pub struct Label {
106 pub span: Span,
107 pub message: String,
108 pub primary: bool,
109}
110#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct Fix {
115 pub title: String,
116 pub edits: Vec<Edit>,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct Edit {
122 pub span: Span,
123 pub text: String,
124}
125
126impl Edit {
127 pub fn insert(at: u32, file: FileId, text: impl Into<String>) -> Edit {
129 Edit {
130 span: Span::point(at, file),
131 text: text.into(),
132 }
133 }
134
135 pub fn replace(span: Span, text: impl Into<String>) -> Edit {
137 Edit {
138 span,
139 text: text.into(),
140 }
141 }
142}
143
144impl Fix {
145 pub fn new(title: impl Into<String>, edits: Vec<Edit>) -> Fix {
146 Fix {
147 title: title.into(),
148 edits,
149 }
150 }
151
152 pub fn apply(&self, db: &FileDb) -> Option<Vec<(FileId, String)>> {
156 let mut files: Vec<FileId> = Vec::new();
157 for e in &self.edits {
158 if !files.contains(&e.span.file) {
159 files.push(e.span.file);
160 }
161 }
162 let mut out = Vec::new();
163 for file in files {
164 let text = db.get_source(file)?;
165 let mut edits: Vec<&Edit> = self.edits.iter().filter(|e| e.span.file == file).collect();
166 edits.sort_by_key(|e| (e.span.start, e.span.end));
167 if edits.windows(2).any(|w| w[0].span.end > w[1].span.start) {
168 return None;
169 }
170 let mut new = text.to_string();
171 for e in edits.iter().rev() {
172 let (start, end) = (e.span.start as usize, e.span.end as usize);
173 if start > end
174 || end > text.len()
175 || !text.is_char_boundary(start)
176 || !text.is_char_boundary(end)
177 {
178 return None;
179 }
180 new.replace_range(start..end, &e.text);
181 }
182 out.push((file, new));
183 }
184 Some(out)
185 }
186}
187
188impl Diagnostic {
189 pub fn error(code: impl Into<String>, message: impl Into<String>) -> Self {
190 Self {
191 severity: Severity::Error,
192 code: code.into(),
193 message: message.into(),
194 labels: Vec::new(),
195 notes: Vec::new(),
196 fixes: Vec::new(),
197 }
198 }
199
200 pub fn warning(code: impl Into<String>, message: impl Into<String>) -> Self {
201 Self {
202 severity: Severity::Warning,
203 code: code.into(),
204 message: message.into(),
205 labels: Vec::new(),
206 notes: Vec::new(),
207 fixes: Vec::new(),
208 }
209 }
210
211 pub fn with_label(mut self, span: Span, message: impl Into<String>, primary: bool) -> Self {
212 self.labels.push(Label {
213 span,
214 message: message.into(),
215 primary,
216 });
217 self
218 }
219
220 pub fn with_primary_label(self, span: Span, message: impl Into<String>) -> Self {
221 self.with_label(span, message, true)
222 }
223
224 pub fn with_secondary_label(self, span: Span, message: impl Into<String>) -> Self {
225 self.with_label(span, message, false)
226 }
227
228 pub fn with_note(mut self, note: impl Into<String>) -> Self {
229 self.notes.push(note.into());
230 self
231 }
232
233 pub fn with_fix(mut self, fix: Fix) -> Self {
234 self.fixes.push(fix);
235 self
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 fn two_files() -> (FileDb, FileId, FileId) {
244 let mut db = FileDb::new();
245 let a = db.add("a.nova", "let x = 1\n");
246 let b = db.add("b.nova", "fn f() {}\n");
247 (db, a, b)
248 }
249
250 #[test]
251 fn a_fix_rides_on_its_diagnostic() {
252 let (_, a, _) = two_files();
253 let fix = Fix::new("make `x` mutable", vec![Edit::insert(4, a, "mut ")]);
254 let d = Diagnostic::error("E0060", "cannot assign").with_fix(fix.clone());
255 assert_eq!(d.fixes, vec![fix]);
256 assert!(Diagnostic::warning("E0021", "unreachable").fixes.is_empty());
257 }
258
259 #[test]
260 fn a_fix_applies_to_each_file_it_edits() {
261 let (db, a, b) = two_files();
262 let fix = Fix::new(
263 "two files",
264 vec![
265 Edit::replace(Span::new(3, 4, b), "g"),
266 Edit::insert(4, a, "mut "),
267 Edit::insert(0, b, "pub "),
268 ],
269 );
270 assert_eq!(
271 fix.apply(&db),
272 Some(vec![
273 (b, "pub fn g() {}\n".to_string()),
274 (a, "let mut x = 1\n".to_string()),
275 ])
276 );
277 }
278
279 #[test]
280 fn a_fix_outside_its_file_splitting_a_character_or_overlapping_does_not_apply() {
281 let mut db = FileDb::new();
282 let t = db.add("t.nova", "ก = 1\n");
284 let outside = Fix::new("outside", vec![Edit::insert(99, t, "x")]);
285 let split = Fix::new("split", vec![Edit::insert(1, t, "x")]);
286 let overlap = Fix::new(
287 "overlap",
288 vec![
289 Edit::replace(Span::new(0, 3, t), "a"),
290 Edit::replace(Span::new(2, 5, t), "b"),
291 ],
292 );
293 assert_eq!(outside.apply(&db), None);
294 assert_eq!(split.apply(&db), None);
295 assert_eq!(overlap.apply(&db), None);
296 }
297
298 #[test]
299 fn the_renderer_prints_each_fix_as_a_help_line_after_the_notes() {
300 let (db, a, _) = two_files();
301 let d = Diagnostic::error("E0060", "cannot assign to immutable variable `x`")
302 .with_primary_label(Span::new(4, 5, a), "here")
303 .with_note("a note")
304 .with_fix(Fix::new(
305 "make `x` mutable",
306 vec![Edit::insert(4, a, "mut ")],
307 ));
308 let out = render::render_to_string(&db, &[d]);
309 let note = out.find("= a note").unwrap_or_else(|| panic!("{out}"));
310 let help = out
311 .find("= help: make `x` mutable")
312 .unwrap_or_else(|| panic!("{out}"));
313 assert!(note < help, "{out}");
314 }
315}