use oneyaml_core::{Diagnostic, DuplicateKeys, Label, Limits, Severity, Span, code};
use crate::error::Error;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct Dropped {
pub(crate) count: usize,
pub(crate) first: Option<usize>,
}
impl Dropped {
pub(crate) fn one(at: usize) -> Self {
Self {
count: 1,
first: Some(at),
}
}
#[must_use]
pub(crate) fn and(self, other: Self) -> Self {
let first = match (self.first, other.first) {
(Some(a), Some(b)) => Some(a.min(b)),
(a, b) => a.or(b),
};
Self {
count: self.count.saturating_add(other.count),
first,
}
}
}
#[derive(Debug)]
struct Kept {
start: usize,
seq: u64,
d: Diagnostic,
}
impl PartialEq for Kept {
fn eq(&self, other: &Self) -> bool {
(self.start, self.seq) == (other.start, other.seq)
}
}
impl Eq for Kept {}
impl PartialOrd for Kept {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Kept {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(self.start, self.seq).cmp(&(other.start, other.seq))
}
}
#[derive(Debug, Default)]
pub(crate) struct Capped {
heap: std::collections::BinaryHeap<Kept>,
keep: usize,
seq: u64,
error: Option<Kept>,
dropped: Dropped,
}
impl Capped {
pub(crate) fn new(limits: &Limits) -> Self {
Self::with_keep(limits.get_max_diagnostics().saturating_add(1))
}
pub(crate) fn with_keep(keep: usize) -> Self {
Self {
heap: std::collections::BinaryHeap::new(),
keep,
seq: 0,
error: None,
dropped: Dropped::default(),
}
}
pub(crate) fn wants_warning(&self, at: usize) -> bool {
self.heap.len() < self.keep || self.heap.peek().is_some_and(|m| at < m.start)
}
pub(crate) fn count(&mut self, at: usize) {
self.dropped = self.dropped.and(Dropped::one(at));
}
pub(crate) fn push(&mut self, d: Diagnostic) {
self.seq = self.seq.wrapping_add(1);
let k = Kept {
start: d.primary.span.start,
seq: self.seq,
d,
};
let out = if self.heap.len() < self.keep {
self.heap.push(k);
return;
} else if self.heap.peek().is_some_and(|m| k < *m) {
let out = self.heap.pop();
self.heap.push(k);
out
} else {
Some(k)
};
if let Some(k) = out {
self.drop_entry(k);
}
}
fn drop_entry(&mut self, k: Kept) {
let error = k.d.severity == Severity::Error && self.keep > 0;
if error && self.error.as_ref().is_none_or(|e| k < *e) {
if let Some(old) = self.error.replace(k) {
self.count(old.start);
}
} else {
self.count(k.start);
}
}
pub(crate) fn duplicate(
&mut self,
policy: DuplicateKeys,
key: impl FnOnce() -> String,
first: Span,
dup: Span,
) -> Result<(), Error> {
if policy == DuplicateKeys::LastWins && !self.wants_warning(dup.start) {
self.count(dup.start);
return Ok(());
}
let d = crate::construct::keys::on_duplicate(policy, &key(), first, dup)?;
self.push(d);
Ok(())
}
#[cfg(test)]
pub(crate) fn total(&self) -> usize {
self.heap
.len()
.saturating_add(usize::from(self.error.is_some()))
.saturating_add(self.dropped.count)
}
pub(crate) fn take(&mut self) -> (Vec<Diagnostic>, Dropped) {
let heap = std::mem::take(&mut self.heap);
let mut kept: Vec<Diagnostic> = heap.into_sorted_vec().into_iter().map(|k| k.d).collect();
if let Some(e) = self.error.take() {
let at = kept.partition_point(|d| d.primary.span.start <= e.start);
kept.insert(at, e.d);
}
(kept, std::mem::take(&mut self.dropped))
}
pub(crate) fn into_parts(mut self) -> (Vec<Diagnostic>, Dropped) {
self.take()
}
}
pub(crate) fn filled(mut d: Diagnostic, limit: usize) -> Diagnostic {
if !oneyaml_syntax::tree::SyntaxTree::is_skeleton(&d) {
return d;
}
d.message = d
.code
.info()
.map_or_else(|| d.code.to_string(), |i| i.summary.to_owned());
d.help = Some(format!(
"its full message was not kept: the input has more diagnostics than \
Limits::max_diagnostics (currently {limit}); raise it to see it"
));
d
}
pub(crate) fn summary(count: usize, limit: usize, at: usize) -> Diagnostic {
let noun = if count == 1 {
"diagnostic"
} else {
"diagnostics"
};
Diagnostic::warning(
code::DIAGNOSTICS_SUPPRESSED,
format!("{count} more {noun} suppressed"),
Label::new(Span::empty(at), "suppressed from here on"),
)
.with_help(format!(
"raise Limits::max_diagnostics (currently {limit}) to see them"
))
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Cut {
pub(crate) limit: usize,
pub(crate) error: Option<usize>,
}
impl Cut {
pub(crate) fn before(self, b: usize) -> usize {
let mut n = b.min(self.limit);
if self.limit < b {
n = n.saturating_add(1);
}
if self.error.is_some_and(|e| e < b) {
n = n.saturating_add(1);
}
n
}
}
pub(crate) fn cap(list: &mut Vec<Diagnostic>, extra: Dropped, limit: usize) -> Option<Cut> {
let total = list.len().saturating_add(extra.count);
if total <= limit {
return None;
}
let error = list
.iter()
.position(|d| d.severity == Severity::Error)
.filter(|e| *e >= limit);
let (at, internal) = match (list.get(limit), extra.first) {
(Some(d), _) => (d.primary.span.start, None),
(None, Some(f)) => (f, None),
(None, None) => {
debug_assert!(false, "capped list without its first suppressed entry");
let at = list.last().map_or(0, |d| d.primary.span.end);
let e = Error::internal(
"capped diagnostics without their first suppressed entry",
Some(Span::empty(at)),
);
(at, Some(e.diagnostic().clone()))
}
};
let first_error = error.and_then(|e| list.get(e).cloned());
list.truncate(limit);
let kept = list
.len()
.saturating_add(usize::from(first_error.is_some()));
list.push(summary(total.saturating_sub(kept), limit, at));
list.extend(first_error);
list.extend(internal);
Some(Cut { limit, error })
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::indexing_slicing)]
use super::*;
fn w(at: usize) -> Diagnostic {
Diagnostic::warning(
code::LENIENT_INLINE_COLON,
"w",
Label::new(Span::empty(at), ""),
)
}
#[test]
#[cfg_attr(
debug_assertions,
should_panic(expected = "capped list without its first suppressed entry")
)]
fn missing_first_offset_is_internal() {
let mut l = vec![w(1)];
let extra = Dropped {
count: 3,
first: None,
};
cap(&mut l, extra, 1).unwrap();
assert_eq!(l.len(), 3);
assert_eq!(l[1].code, code::DIAGNOSTICS_SUPPRESSED);
assert_eq!(l[2].code, code::INTERNAL_ERROR);
assert_eq!(l[2].severity, Severity::Error);
}
fn e(at: usize) -> Diagnostic {
Diagnostic::error(code::INVALID_ESCAPE, "e", Label::new(Span::empty(at), ""))
}
#[test]
fn within_the_limit_nothing_changes() {
let mut l = vec![w(0), w(1)];
assert!(cap(&mut l, Dropped::default(), 2).is_none());
assert_eq!(l.len(), 2);
}
#[test]
fn one_over_keeps_the_limit_and_a_summary() {
let mut l = vec![w(0), w(1), w(2)];
let cut = cap(&mut l, Dropped::default(), 2).unwrap();
assert_eq!(l.len(), 3);
assert_eq!(l[2].code, code::DIAGNOSTICS_SUPPRESSED);
assert_eq!(l[2].message, "1 more diagnostic suppressed");
assert_eq!(l[2].primary.span, Span::empty(2));
assert_eq!(l[2].severity, Severity::Warning);
assert_eq!(cut.before(3), 3);
assert_eq!(cut.before(1), 1);
}
#[test]
fn the_first_error_is_kept_and_counts_are_exact() {
let mut l = vec![w(0), w(1), w(2), e(3), e(4)];
let cut = cap(
&mut l,
Dropped {
count: 7,
first: Some(9),
},
1,
)
.unwrap();
assert_eq!(l.len(), 3);
assert_eq!(l[1].message, "10 more diagnostics suppressed");
assert_eq!(l[2].primary.span, Span::empty(3));
assert_eq!(cut.before(5), 3);
assert_eq!(cut.before(3), 2);
}
#[test]
fn a_short_list_places_the_summary_at_the_first_dropped_entry() {
let mut l = vec![w(0)];
cap(
&mut l,
Dropped {
count: 3,
first: Some(40),
},
2,
)
.unwrap();
assert_eq!(l[1].primary.span, Span::empty(40));
assert_eq!(l[1].message, "3 more diagnostics suppressed");
}
#[test]
fn skeletons_are_filled_with_their_summary() {
let mut d = e(3);
d.message = String::new();
let f = filled(d, 7);
assert_eq!(
f.message,
"invalid escape sequence in a double-quoted scalar"
);
assert!(f.help.unwrap().contains("currently 7"));
assert_eq!(filled(w(1), 7).message, "w");
}
#[test]
fn a_limit_of_zero_keeps_only_the_summary_and_the_first_error() {
let mut l = vec![w(0), e(1)];
cap(&mut l, Dropped::default(), 0).unwrap();
assert_eq!(l.len(), 2);
assert_eq!(l[0].code, code::DIAGNOSTICS_SUPPRESSED);
assert_eq!(l[1].severity, Severity::Error);
}
#[test]
fn collectors_keep_limit_plus_one_and_the_first_error() {
let mut c = Capped::new(&Limits::default().max_diagnostics(1));
for i in 0..5 {
c.push(w(i));
}
c.push(e(5));
c.push(e(6));
let (items, dropped) = c.into_parts();
assert_eq!(items.len(), 3);
assert_eq!(
dropped,
Dropped {
count: 4,
first: Some(2)
}
);
let mut n = Capped::with_keep(0);
n.push(e(0));
n.push(w(1));
assert_eq!(n.total(), 2);
assert_eq!(n.into_parts().0.len(), 0);
}
}