1use std::collections::{HashMap, HashSet};
2use std::path::{Path, PathBuf};
3use std::time::Instant;
4
5use crate::lsp::registry::ServerKind;
6use crate::lsp::roots::ServerKey;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct StoredDiagnostic {
11 pub file: PathBuf,
12 pub line: u32,
13 pub column: u32,
14 pub end_line: u32,
15 pub end_column: u32,
16 pub severity: DiagnosticSeverity,
17 pub message: String,
18 pub code: Option<String>,
19 pub source: Option<String>,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum DiagnosticSeverity {
24 Error,
25 Warning,
26 Information,
27 Hint,
28}
29
30impl DiagnosticSeverity {
31 pub fn as_str(self) -> &'static str {
32 match self {
33 Self::Error => "error",
34 Self::Warning => "warning",
35 Self::Information => "information",
36 Self::Hint => "hint",
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
45pub struct DiagnosticEntry {
46 pub diagnostics: Vec<StoredDiagnostic>,
47 pub epoch: u64,
51 pub result_id: Option<String>,
54 pub version: Option<i32>,
61 pub stale: bool,
67 pub provisional: bool,
71}
72
73pub struct DiagnosticsStore {
92 entries: HashMap<(ServerKey, PathBuf), DiagnosticEntry>,
94 by_file: HashMap<PathBuf, HashSet<ServerKey>>,
97 order: Vec<(ServerKey, PathBuf)>,
100 capacity: usize,
102 next_epoch: u64,
104 generation: u64,
107 last_publish_at_for_file: HashMap<(ServerKey, PathBuf), Instant>,
110}
111
112impl DiagnosticsStore {
113 pub fn new() -> Self {
114 Self::with_capacity(5000)
115 }
116
117 pub fn with_capacity(capacity: usize) -> Self {
118 Self {
119 entries: HashMap::new(),
120 by_file: HashMap::new(),
121 order: Vec::new(),
122 capacity,
123 next_epoch: 0,
124 generation: 0,
125 last_publish_at_for_file: HashMap::new(),
126 }
127 }
128
129 pub fn set_capacity(&mut self, capacity: usize) {
133 if self.capacity != capacity {
134 self.generation = self.generation.wrapping_add(1);
135 }
136 self.capacity = capacity;
137 if capacity > 0 {
138 while self.entries.len() > capacity {
139 self.evict_lru();
140 }
141 }
142 self.debug_assert_index_consistent();
143 }
144
145 pub fn len(&self) -> usize {
147 self.entries.len()
148 }
149
150 pub fn generation(&self) -> u64 {
151 self.generation
152 }
153
154 pub fn estimated_memory(&self) -> crate::memory::MemoryEstimate {
155 let mut diagnostic_count = 0usize;
156 let entry_bytes = self
157 .entries
158 .iter()
159 .fold(0u64, |bytes, ((server, path), entry)| {
160 diagnostic_count = diagnostic_count.saturating_add(entry.diagnostics.len());
161 let diagnostics_bytes = entry.diagnostics.iter().fold(0u64, |bytes, diagnostic| {
162 bytes
163 .saturating_add(std::mem::size_of::<StoredDiagnostic>() as u64)
164 .saturating_add(crate::memory::path_bytes(&diagnostic.file))
165 .saturating_add(crate::memory::usize_to_u64(diagnostic.message.len()))
166 .saturating_add(
167 diagnostic
168 .code
169 .as_ref()
170 .map(|code| crate::memory::usize_to_u64(code.len()))
171 .unwrap_or(0),
172 )
173 .saturating_add(
174 diagnostic
175 .source
176 .as_ref()
177 .map(|source| crate::memory::usize_to_u64(source.len()))
178 .unwrap_or(0),
179 )
180 });
181 bytes
182 .saturating_add(std::mem::size_of_val(server) as u64)
183 .saturating_add(crate::memory::path_bytes(&server.root))
184 .saturating_add(std::mem::size_of::<PathBuf>() as u64)
185 .saturating_add(crate::memory::path_bytes(path))
186 .saturating_add(std::mem::size_of::<DiagnosticEntry>() as u64)
187 .saturating_add(
188 entry
189 .result_id
190 .as_ref()
191 .map(|result_id| crate::memory::usize_to_u64(result_id.len()))
192 .unwrap_or(0),
193 )
194 .saturating_add(diagnostics_bytes)
195 });
196 let order_bytes = self.order.iter().fold(0u64, |bytes, (server, path)| {
197 bytes
198 .saturating_add(std::mem::size_of_val(server) as u64)
199 .saturating_add(crate::memory::path_bytes(&server.root))
200 .saturating_add(std::mem::size_of::<PathBuf>() as u64)
201 .saturating_add(crate::memory::path_bytes(path))
202 });
203 let publish_bytes =
204 self.last_publish_at_for_file
205 .iter()
206 .fold(0u64, |bytes, ((server, path), _)| {
207 bytes
208 .saturating_add(std::mem::size_of_val(server) as u64)
209 .saturating_add(crate::memory::path_bytes(&server.root))
210 .saturating_add(std::mem::size_of::<PathBuf>() as u64)
211 .saturating_add(crate::memory::path_bytes(path))
212 .saturating_add(std::mem::size_of::<Instant>() as u64)
213 });
214 let by_file_bytes = self.by_file.iter().fold(0u64, |bytes, (path, servers)| {
215 let server_bytes = servers.iter().fold(0u64, |server_bytes, server| {
216 server_bytes
217 .saturating_add(std::mem::size_of_val(server) as u64)
218 .saturating_add(crate::memory::path_bytes(&server.root))
219 });
220 bytes
221 .saturating_add(std::mem::size_of::<PathBuf>() as u64)
222 .saturating_add(crate::memory::path_bytes(path))
223 .saturating_add(server_bytes)
224 });
225 crate::memory::MemoryEstimate::estimated(
226 entry_bytes
227 .saturating_add(order_bytes)
228 .saturating_add(publish_bytes)
229 .saturating_add(by_file_bytes),
230 )
231 .count("diagnostic_entries", self.entries.len())
232 .count("diagnostics", diagnostic_count)
233 }
234
235 #[cfg(test)]
238 pub fn capacity_for_test(&self) -> usize {
239 self.capacity
240 }
241
242 pub fn is_empty(&self) -> bool {
243 self.entries.is_empty()
244 }
245
246 pub fn has_any_fresh_report(&self) -> bool {
249 self.entries.values().any(|entry| !entry.stale)
250 }
251
252 pub fn publish(
261 &mut self,
262 server: ServerKey,
263 file: PathBuf,
264 diagnostics: Vec<StoredDiagnostic>,
265 ) {
266 self.publish_with_result_id(server, file, diagnostics, None);
267 }
268
269 pub fn publish_with_result_id(
272 &mut self,
273 server: ServerKey,
274 file: PathBuf,
275 diagnostics: Vec<StoredDiagnostic>,
276 result_id: Option<String>,
277 ) {
278 self.publish_full(server, file, diagnostics, result_id, None);
279 }
280
281 pub fn publish_full(
285 &mut self,
286 server: ServerKey,
287 file: PathBuf,
288 diagnostics: Vec<StoredDiagnostic>,
289 result_id: Option<String>,
290 version: Option<i32>,
291 ) {
292 self.publish_full_with_provisional(server, file, diagnostics, result_id, version, false);
293 }
294
295 pub fn publish_full_with_provisional(
298 &mut self,
299 server: ServerKey,
300 file: PathBuf,
301 diagnostics: Vec<StoredDiagnostic>,
302 result_id: Option<String>,
303 version: Option<i32>,
304 provisional: bool,
305 ) {
306 let key = (server, file);
307 self.next_epoch = self.next_epoch.saturating_add(1);
308 self.generation = self.generation.wrapping_add(1);
309 let entry = DiagnosticEntry {
310 diagnostics,
311 epoch: self.next_epoch,
312 result_id,
313 version,
314 stale: false,
315 provisional,
316 };
317
318 self.last_publish_at_for_file
319 .insert(key.clone(), Instant::now());
320
321 if self.entries.contains_key(&key) {
322 self.entries.insert(key.clone(), entry);
323 self.index_entry(&key);
324 self.touch_existing(&key);
325 } else {
326 if self.capacity > 0 && self.entries.len() >= self.capacity {
328 self.evict_lru();
329 }
330 self.entries.insert(key.clone(), entry);
331 self.index_entry(&key);
332 self.order.push(key);
333 }
334 self.debug_assert_index_consistent();
335 }
336
337 pub fn publish_with_kind(
343 &mut self,
344 kind: ServerKind,
345 file: PathBuf,
346 diagnostics: Vec<StoredDiagnostic>,
347 ) {
348 let key = ServerKey {
349 kind,
350 root: PathBuf::new(),
351 };
352 self.publish(key, file, diagnostics);
353 }
354
355 pub fn for_file(&self, file: &Path) -> Vec<&StoredDiagnostic> {
358 let Some(servers) = self.by_file.get(file) else {
359 return Vec::new();
360 };
361 let file = file.to_path_buf();
362 let mut diagnostics = Vec::new();
363 for server in servers {
364 if let Some(entry) = self.entries.get(&(server.clone(), file.clone())) {
365 if !entry.stale {
366 diagnostics.extend(&entry.diagnostics);
367 }
368 }
369 }
370 diagnostics
371 }
372
373 pub fn for_file_with_provisional(&self, file: &Path) -> Vec<(&StoredDiagnostic, bool)> {
377 let Some(servers) = self.by_file.get(file) else {
378 return Vec::new();
379 };
380 let file = file.to_path_buf();
381 let mut diagnostics = Vec::new();
382 for server in servers {
383 if let Some(entry) = self.entries.get(&(server.clone(), file.clone())) {
384 if !entry.stale {
385 diagnostics.extend(
386 entry
387 .diagnostics
388 .iter()
389 .map(|diagnostic| (diagnostic, entry.provisional)),
390 );
391 }
392 }
393 }
394 diagnostics
395 }
396
397 pub fn entries_for_file(&self, file: &Path) -> Vec<(&ServerKey, &DiagnosticEntry)> {
400 let Some(servers) = self.by_file.get(file) else {
401 return Vec::new();
402 };
403 let file = file.to_path_buf();
404 servers
405 .iter()
406 .filter_map(|server| {
407 self.entries
408 .get_key_value(&(server.clone(), file.clone()))
409 .map(|((stored_server, _), entry)| (stored_server, entry))
410 })
411 .collect()
412 }
413
414 pub fn has_any_report_for_file(&self, file: &Path) -> bool {
416 self.by_file.contains_key(file)
417 }
418
419 pub fn has_any_fresh_report_for_file(&self, file: &Path) -> bool {
421 let Some(servers) = self.by_file.get(file) else {
422 return false;
423 };
424 let file = file.to_path_buf();
425 servers.iter().any(|server| {
426 self.entries
427 .get(&(server.clone(), file.clone()))
428 .is_some_and(|entry| !entry.stale)
429 })
430 }
431
432 pub fn has_authoritative_report_for_file(&self, file: &Path) -> bool {
438 let Some(servers) = self.by_file.get(file) else {
439 return false;
440 };
441 let file = file.to_path_buf();
442 servers.iter().any(|server| {
443 self.entries
444 .get(&(server.clone(), file.clone()))
445 .is_some_and(|entry| !entry.stale && !entry.provisional)
446 })
447 }
448
449 pub fn has_authoritative_report_for_server(&self, server: &ServerKey) -> bool {
453 self.entries
454 .iter()
455 .any(|((key, _), entry)| key == server && !entry.stale && !entry.provisional)
456 }
457
458 pub fn has_report_for_server_file(&self, server: &ServerKey, file: &Path) -> bool {
462 self.entries
463 .contains_key(&(server.clone(), file.to_path_buf()))
464 }
465
466 pub fn has_fresh_report_for_server_file(&self, server: &ServerKey, file: &Path) -> bool {
468 self.entries
469 .get(&(server.clone(), file.to_path_buf()))
470 .is_some_and(|entry| !entry.stale)
471 }
472
473 pub fn has_publish_for_file_after(
477 &self,
478 server: &ServerKey,
479 file: &Path,
480 since: Instant,
481 ) -> bool {
482 self.last_publish_at_for_file
483 .get(&(server.clone(), file.to_path_buf()))
484 .is_some_and(|published_at| {
485 *published_at >= since && self.has_fresh_report_for_server_file(server, file)
486 })
487 }
488
489 pub fn for_directory(&self, dir: &Path) -> Vec<&StoredDiagnostic> {
491 self.entries
492 .iter()
493 .filter(|((_, stored_file), entry)| stored_file.starts_with(dir) && !entry.stale)
494 .flat_map(|(_, entry)| entry.diagnostics.iter())
495 .collect()
496 }
497
498 pub fn for_directory_with_provisional(&self, dir: &Path) -> Vec<(&StoredDiagnostic, bool)> {
501 self.entries
502 .iter()
503 .filter(|((_, stored_file), entry)| stored_file.starts_with(dir) && !entry.stale)
504 .flat_map(|(_, entry)| {
505 entry
506 .diagnostics
507 .iter()
508 .map(|diagnostic| (diagnostic, entry.provisional))
509 })
510 .collect()
511 }
512
513 pub fn all(&self) -> Vec<&StoredDiagnostic> {
515 self.entries
516 .values()
517 .filter(|entry| !entry.stale)
518 .flat_map(|entry| entry.diagnostics.iter())
519 .collect()
520 }
521
522 pub fn all_with_provisional(&self) -> Vec<(&StoredDiagnostic, bool)> {
524 self.entries
525 .values()
526 .filter(|entry| !entry.stale)
527 .flat_map(|entry| {
528 entry
529 .diagnostics
530 .iter()
531 .map(|diagnostic| (diagnostic, entry.provisional))
532 })
533 .collect()
534 }
535
536 pub fn error_warning_counts(&self) -> (usize, usize) {
542 self.error_warning_counts_with_provisional().0
543 }
544
545 pub fn error_warning_counts_with_provisional(&self) -> ((usize, usize), bool) {
547 let mut errors = 0usize;
548 let mut warnings = 0usize;
549 let mut has_provisional = false;
550 for entry in self.entries.values() {
551 if entry.provisional {
552 has_provisional = true;
553 }
554 if entry.stale || entry.provisional {
555 continue;
556 }
557 for diagnostic in &entry.diagnostics {
558 match diagnostic.severity {
559 DiagnosticSeverity::Error => errors += 1,
560 DiagnosticSeverity::Warning => warnings += 1,
561 _ => {}
562 }
563 }
564 }
565 ((errors, warnings), has_provisional)
566 }
567
568 pub fn filtered_error_warning_counts(&self, keep: impl FnMut(&Path) -> bool) -> (usize, usize) {
581 self.filtered_error_warning_counts_with_provisional(keep).0
582 }
583
584 pub fn filtered_error_warning_counts_with_provisional(
588 &self,
589 mut keep: impl FnMut(&Path) -> bool,
590 ) -> ((usize, usize), bool) {
591 let mut seen: std::collections::HashSet<(
595 &Path,
596 u32,
597 u32,
598 u32,
599 u32,
600 &str,
601 &str,
602 Option<&str>,
603 )> = std::collections::HashSet::new();
604 let mut errors = 0usize;
605 let mut warnings = 0usize;
606 let mut has_provisional = false;
607 for ((_, file), entry) in &self.entries {
608 if entry.provisional && keep(file) {
609 has_provisional = true;
610 }
611 if entry.stale || entry.provisional {
612 continue;
613 }
614 if !keep(file) {
618 continue;
619 }
620 for diagnostic in &entry.diagnostics {
621 if crate::lsp::environmental::is_environmental_diagnostic(diagnostic) {
622 continue;
623 }
624 let dedup_key = (
625 diagnostic.file.as_path(),
626 diagnostic.line,
627 diagnostic.column,
628 diagnostic.end_line,
629 diagnostic.end_column,
630 diagnostic.severity.as_str(),
631 diagnostic.message.as_str(),
632 diagnostic.source.as_deref(),
633 );
634 if !seen.insert(dedup_key) {
635 continue;
636 }
637 match diagnostic.severity {
638 DiagnosticSeverity::Error => errors += 1,
639 DiagnosticSeverity::Warning => warnings += 1,
640 _ => {}
641 }
642 }
643 }
644 ((errors, warnings), has_provisional)
645 }
646
647 pub fn clear_server(&mut self, server: ServerKind) {
651 let before = self.entries.len();
652 self.entries
653 .retain(|(stored_key, _), _| stored_key.kind != server);
654 self.order
655 .retain(|(stored_key, _)| stored_key.kind != server);
656 self.last_publish_at_for_file
657 .retain(|(stored_key, _), _| stored_key.kind != server);
658 self.by_file.retain(|_, servers| {
659 servers.retain(|stored_key| stored_key.kind != server);
660 !servers.is_empty()
661 });
662 if self.entries.len() != before {
663 self.generation = self.generation.wrapping_add(1);
664 }
665 self.debug_assert_index_consistent();
666 }
667
668 pub fn clear_for_server_file(&mut self, key: &ServerKey, file: &Path) {
670 let cache_key = (key.clone(), file.to_path_buf());
671 if self.entries.remove(&cache_key).is_some() {
672 self.unindex_entry(&cache_key);
673 self.generation = self.generation.wrapping_add(1);
674 }
675 self.order.retain(|entry_key| entry_key != &cache_key);
676 self.last_publish_at_for_file.remove(&cache_key);
677 self.debug_assert_index_consistent();
678 }
679
680 pub fn clear_for_file(&mut self, file: &Path) -> bool {
686 let Some(servers) = self.by_file.remove(file) else {
687 self.debug_assert_index_consistent();
688 return false;
689 };
690 let mut removed = false;
691 for server in servers {
692 let cache_key = (server, file.to_path_buf());
693 removed |= self.entries.remove(&cache_key).is_some();
694 self.last_publish_at_for_file.remove(&cache_key);
695 }
696 if removed {
697 self.generation = self.generation.wrapping_add(1);
698 self.order.retain(|(_, stored_file)| stored_file != file);
699 }
700 self.debug_assert_index_consistent();
701 removed
702 }
703
704 pub fn mark_stale_for_file(&mut self, file: &Path) -> (bool, bool) {
712 let Some(servers) = self.by_file.get(file) else {
713 return (false, false);
714 };
715 let had_entries = !servers.is_empty();
716 let mut changed = false;
717 for server in servers {
718 let cache_key = (server.clone(), file.to_path_buf());
719 if let Some(entry) = self
720 .entries
721 .get_mut(&cache_key)
722 .filter(|entry| !entry.stale)
723 {
724 entry.stale = true;
725 changed = true;
726 }
727 }
728 if changed {
729 self.generation = self.generation.wrapping_add(1);
730 }
731 self.debug_assert_index_consistent();
732 (had_entries, changed)
733 }
734
735 pub fn mark_fresh_for_server_file(&mut self, key: &ServerKey, file: &Path) -> bool {
738 let cache_key = (key.clone(), file.to_path_buf());
739 let Some(entry) = self.entries.get_mut(&cache_key) else {
740 return false;
741 };
742 let changed = entry.stale;
743 entry.stale = false;
744 if changed {
745 self.generation = self.generation.wrapping_add(1);
746 }
747 self.touch_existing(&cache_key);
748 changed
749 }
750
751 pub fn confirm_for_server_file_version(
755 &mut self,
756 key: &ServerKey,
757 file: &Path,
758 version: i32,
759 ) -> bool {
760 let cache_key = (key.clone(), file.to_path_buf());
761 let Some(entry) = self.entries.get_mut(&cache_key) else {
762 return false;
763 };
764 self.next_epoch = self.next_epoch.saturating_add(1);
765 self.generation = self.generation.wrapping_add(1);
766 entry.epoch = self.next_epoch;
767 entry.version = Some(version);
768 entry.stale = false;
769 self.touch_existing(&cache_key);
770 true
771 }
772
773 pub fn promote_provisional_for_server(&mut self, key: &ServerKey) -> bool {
778 let mut changed = false;
779 for ((stored_key, _), entry) in &mut self.entries {
780 if stored_key == key && entry.provisional {
781 entry.provisional = false;
782 changed = true;
783 }
784 }
785 if changed {
786 self.generation = self.generation.wrapping_add(1);
787 }
788 self.debug_assert_index_consistent();
789 changed
790 }
791
792 pub fn clear_provisional_for_server_file(&mut self, key: &ServerKey, file: &Path) -> bool {
796 let cache_key = (key.clone(), file.to_path_buf());
797 let Some(entry) = self.entries.get_mut(&cache_key) else {
798 return false;
799 };
800 if !entry.provisional {
801 return false;
802 }
803 entry.provisional = false;
804 self.generation = self.generation.wrapping_add(1);
805 true
806 }
807
808 pub fn clear_for_server(&mut self, key: &ServerKey) {
810 let before = self.entries.len();
811 self.entries.retain(|(k, _), _| k != key);
812 self.order.retain(|(k, _)| k != key);
813 self.last_publish_at_for_file.retain(|(k, _), _| k != key);
814 self.by_file.retain(|_, servers| {
815 servers.remove(key);
816 !servers.is_empty()
817 });
818 if self.entries.len() != before {
819 self.generation = self.generation.wrapping_add(1);
820 }
821 self.debug_assert_index_consistent();
822 }
823
824 pub fn clear_server_instance(&mut self, key: &ServerKey) {
827 self.clear_for_server(key);
828 }
829
830 fn evict_lru(&mut self) -> Option<(ServerKey, PathBuf)> {
832 if self.order.is_empty() {
833 return None;
834 }
835 let evicted = self.order.remove(0);
836 self.entries.remove(&evicted);
837 self.unindex_entry(&evicted);
838 self.last_publish_at_for_file.remove(&evicted);
839 self.debug_assert_index_consistent();
840 Some(evicted)
841 }
842
843 fn touch_existing(&mut self, key: &(ServerKey, PathBuf)) {
844 if let Some(idx) = self.order.iter().position(|k| k == key) {
845 let removed = self.order.remove(idx);
846 self.order.push(removed);
847 }
848 }
849
850 fn index_entry(&mut self, (server, file): &(ServerKey, PathBuf)) {
851 self.by_file
852 .entry(file.clone())
853 .or_default()
854 .insert(server.clone());
855 }
856
857 fn unindex_entry(&mut self, (server, file): &(ServerKey, PathBuf)) {
858 let remove_file = self.by_file.get_mut(file).is_some_and(|servers| {
859 servers.remove(server);
860 servers.is_empty()
861 });
862 if remove_file {
863 self.by_file.remove(file);
864 }
865 }
866
867 fn debug_assert_index_consistent(&self) {
868 #[cfg(debug_assertions)]
869 {
870 let indexed_entries = self.by_file.values().map(HashSet::len).sum::<usize>();
871 debug_assert_eq!(indexed_entries, self.entries.len());
872 for (server, file) in self.entries.keys() {
873 debug_assert!(self
874 .by_file
875 .get(file)
876 .is_some_and(|servers| servers.contains(server)));
877 }
878 for (file, servers) in &self.by_file {
879 debug_assert!(!servers.is_empty());
880 for server in servers {
881 debug_assert!(self.entries.contains_key(&(server.clone(), file.clone())));
882 }
883 }
884 }
885 }
886
887 #[cfg(test)]
888 fn mark_stale_for_file_linear_reference(&self, file: &Path) -> (bool, bool) {
889 let mut had_entries = false;
890 let mut changed = false;
891 for ((_, stored_file), entry) in &self.entries {
892 if stored_file == file {
893 had_entries = true;
894 changed |= !entry.stale;
895 }
896 }
897 (had_entries, changed)
898 }
899}
900
901impl Default for DiagnosticsStore {
902 fn default() -> Self {
903 Self::new()
904 }
905}
906
907pub fn from_lsp_diagnostics(
910 file: PathBuf,
911 lsp_diagnostics: Vec<lsp_types::Diagnostic>,
912) -> Vec<StoredDiagnostic> {
913 lsp_diagnostics
914 .into_iter()
915 .map(|diagnostic| StoredDiagnostic {
916 file: file.clone(),
917 line: diagnostic.range.start.line + 1,
918 column: diagnostic.range.start.character + 1,
919 end_line: diagnostic.range.end.line + 1,
920 end_column: diagnostic.range.end.character + 1,
921 severity: match diagnostic.severity {
922 Some(lsp_types::DiagnosticSeverity::ERROR) => DiagnosticSeverity::Error,
923 Some(lsp_types::DiagnosticSeverity::WARNING) => DiagnosticSeverity::Warning,
924 Some(lsp_types::DiagnosticSeverity::INFORMATION) => DiagnosticSeverity::Information,
925 Some(lsp_types::DiagnosticSeverity::HINT) => DiagnosticSeverity::Hint,
926 _ => DiagnosticSeverity::Warning,
927 },
928 message: diagnostic.message,
929 code: diagnostic.code.map(|code| match code {
930 lsp_types::NumberOrString::Number(value) => value.to_string(),
931 lsp_types::NumberOrString::String(value) => value,
932 }),
933 source: diagnostic.source,
934 })
935 .collect()
936}
937
938#[cfg(test)]
939mod tests {
940 use std::path::{Path, PathBuf};
941
942 use lsp_types::{
943 Diagnostic, DiagnosticSeverity as LspDiagnosticSeverity, NumberOrString, Position, Range,
944 };
945
946 use super::{from_lsp_diagnostics, DiagnosticSeverity, DiagnosticsStore, StoredDiagnostic};
947 use crate::lsp::registry::ServerKind;
948 use crate::lsp::roots::ServerKey;
949
950 fn server_key(kind: ServerKind) -> ServerKey {
951 ServerKey {
952 kind,
953 root: PathBuf::from("/tmp/repo"),
954 }
955 }
956
957 fn diag(file: &str, line: u32, msg: &str, sev: DiagnosticSeverity) -> StoredDiagnostic {
958 StoredDiagnostic {
959 file: PathBuf::from(file),
960 line,
961 column: 1,
962 end_line: line,
963 end_column: 2,
964 severity: sev,
965 message: msg.into(),
966 code: None,
967 source: None,
968 }
969 }
970
971 #[test]
972 fn diagnostics_memory_estimate_is_zero_when_empty_and_nonzero_when_populated() {
973 let mut store = DiagnosticsStore::new();
974 assert_eq!(store.estimated_memory().estimated_bytes, Some(0));
975 let file = PathBuf::from("/tmp/memory.rs");
976 store.publish(
977 server_key(ServerKind::Rust),
978 file.clone(),
979 vec![diag(
980 file.to_str().unwrap(),
981 1,
982 "resident diagnostic message",
983 DiagnosticSeverity::Warning,
984 )],
985 );
986 let estimate = store.estimated_memory();
987 assert!(estimate.estimated_bytes.unwrap() > 0);
988 assert_eq!(estimate.counts["diagnostic_entries"], 1);
989 assert_eq!(estimate.counts["diagnostics"], 1);
990 }
991
992 #[test]
993 fn converts_lsp_positions_to_one_based() {
994 let file = PathBuf::from("/tmp/demo.rs");
995 let diagnostics = from_lsp_diagnostics(
996 file.clone(),
997 vec![Diagnostic {
998 range: Range::new(Position::new(0, 0), Position::new(1, 4)),
999 severity: Some(LspDiagnosticSeverity::ERROR),
1000 code: Some(NumberOrString::String("E1".into())),
1001 code_description: None,
1002 source: Some("fake".into()),
1003 message: "boom".into(),
1004 related_information: None,
1005 tags: None,
1006 data: None,
1007 }],
1008 );
1009
1010 assert_eq!(diagnostics.len(), 1);
1011 assert_eq!(diagnostics[0].file, file);
1012 assert_eq!(diagnostics[0].line, 1);
1013 assert_eq!(diagnostics[0].column, 1);
1014 assert_eq!(diagnostics[0].end_line, 2);
1015 assert_eq!(diagnostics[0].end_column, 5);
1016 assert_eq!(diagnostics[0].severity, DiagnosticSeverity::Error);
1017 assert_eq!(diagnostics[0].code.as_deref(), Some("E1"));
1018 }
1019
1020 #[test]
1021 fn publish_replaces_existing_file_diagnostics() {
1022 let file = PathBuf::from("/tmp/demo.rs");
1023 let mut store = DiagnosticsStore::new();
1024 let key = server_key(ServerKind::Rust);
1025
1026 store.publish(
1027 key.clone(),
1028 file.clone(),
1029 vec![diag(
1030 "/tmp/demo.rs",
1031 1,
1032 "first",
1033 DiagnosticSeverity::Warning,
1034 )],
1035 );
1036 store.publish(
1037 key.clone(),
1038 file.clone(),
1039 vec![diag("/tmp/demo.rs", 2, "second", DiagnosticSeverity::Error)],
1040 );
1041
1042 let stored = store.for_file(&file);
1043 assert_eq!(stored.len(), 1);
1044 assert_eq!(stored[0].message, "second");
1045 }
1046
1047 #[test]
1048 fn empty_publish_is_preserved_as_checked_clean() {
1049 let file = PathBuf::from("/tmp/clean.rs");
1053 let mut store = DiagnosticsStore::new();
1054 let key = server_key(ServerKind::Rust);
1055
1056 store.publish(
1058 key.clone(),
1059 file.clone(),
1060 vec![diag(
1061 "/tmp/clean.rs",
1062 5,
1063 "fix me",
1064 DiagnosticSeverity::Warning,
1065 )],
1066 );
1067 assert!(store.has_any_report_for_file(&file));
1068 assert_eq!(store.for_file(&file).len(), 1);
1069
1070 store.publish(key.clone(), file.clone(), Vec::new());
1073 assert!(
1074 store.has_any_report_for_file(&file),
1075 "checked-clean must be distinguishable from never-checked"
1076 );
1077 assert_eq!(store.for_file(&file).len(), 0);
1078
1079 let entries = store.entries_for_file(&file);
1080 assert_eq!(entries.len(), 1);
1081 assert!(entries[0].1.epoch > 0);
1082 }
1083
1084 #[test]
1085 fn never_checked_returns_no_report() {
1086 let store = DiagnosticsStore::new();
1087 let file = PathBuf::from("/tmp/never.rs");
1088 assert!(!store.has_any_report_for_file(&file));
1089 assert!(store.for_file(&file).is_empty());
1090 }
1091
1092 #[test]
1093 fn stale_entries_are_hidden_but_preserved_for_refresh() {
1094 let file = PathBuf::from("/tmp/stale.rs");
1095 let mut store = DiagnosticsStore::new();
1096 let key = server_key(ServerKind::Rust);
1097 store.publish(
1098 key.clone(),
1099 file.clone(),
1100 vec![diag("/tmp/stale.rs", 1, "old", DiagnosticSeverity::Error)],
1101 );
1102
1103 let (had_entries, changed) = store.mark_stale_for_file(&file);
1104
1105 assert!(had_entries);
1106 assert!(changed);
1107 assert!(store.has_any_report_for_file(&file));
1108 assert!(!store.has_any_fresh_report_for_file(&file));
1109 assert!(store.for_file(&file).is_empty());
1110 assert!(store.all().is_empty());
1111 assert_eq!(store.error_warning_counts(), (0, 0));
1112 assert_eq!(store.entries_for_file(&file).len(), 1);
1113
1114 assert!(store.mark_fresh_for_server_file(&key, &file));
1115 assert!(store.has_any_fresh_report_for_file(&file));
1116 assert_eq!(store.for_file(&file).len(), 1);
1117 assert_eq!(store.error_warning_counts(), (1, 0));
1118 }
1119
1120 #[test]
1121 fn per_server_state_is_tracked_independently() {
1122 let file = PathBuf::from("/tmp/multi.py");
1123 let mut store = DiagnosticsStore::new();
1124 let pyright_key = server_key(ServerKind::Python);
1125 let ty_key = server_key(ServerKind::Ty);
1126
1127 store.publish(
1128 pyright_key,
1129 file.clone(),
1130 vec![diag(
1131 "/tmp/multi.py",
1132 1,
1133 "pyright says X",
1134 DiagnosticSeverity::Error,
1135 )],
1136 );
1137 store.publish(
1138 ty_key,
1139 file.clone(),
1140 vec![diag(
1141 "/tmp/multi.py",
1142 2,
1143 "ty says Y",
1144 DiagnosticSeverity::Warning,
1145 )],
1146 );
1147
1148 let messages: Vec<&str> = store
1149 .for_file(&file)
1150 .into_iter()
1151 .map(|d| d.message.as_str())
1152 .collect();
1153
1154 assert_eq!(messages.len(), 2, "both servers' reports preserved");
1155 assert!(messages.iter().any(|m| m == &"pyright says X"));
1156 assert!(messages.iter().any(|m| m == &"ty says Y"));
1157 }
1158
1159 #[test]
1160 fn clear_for_server_file_removes_only_exact_entry() {
1161 let file_a = PathBuf::from("/tmp/a.rs");
1162 let file_b = PathBuf::from("/tmp/b.rs");
1163 let mut store = DiagnosticsStore::new();
1164 let rust_key = server_key(ServerKind::Rust);
1165 let py_key = server_key(ServerKind::Python);
1166
1167 store.publish(
1168 rust_key.clone(),
1169 file_a.clone(),
1170 vec![diag("/tmp/a.rs", 1, "rust a", DiagnosticSeverity::Error)],
1171 );
1172 store.publish(
1173 rust_key.clone(),
1174 file_b.clone(),
1175 vec![diag("/tmp/b.rs", 1, "rust b", DiagnosticSeverity::Warning)],
1176 );
1177 store.publish(
1178 py_key.clone(),
1179 file_a.clone(),
1180 vec![diag("/tmp/a.rs", 2, "py a", DiagnosticSeverity::Warning)],
1181 );
1182
1183 store.clear_for_server_file(&rust_key, &file_a);
1184
1185 assert!(!store.has_report_for_server_file(&rust_key, &file_a));
1186 assert!(store.has_report_for_server_file(&rust_key, &file_b));
1187 assert!(store.has_report_for_server_file(&py_key, &file_a));
1188 }
1189
1190 #[test]
1191 fn lru_evicts_oldest_when_capacity_exceeded() {
1192 let mut store = DiagnosticsStore::with_capacity(2);
1193 let key = server_key(ServerKind::Rust);
1194
1195 store.publish(
1196 key.clone(),
1197 PathBuf::from("/a.rs"),
1198 vec![diag("/a.rs", 1, "a", DiagnosticSeverity::Warning)],
1199 );
1200 store.publish(
1201 key.clone(),
1202 PathBuf::from("/b.rs"),
1203 vec![diag("/b.rs", 1, "b", DiagnosticSeverity::Warning)],
1204 );
1205 assert_eq!(store.len(), 2);
1206
1207 store.publish(
1209 key.clone(),
1210 PathBuf::from("/c.rs"),
1211 vec![diag("/c.rs", 1, "c", DiagnosticSeverity::Warning)],
1212 );
1213 assert_eq!(store.len(), 2);
1214 assert!(!store.has_any_report_for_file(Path::new("/a.rs")));
1215 assert!(!store.by_file.contains_key(Path::new("/a.rs")));
1216 assert!(store.has_any_report_for_file(Path::new("/b.rs")));
1217 assert!(store.has_any_report_for_file(Path::new("/c.rs")));
1218 store.debug_assert_index_consistent();
1219 }
1220
1221 #[test]
1222 fn secondary_index_stays_consistent_through_seeded_mutation_storm() {
1223 fn next_random(seed: &mut u64) -> u64 {
1224 *seed = seed
1225 .wrapping_mul(6_364_136_223_846_793_005)
1226 .wrapping_add(1_442_695_040_888_963_407);
1227 *seed
1228 }
1229
1230 let servers = [
1231 server_key(ServerKind::Rust),
1232 server_key(ServerKind::TypeScript),
1233 server_key(ServerKind::Python),
1234 server_key(ServerKind::Biome),
1235 ];
1236 let files = (0..11)
1237 .map(|index| PathBuf::from(format!("/tmp/index-{index}.rs")))
1238 .collect::<Vec<_>>();
1239 let mut store = DiagnosticsStore::with_capacity(7);
1240 let mut seed = 0x05ee_dd1a_6005_71c5_u64;
1241 let mut operation_counts = [0usize; 7];
1242 let mut stale_hits = 0usize;
1243
1244 for step in 0..1_000 {
1245 let operation = (next_random(&mut seed) % operation_counts.len() as u64) as usize;
1246 operation_counts[operation] += 1;
1247 let server = servers[(next_random(&mut seed) % servers.len() as u64) as usize].clone();
1248 let file = files[(next_random(&mut seed) % files.len() as u64) as usize].clone();
1249
1250 match operation {
1251 0 | 1 => store.publish(
1252 server,
1253 file.clone(),
1254 vec![diag(
1255 file.to_str().unwrap(),
1256 step + 1,
1257 "seeded diagnostic",
1258 DiagnosticSeverity::Warning,
1259 )],
1260 ),
1261 2 => {
1262 let expected = store.mark_stale_for_file_linear_reference(&file);
1263 let actual = store.mark_stale_for_file(&file);
1264 assert_eq!(actual, expected);
1265 stale_hits += usize::from(actual.0);
1266 }
1267 3 => {
1268 store.clear_for_server_file(&server, &file);
1269 }
1270 4 => {
1271 store.clear_for_file(&file);
1272 }
1273 5 => {
1274 store.clear_for_server(&server);
1275 }
1276 6 => {
1277 store.clear_server(server.kind);
1278 }
1279 _ => unreachable!(),
1280 }
1281
1282 store.debug_assert_index_consistent();
1283 assert!(store.len() <= 7);
1284 }
1285
1286 assert!(operation_counts.into_iter().all(|count| count > 0));
1287 assert!(
1288 stale_hits > 0,
1289 "seeded sequence must stale existing entries"
1290 );
1291 }
1292
1293 #[test]
1294 fn touching_existing_entry_moves_it_to_end_of_lru() {
1295 let mut store = DiagnosticsStore::with_capacity(2);
1296 let key = server_key(ServerKind::Rust);
1297
1298 store.publish(
1299 key.clone(),
1300 PathBuf::from("/a.rs"),
1301 vec![diag("/a.rs", 1, "a", DiagnosticSeverity::Warning)],
1302 );
1303 store.publish(
1304 key.clone(),
1305 PathBuf::from("/b.rs"),
1306 vec![diag("/b.rs", 1, "b", DiagnosticSeverity::Warning)],
1307 );
1308
1309 store.publish(
1312 key.clone(),
1313 PathBuf::from("/a.rs"),
1314 vec![diag("/a.rs", 1, "a2", DiagnosticSeverity::Error)],
1315 );
1316 store.publish(
1317 key.clone(),
1318 PathBuf::from("/c.rs"),
1319 vec![diag("/c.rs", 1, "c", DiagnosticSeverity::Warning)],
1320 );
1321
1322 assert!(store.has_any_report_for_file(Path::new("/a.rs")));
1323 assert!(!store.has_any_report_for_file(Path::new("/b.rs")));
1324 assert!(store.has_any_report_for_file(Path::new("/c.rs")));
1325 }
1326
1327 #[test]
1328 fn capacity_zero_disables_eviction() {
1329 let mut store = DiagnosticsStore::with_capacity(0);
1330 let key = server_key(ServerKind::Rust);
1331
1332 for i in 0..50 {
1333 store.publish(
1334 key.clone(),
1335 PathBuf::from(format!("/f{i}.rs")),
1336 vec![diag(
1337 &format!("/f{i}.rs"),
1338 1,
1339 "x",
1340 DiagnosticSeverity::Warning,
1341 )],
1342 );
1343 }
1344 assert_eq!(store.len(), 50);
1345 }
1346
1347 #[test]
1348 fn set_capacity_evicts_on_shrink() {
1349 let mut store = DiagnosticsStore::with_capacity(0);
1350 let key = server_key(ServerKind::Rust);
1351 for i in 0..10 {
1352 store.publish(
1353 key.clone(),
1354 PathBuf::from(format!("/f{i}.rs")),
1355 vec![diag(
1356 &format!("/f{i}.rs"),
1357 1,
1358 "x",
1359 DiagnosticSeverity::Warning,
1360 )],
1361 );
1362 }
1363 assert_eq!(store.len(), 10);
1364
1365 store.set_capacity(3);
1366 assert_eq!(store.len(), 3);
1367 assert!(store.has_any_report_for_file(Path::new("/f9.rs")));
1369 assert!(!store.has_any_report_for_file(Path::new("/f0.rs")));
1370 }
1371
1372 #[test]
1373 fn epoch_increments_monotonically() {
1374 let mut store = DiagnosticsStore::new();
1375 let key = server_key(ServerKind::Rust);
1376 let file = PathBuf::from("/e.rs");
1377
1378 store.publish(key.clone(), file.clone(), Vec::new());
1379 let e1 = store.entries_for_file(&file)[0].1.epoch;
1380
1381 store.publish(key.clone(), file.clone(), Vec::new());
1382 let e2 = store.entries_for_file(&file)[0].1.epoch;
1383
1384 assert!(e2 > e1, "epoch must increase on republish");
1385 }
1386
1387 #[test]
1388 fn result_id_is_round_tripped() {
1389 let mut store = DiagnosticsStore::new();
1390 let key = server_key(ServerKind::Rust);
1391 let file = PathBuf::from("/r.rs");
1392
1393 store.publish_with_result_id(
1394 key.clone(),
1395 file.clone(),
1396 Vec::new(),
1397 Some("rev-42".to_string()),
1398 );
1399
1400 let entries = store.entries_for_file(&file);
1401 assert_eq!(entries[0].1.result_id.as_deref(), Some("rev-42"));
1402 }
1403
1404 #[test]
1405 fn clear_server_drops_all_entries_for_kind() {
1406 let mut store = DiagnosticsStore::new();
1407 let py_key = server_key(ServerKind::Python);
1408 let rust_key = server_key(ServerKind::Rust);
1409
1410 store.publish(
1411 py_key.clone(),
1412 PathBuf::from("/a.py"),
1413 vec![diag("/a.py", 1, "x", DiagnosticSeverity::Error)],
1414 );
1415 store.publish(
1416 rust_key.clone(),
1417 PathBuf::from("/b.rs"),
1418 vec![diag("/b.rs", 1, "y", DiagnosticSeverity::Error)],
1419 );
1420
1421 store.clear_server(ServerKind::Python);
1422 assert!(!store.has_any_report_for_file(Path::new("/a.py")));
1423 assert!(store.has_any_report_for_file(Path::new("/b.rs")));
1424 }
1425
1426 #[test]
1427 fn clear_for_file_drops_every_server_entry_and_updates_counts() {
1428 let mut store = DiagnosticsStore::new();
1429 let py_key = server_key(ServerKind::Python);
1430 let biome_key = server_key(ServerKind::Biome);
1431
1432 store.publish(
1435 py_key,
1436 PathBuf::from("/gone.ts"),
1437 vec![diag("/gone.ts", 4, "type error", DiagnosticSeverity::Error)],
1438 );
1439 store.publish(
1440 biome_key,
1441 PathBuf::from("/gone.ts"),
1442 vec![diag(
1443 "/gone.ts",
1444 7,
1445 "lint warning",
1446 DiagnosticSeverity::Warning,
1447 )],
1448 );
1449 store.publish(
1450 server_key(ServerKind::Rust),
1451 PathBuf::from("/keep.rs"),
1452 vec![diag("/keep.rs", 1, "live error", DiagnosticSeverity::Error)],
1453 );
1454
1455 assert_eq!(store.error_warning_counts(), (2, 1));
1456
1457 let removed = store.clear_for_file(Path::new("/gone.ts"));
1459 assert!(removed);
1460 assert!(!store.has_any_report_for_file(Path::new("/gone.ts")));
1461 assert!(store.has_any_report_for_file(Path::new("/keep.rs")));
1463 assert_eq!(store.error_warning_counts(), (1, 0));
1464
1465 assert!(!store.clear_for_file(Path::new("/gone.ts")));
1467 }
1468
1469 #[test]
1470 fn filtered_counts_apply_keep_predicate() {
1471 let mut store = DiagnosticsStore::new();
1472 store.publish(
1473 server_key(ServerKind::TypeScript),
1474 PathBuf::from("/repo/src/app.ts"),
1475 vec![diag(
1476 "/repo/src/app.ts",
1477 1,
1478 "in build",
1479 DiagnosticSeverity::Error,
1480 )],
1481 );
1482 store.publish(
1483 server_key(ServerKind::TypeScript),
1484 PathBuf::from("/repo/src/app.test.ts"),
1485 vec![diag(
1486 "/repo/src/app.test.ts",
1487 1,
1488 "excluded",
1489 DiagnosticSeverity::Error,
1490 )],
1491 );
1492
1493 assert_eq!(store.error_warning_counts(), (2, 0));
1495 let counts = store.filtered_error_warning_counts(|file| !file.ends_with("app.test.ts"));
1497 assert_eq!(counts, (1, 0));
1498 }
1499
1500 #[test]
1501 fn filtered_counts_dedup_across_servers() {
1502 let mut store = DiagnosticsStore::new();
1503 let file = "/repo/src/app.ts";
1504 store.publish(
1509 server_key(ServerKind::TypeScript),
1510 PathBuf::from(file),
1511 vec![diag(file, 7, "dup", DiagnosticSeverity::Error)],
1512 );
1513 store.publish(
1514 server_key(ServerKind::Biome),
1515 PathBuf::from(file),
1516 vec![diag(file, 7, "dup", DiagnosticSeverity::Error)],
1517 );
1518
1519 assert_eq!(store.error_warning_counts(), (2, 0));
1520 assert_eq!(store.filtered_error_warning_counts(|_| true), (1, 0));
1521 }
1522
1523 #[test]
1524 fn filtered_counts_keep_distinct_diagnostics_same_file() {
1525 let mut store = DiagnosticsStore::new();
1526 let file = "/repo/src/app.ts";
1527 store.publish(
1530 server_key(ServerKind::TypeScript),
1531 PathBuf::from(file),
1532 vec![diag(file, 7, "type error", DiagnosticSeverity::Error)],
1533 );
1534 store.publish(
1535 server_key(ServerKind::Biome),
1536 PathBuf::from(file),
1537 vec![diag(file, 12, "lint warn", DiagnosticSeverity::Warning)],
1538 );
1539 assert_eq!(store.filtered_error_warning_counts(|_| true), (1, 1));
1540 }
1541
1542 #[test]
1543 fn filtered_counts_exclude_environmental_diagnostics() {
1544 let mut store = DiagnosticsStore::new();
1545 let file = "/repo/src/app.ts";
1546 store.publish(
1547 server_key(ServerKind::TypeScript),
1548 PathBuf::from(file),
1549 vec![
1550 diag(
1551 file,
1552 1,
1553 "Cannot find name 'foo'.",
1554 DiagnosticSeverity::Error,
1555 ),
1556 diag(
1557 file,
1558 2,
1559 "Failed to load schema from https://cdn.example/pkg/schema.json",
1560 DiagnosticSeverity::Error,
1561 ),
1562 ],
1563 );
1564 assert_eq!(store.error_warning_counts(), (2, 0));
1565 assert_eq!(
1566 store.filtered_error_warning_counts(|_| true),
1567 (1, 0),
1568 "environmental schema-fetch must not inflate E count"
1569 );
1570 }
1571
1572 #[test]
1573 fn environmental_flap_does_not_change_filtered_counts() {
1574 let mut store = DiagnosticsStore::new();
1575 let file = "/repo/package.json";
1576 let key = server_key(ServerKind::TypeScript);
1577 let env_msg =
1578 "Failed to fetch schema from https://json.schemastore.org/package.json: network";
1579
1580 assert_eq!(store.filtered_error_warning_counts(|_| true), (0, 0));
1581
1582 store.publish(
1583 key.clone(),
1584 PathBuf::from(file),
1585 vec![diag(file, 1, env_msg, DiagnosticSeverity::Error)],
1586 );
1587 assert_eq!(
1588 store.filtered_error_warning_counts(|_| true),
1589 (0, 0),
1590 "publish environmental diagnostic must not change filtered E/W"
1591 );
1592
1593 store.publish(key, PathBuf::from(file), vec![]);
1594 assert_eq!(
1595 store.filtered_error_warning_counts(|_| true),
1596 (0, 0),
1597 "removing environmental diagnostic must not change filtered E/W"
1598 );
1599 }
1600
1601 #[test]
1602 fn mixed_syntax_and_schema_fetch_counts_one_error() {
1603 let mut store = DiagnosticsStore::new();
1604 let file = "/repo/src/mixed.ts";
1605 store.publish(
1606 server_key(ServerKind::TypeScript),
1607 PathBuf::from(file),
1608 vec![
1609 diag(
1610 file,
1611 3,
1612 "Cannot find name 'bar'.",
1613 DiagnosticSeverity::Error,
1614 ),
1615 diag(
1616 file,
1617 1,
1618 "Failed to resolve schema https://example.com/x.json",
1619 DiagnosticSeverity::Error,
1620 ),
1621 ],
1622 );
1623 assert_eq!(
1624 store.filtered_error_warning_counts(|_| true),
1625 (1, 0),
1626 "classifier is per-diagnostic: one real syntax error => E1"
1627 );
1628 }
1629
1630 #[test]
1631 fn authoritative_report_requires_settled_non_stale_entry() {
1632 let mut store = DiagnosticsStore::new();
1633 let key = server_key(ServerKind::Rust);
1634 let file = PathBuf::from("/tmp/auth.rs");
1635
1636 assert!(!store.has_authoritative_report_for_file(&file));
1638
1639 store.publish_full_with_provisional(
1641 key.clone(),
1642 file.clone(),
1643 vec![diag(
1644 "/tmp/auth.rs",
1645 1,
1646 "warming",
1647 DiagnosticSeverity::Error,
1648 )],
1649 None,
1650 None,
1651 true,
1652 );
1653 assert!(store.has_any_fresh_report_for_file(&file));
1654 assert!(!store.has_authoritative_report_for_file(&file));
1655
1656 store.promote_provisional_for_server(&key);
1658 assert!(store.has_authoritative_report_for_file(&file));
1659
1660 assert!(store.mark_stale_for_file(&file).0);
1662 assert!(!store.has_authoritative_report_for_file(&file));
1663 }
1664
1665 #[test]
1666 fn authoritative_report_includes_empty_checked_clean() {
1667 let mut store = DiagnosticsStore::new();
1668 let file = PathBuf::from("/tmp/clean.rs");
1669 store.publish(server_key(ServerKind::Rust), file.clone(), Vec::new());
1670 assert!(
1671 store.has_authoritative_report_for_file(&file),
1672 "an empty checked-clean report still proves the file was analyzed"
1673 );
1674 }
1675}