Fork of daniellemaywood.uk/gleam — Wasm codegen work
14 kB
433 lines
1use crate::{diagnostic::Diagnostic, Error, Warning};
2use std::collections::{HashMap, HashSet};
3
4use camino::Utf8PathBuf;
5
6use super::engine::Compilation;
7
8#[derive(Debug, Default, PartialEq, Eq)]
9pub struct Feedback {
10 pub diagnostics: HashMap<Utf8PathBuf, Vec<Diagnostic>>,
11 pub messages: Vec<Diagnostic>,
12}
13
14impl Feedback {
15 /// Set the diagnostics for a file to an empty vector. This will overwrite
16 /// any existing diagnostics on the client.
17 pub fn unset_existing_diagnostics(&mut self, path: Utf8PathBuf) {
18 _ = self.diagnostics.insert(path, vec![]);
19 }
20
21 pub fn append_diagnostic(&mut self, path: Utf8PathBuf, diagnostic: Diagnostic) {
22 self.diagnostics
23 .entry(path)
24 .or_insert_with(Vec::new)
25 .push(diagnostic);
26 }
27
28 fn append_message(&mut self, diagnostic: Diagnostic) {
29 self.messages.push(diagnostic);
30 }
31}
32
33/// When an operation succeeds or fails we want to send diagnostics and
34/// messages to the client for displaying to the user. This object converts
35/// Gleam warnings, errors, etc to these feedback items.
36///
37/// Gleam has incremental compilation so we cannot erase all previous
38/// diagnostics and replace each time new diagnostics are available; if a file
39/// has not been recompiled then any diagnostics it had previously are still
40/// valid and must not be erased.
41/// To do this we keep track of which files have diagnostics and only overwrite
42/// them if the file has been recompiled.
43///
44#[derive(Debug, Default)]
45pub struct FeedbackBookKeeper {
46 files_with_warnings: HashSet<Utf8PathBuf>,
47 files_with_errors: HashSet<Utf8PathBuf>,
48}
49
50impl FeedbackBookKeeper {
51 /// Send diagnostics for any warnings and remove any diagnostics for files
52 /// that have compiled without warnings.
53 ///
54 pub fn response(&mut self, compilation: Compilation, warnings: Vec<Warning>) -> Feedback {
55 let mut feedback = Feedback::default();
56
57 if let Compilation::Yes(compiled_modules) = compilation {
58 // Any existing diagnostics for files that have been compiled are no
59 // longer valid so we set an empty vector of diagnostics for the files
60 // to erase their diagnostics.
61 for path in compiled_modules {
62 let has_existing_diagnostics = self.files_with_warnings.remove(&path);
63 if has_existing_diagnostics {
64 feedback.unset_existing_diagnostics(path);
65 }
66 }
67
68 // Compilation was attempted and there is no error (which there is not
69 // in this function) then it means that compilation has succeeded, so
70 // there should be no error diagnostics.
71 // We don't limit this to files that have been compiled as a previous
72 // cached version could be used instead of a recompile.
73 self.unset_errors(&mut feedback);
74 }
75
76 for warning in warnings {
77 self.insert_warning(&mut feedback, warning);
78 }
79
80 feedback
81 }
82
83 fn unset_errors(&mut self, feedback: &mut Feedback) {
84 // TODO: avoid clobbering warnings. They should be preserved rather than
85 // removed with the errors here. We will need to store the warnings and
86 // re-send them.
87 for path in self.files_with_errors.drain() {
88 feedback.unset_existing_diagnostics(path);
89 }
90 }
91
92 /// Compilation failed, boo!
93 ///
94 /// Send diagnostics for any warnings and remove any diagnostics for files
95 /// that have compiled without warnings, AND ALSO send diagnostics for the
96 /// error that caused compilation to fail.
97 ///
98 pub fn build_with_error(
99 &mut self,
100 error: Error,
101 compilation: Compilation,
102 warnings: Vec<Warning>,
103 ) -> Feedback {
104 let diagnostic = error.to_diagnostic();
105 let mut feedback = self.response(compilation, warnings);
106
107 // A new error means that any existing errors are no longer valid. Unset them.
108 self.unset_errors(&mut feedback);
109
110 match diagnostic.location.as_ref().map(|l| l.path.clone()) {
111 Some(path) => {
112 _ = self.files_with_errors.insert(path.clone());
113 feedback.append_diagnostic(path, diagnostic);
114 }
115
116 None => {
117 feedback.append_message(diagnostic);
118 }
119 }
120
121 feedback
122 }
123
124 pub fn error(&mut self, error: Error) -> Feedback {
125 self.build_with_error(error, Compilation::No, vec![])
126 }
127
128 fn insert_warning(&mut self, feedback: &mut Feedback, warning: Warning) {
129 let diagnostic = warning.to_diagnostic();
130 if let Some(path) = diagnostic.location.as_ref().map(|l| l.path.clone()) {
131 _ = self.files_with_warnings.insert(path.clone());
132 feedback.append_diagnostic(path, diagnostic);
133 }
134 }
135}
136
137#[cfg(test)]
138mod tests {
139
140 use super::*;
141 use crate::{
142 ast::SrcSpan,
143 parse::error::{ParseError, ParseErrorType},
144 type_,
145 };
146
147 #[test]
148 fn feedback() {
149 let mut book_keeper = FeedbackBookKeeper::default();
150 let file1 = Utf8PathBuf::from("src/file1.gleam");
151 let file2 = Utf8PathBuf::from("src/file2.gleam");
152 let file3 = Utf8PathBuf::from("src/file3.gleam");
153
154 let warning1 = Warning::Type {
155 path: file1.clone(),
156 src: "src".into(),
157 warning: type_::Warning::NoFieldsRecordUpdate {
158 location: SrcSpan::new(1, 2),
159 },
160 };
161 let warning2 = Warning::Type {
162 path: file2.clone(),
163 src: "src".into(),
164 warning: type_::Warning::NoFieldsRecordUpdate {
165 location: SrcSpan::new(1, 2),
166 },
167 };
168
169 let feedback = book_keeper.response(
170 Compilation::Yes(vec![file1.clone()]),
171 vec![warning1.clone(), warning1.clone(), warning2.clone()],
172 );
173
174 assert_eq!(
175 Feedback {
176 diagnostics: HashMap::from([
177 (
178 file1.clone(),
179 vec![warning1.to_diagnostic(), warning1.to_diagnostic(),]
180 ),
181 (file2.clone(), vec![warning2.to_diagnostic(),])
182 ]),
183 messages: vec![],
184 },
185 feedback
186 );
187
188 let feedback = book_keeper.response(
189 Compilation::Yes(vec![file1.clone(), file2.clone(), file3]),
190 vec![],
191 );
192
193 assert_eq!(
194 Feedback {
195 diagnostics: HashMap::from([
196 // File 1 and 2 had diagnostics before so they have been unset
197 (file1, vec![]),
198 (file2, vec![]),
199 // File 3 had no diagnostics so does not need to to be unset
200 ]),
201 messages: vec![],
202 },
203 feedback
204 );
205 }
206
207 #[test]
208 fn locationless_error() {
209 // The failed method sets an additional messages for errors without a
210 // location.
211
212 let mut book_keeper = FeedbackBookKeeper::default();
213 let file1 = Utf8PathBuf::from("src/file1.gleam");
214
215 let warning1 = Warning::Type {
216 path: file1.clone(),
217 src: "src".into(),
218 warning: type_::Warning::NoFieldsRecordUpdate {
219 location: SrcSpan::new(1, 2),
220 },
221 };
222
223 let locationless_error = Error::Gzip("Hello!".into());
224
225 let feedback = book_keeper.build_with_error(
226 locationless_error.clone(),
227 Compilation::Yes(vec![]),
228 vec![warning1.clone()],
229 );
230
231 assert_eq!(
232 Feedback {
233 diagnostics: HashMap::from([(file1, vec![warning1.to_diagnostic()])]),
234 messages: vec![locationless_error.to_diagnostic()],
235 },
236 feedback
237 );
238 }
239
240 #[test]
241 fn error() {
242 // The failed method sets an additional diagnostic if the error has a
243 // location.
244
245 let mut book_keeper = FeedbackBookKeeper::default();
246 let file1 = Utf8PathBuf::from("src/file1.gleam");
247 let file3 = Utf8PathBuf::from("src/file2.gleam");
248
249 let warning1 = Warning::Type {
250 path: file1.clone(),
251 src: "src".into(),
252 warning: type_::Warning::NoFieldsRecordUpdate {
253 location: SrcSpan::new(1, 2),
254 },
255 };
256 let error = Error::Parse {
257 path: file3.clone(),
258 src: "blah".into(),
259 error: ParseError {
260 error: ParseErrorType::ConcatPatternVariableLeftHandSide,
261 location: SrcSpan::new(1, 4),
262 },
263 };
264
265 let feedback = book_keeper.build_with_error(
266 error.clone(),
267 Compilation::Yes(vec![]),
268 vec![warning1.clone()],
269 );
270
271 assert_eq!(
272 Feedback {
273 diagnostics: HashMap::from([
274 (file1, vec![warning1.to_diagnostic()]),
275 (file3.clone(), vec![error.to_diagnostic()]),
276 ]),
277 messages: vec![],
278 },
279 feedback
280 );
281
282 // The error diagnostic should be removed if the file compiles later.
283
284 let feedback = book_keeper.response(Compilation::Yes(vec![file3.clone()]), vec![]);
285
286 assert_eq!(
287 Feedback {
288 diagnostics: HashMap::from([(file3, vec![])]),
289 messages: vec![],
290 },
291 feedback
292 );
293 }
294
295 // https://github.com/gleam-lang/gleam/issues/2093
296 #[test]
297 fn successful_compilation_removes_error_diagnostic() {
298 // It is possible for a compile error to be fixed but the module that
299 // had the error to not actually be recompiled.
300 //
301 // 1. File is OK
302 // 2. File is edited to an invalid state
303 // 3. A compile error is emitted
304 // 4. File is edited back to the earlier valid state
305 // 5. File is not recompiled as the cache from step 1 is still valid
306 //
307 // Because of this the compiled files iterator does not contain the
308 // file, so we need to make sure that the error is removed through other
309 // means, such as tracking which files have errors and removing them all
310 // when a successful compilation occurs.
311
312 let mut book_keeper = FeedbackBookKeeper::default();
313 let file1 = Utf8PathBuf::from("src/file1.gleam");
314 let file2 = Utf8PathBuf::from("src/file2.gleam");
315
316 let error = Error::Parse {
317 path: file1.clone(),
318 src: "blah".into(),
319 error: ParseError {
320 error: ParseErrorType::ConcatPatternVariableLeftHandSide,
321 location: SrcSpan::new(1, 4),
322 },
323 };
324
325 let feedback =
326 book_keeper.build_with_error(error.clone(), Compilation::Yes(vec![]), vec![]);
327
328 assert_eq!(
329 Feedback {
330 diagnostics: HashMap::from([(file1.clone(), vec![error.to_diagnostic()])]),
331 messages: vec![],
332 },
333 feedback
334 );
335
336 // The error diagnostic should be removed on a successful compilation,
337 // even though the file is not in the compiled files iterator.
338
339 let feedback = book_keeper.response(Compilation::Yes(vec![file2]), vec![]);
340
341 assert_eq!(
342 Feedback {
343 diagnostics: HashMap::from([(file1, vec![])]),
344 messages: vec![],
345 },
346 feedback
347 );
348 }
349
350 // https://github.com/gleam-lang/gleam/issues/2122
351 #[test]
352 fn second_failure_unsets_previous_error() {
353 let mut book_keeper = FeedbackBookKeeper::default();
354 let file1 = Utf8PathBuf::from("src/file1.gleam");
355 let file2 = Utf8PathBuf::from("src/file2.gleam");
356
357 let error = |file: &camino::Utf8Path| Error::Parse {
358 path: file.to_path_buf(),
359 src: "blah".into(),
360 error: ParseError {
361 error: ParseErrorType::ConcatPatternVariableLeftHandSide,
362 location: SrcSpan::new(1, 4),
363 },
364 };
365
366 let feedback =
367 book_keeper.build_with_error(error(&file1), Compilation::Yes(vec![]), vec![]);
368
369 assert_eq!(
370 Feedback {
371 diagnostics: HashMap::from([(file1.clone(), vec![error(&file1).to_diagnostic()])]),
372 messages: vec![],
373 },
374 feedback
375 );
376
377 let feedback =
378 book_keeper.build_with_error(error(&file2), Compilation::Yes(vec![]), vec![]);
379
380 assert_eq!(
381 Feedback {
382 diagnostics: HashMap::from([
383 // Unset the previous error
384 (file1, vec![]),
385 // Set the new one
386 (file2.clone(), vec![error(&file2).to_diagnostic()]),
387 ]),
388 messages: vec![],
389 },
390 feedback
391 );
392 }
393
394 // https://github.com/gleam-lang/gleam/issues/2105
395 #[test]
396 fn successful_non_compilation_does_not_remove_error_diagnostic() {
397 let mut book_keeper = FeedbackBookKeeper::default();
398 let file1 = Utf8PathBuf::from("src/file1.gleam");
399
400 let error = Error::Parse {
401 path: file1.clone(),
402 src: "blah".into(),
403 error: ParseError {
404 error: ParseErrorType::ConcatPatternVariableLeftHandSide,
405 location: SrcSpan::new(1, 4),
406 },
407 };
408
409 let feedback =
410 book_keeper.build_with_error(error.clone(), Compilation::Yes(vec![]), vec![]);
411
412 assert_eq!(
413 Feedback {
414 diagnostics: HashMap::from([(file1, vec![error.to_diagnostic()])]),
415 messages: vec![],
416 },
417 feedback
418 );
419
420 // The error diagnostic should not be removed, nothing has been
421 // successfully compiled.
422
423 let feedback = book_keeper.response(Compilation::No, vec![]);
424
425 assert_eq!(
426 Feedback {
427 diagnostics: HashMap::new(),
428 messages: vec![],
429 },
430 feedback
431 );
432 }
433}