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