Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

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}