use std::collections::BTreeMap;
use crate::lsp::{LspEvent, PublishDiagnostics};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BarrierState {
Waiting,
Settled,
}
#[derive(Debug, Clone)]
pub struct FlycheckBarrier {
stale_ends_remaining: u32,
window: BTreeMap<String, PublishDiagnostics>,
settled: bool,
}
impl FlycheckBarrier {
pub fn arm(prior_flycheck_in_flight: bool) -> Self {
Self::arm_skipping(u32::from(prior_flycheck_in_flight))
}
pub fn arm_skipping(stale_ends: u32) -> Self {
Self {
stale_ends_remaining: stale_ends,
window: BTreeMap::new(),
settled: false,
}
}
pub fn observe(&mut self, ev: &LspEvent) -> BarrierState {
if self.settled {
return BarrierState::Settled;
}
match ev {
LspEvent::Diagnostics(pd) => {
self.window.insert(pd.uri.clone(), pd.clone());
BarrierState::Waiting
}
LspEvent::FlycheckEnded => {
if self.stale_ends_remaining > 0 {
self.stale_ends_remaining -= 1;
self.window.clear();
BarrierState::Waiting
} else {
self.settled = true;
BarrierState::Settled
}
}
LspEvent::IndexingEnded => BarrierState::Waiting,
}
}
pub fn is_settled(&self) -> bool {
self.settled
}
pub fn snapshot(&self) -> &BTreeMap<String, PublishDiagnostics> {
&self.window
}
pub fn has_authoritative_error(&self) -> bool {
self.window
.values()
.any(PublishDiagnostics::has_authoritative_error)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Diagnostic, Severity};
fn diags(uri: &str, auth: usize) -> LspEvent {
let rich = (0..auth)
.map(|i| Diagnostic {
file_path: uri.into(),
line: (i as u32) + 1,
col: 1,
severity: Severity::Error,
code: Some("E0599".into()),
message: "no method".into(),
source: Some("rustc".into()),
})
.collect::<Vec<_>>();
LspEvent::Diagnostics(PublishDiagnostics {
uri: uri.into(),
authoritative_errors: auth,
advisory_errors: 0,
total: auth,
diagnostics: rich,
})
}
fn advisory(uri: &str) -> LspEvent {
LspEvent::Diagnostics(PublishDiagnostics {
uri: uri.into(),
authoritative_errors: 0,
advisory_errors: 1,
total: 1,
diagnostics: vec![Diagnostic {
file_path: uri.into(),
line: 1,
col: 1,
severity: Severity::Error,
code: None,
message: "native".into(),
source: Some("rust-analyzer".into()),
}],
})
}
#[test]
fn quiescent_prior_first_flycheck_end_settles() {
let mut b = FlycheckBarrier::arm(false);
assert_eq!(
b.observe(&diags("file:///w/a.rs", 0)),
BarrierState::Waiting
);
assert_eq!(
b.observe(&diags("file:///w/b.rs", 1)),
BarrierState::Waiting
);
assert!(!b.is_settled());
assert_eq!(b.observe(&LspEvent::FlycheckEnded), BarrierState::Settled);
assert!(b.is_settled());
assert!(b.has_authoritative_error(), "b.rs has a rustc error");
assert_eq!(b.snapshot().len(), 2);
}
#[test]
fn never_settles_before_any_flycheck_end() {
let mut b = FlycheckBarrier::arm(false);
for _ in 0..5 {
assert_eq!(
b.observe(&diags("file:///w/x.rs", 1)),
BarrierState::Waiting
);
}
assert_eq!(
b.observe(&LspEvent::IndexingEnded),
BarrierState::Waiting,
"indexing-end is NOT a verdict boundary (FIELD FINDING #3a)"
);
assert!(!b.is_settled(), "no flycheck-end seen ⇒ never settled");
}
#[test]
fn stale_in_flight_end_is_skipped_and_v_window_dropped() {
let mut b = FlycheckBarrier::arm(true);
assert_eq!(
b.observe(&diags("file:///v/only_v.rs", 3)),
BarrierState::Waiting
);
assert!(b.has_authoritative_error(), "V window currently has errors");
assert_eq!(b.observe(&LspEvent::FlycheckEnded), BarrierState::Waiting);
assert!(!b.is_settled(), "stale end must not settle the barrier");
assert!(
b.snapshot().is_empty(),
"V's window must be dropped at the stale boundary"
);
assert!(
!b.has_authoritative_error(),
"no V error may survive into W's window"
);
assert_eq!(
b.observe(&diags("file:///w/only_w.rs", 0)),
BarrierState::Waiting
);
assert_eq!(b.observe(&LspEvent::FlycheckEnded), BarrierState::Settled);
assert!(b.is_settled());
assert!(b.snapshot().contains_key("file:///w/only_w.rs"));
assert!(
!b.snapshot().contains_key("file:///v/only_v.rs"),
"no stale-V URI may appear in W's settled snapshot"
);
assert!(
!b.has_authoritative_error(),
"W is authoritatively green; V's red must not be attributed to W"
);
}
#[test]
fn per_uri_replace_semantics() {
let mut b = FlycheckBarrier::arm(false);
b.observe(&diags("file:///w/a.rs", 2)); b.observe(&diags("file:///w/a.rs", 0)); b.observe(&LspEvent::FlycheckEnded);
assert!(b.is_settled());
assert_eq!(b.snapshot().len(), 1, "same URI replaced, not appended");
assert!(
!b.has_authoritative_error(),
"the cleared (latest) publish wins → green"
);
}
#[test]
fn post_settle_events_are_no_ops() {
let mut b = FlycheckBarrier::arm(false);
b.observe(&diags("file:///w/a.rs", 0));
assert_eq!(b.observe(&LspEvent::FlycheckEnded), BarrierState::Settled);
assert_eq!(
b.observe(&diags("file:///w/a.rs", 5)),
BarrierState::Settled
);
assert_eq!(b.observe(&LspEvent::FlycheckEnded), BarrierState::Settled);
assert_eq!(b.observe(&LspEvent::IndexingEnded), BarrierState::Settled);
assert!(
!b.has_authoritative_error(),
"post-settle events must not mutate the latched verdict"
);
assert_eq!(b.snapshot().len(), 1);
}
#[test]
fn advisory_only_window_is_authoritatively_green() {
let mut b = FlycheckBarrier::arm(false);
b.observe(&advisory("file:///w/a.rs"));
b.observe(&LspEvent::FlycheckEnded);
assert!(b.is_settled());
assert!(
!b.has_authoritative_error(),
"advisory-only ⇒ authoritatively green (no rustc error)"
);
assert_eq!(
b.snapshot().len(),
1,
"the advisory publish is still tracked"
);
}
#[test]
fn multi_stale_end_robustness() {
let mut b = FlycheckBarrier::arm_skipping(2);
b.observe(&diags("file:///stale1.rs", 1));
assert_eq!(b.observe(&LspEvent::FlycheckEnded), BarrierState::Waiting); assert!(b.snapshot().is_empty());
b.observe(&diags("file:///stale2.rs", 1));
assert_eq!(b.observe(&LspEvent::FlycheckEnded), BarrierState::Waiting); assert!(b.snapshot().is_empty());
assert!(!b.is_settled(), "still owed W's own end");
b.observe(&diags("file:///w/clean.rs", 0));
assert_eq!(b.observe(&LspEvent::FlycheckEnded), BarrierState::Settled);
assert!(b.is_settled());
assert_eq!(b.snapshot().len(), 1);
assert!(!b.has_authoritative_error());
}
#[test]
fn indexing_end_does_not_drop_or_settle_window() {
let mut b = FlycheckBarrier::arm(false);
b.observe(&diags("file:///w/a.rs", 1));
assert_eq!(b.observe(&LspEvent::IndexingEnded), BarrierState::Waiting);
assert_eq!(
b.snapshot().len(),
1,
"indexing-end must not clear the window (only a stale flycheck-end does)"
);
assert_eq!(b.observe(&LspEvent::FlycheckEnded), BarrierState::Settled);
assert!(
b.has_authoritative_error(),
"the pre-indexing-end error survived to W's settled snapshot"
);
}
}