use lsp_types::{Diagnostic as LspDiagnostic, Uri};
use tracing::warn;
use super::{ReportedSeverity, message_as_str};
use crate::util::{truncate_str, truncate_string};
pub(super) const MAX_ENTRY_TEXT_BYTES: usize = 256 * 1024;
pub(super) const MAX_DIAGNOSTICS_ENTRY_BYTES: usize = 1024 * 1024;
#[derive(Debug)]
pub struct BoundedDiagnostics(pub(super) Vec<LspDiagnostic>);
impl BoundedDiagnostics {
pub(crate) fn into_vec(self) -> Vec<LspDiagnostic> {
self.0
}
pub(crate) fn as_slice(&self) -> &[LspDiagnostic] {
&self.0
}
pub(crate) fn new(uri: &Uri, mut diagnostics: Vec<LspDiagnostic>) -> Self {
for diagnostic in &mut diagnostics {
let placeholder = lsp_types::Message::String(String::new());
diagnostic.message = truncate_message(
std::mem::replace(&mut diagnostic.message, placeholder),
MAX_ENTRY_TEXT_BYTES,
);
}
cap_diagnostics_entry_size(uri, &mut diagnostics);
Self(diagnostics)
}
}
const DIAGNOSTIC_ESTIMATE_OVERHEAD_BYTES: usize = 256;
const JSON_ESCAPE_WORST_CASE_FACTOR: usize = 6;
pub(super) const DIAGNOSTIC_TERMINAL_FALLBACK_MESSAGE_BYTES: usize = 1024;
fn largest_fitting_prefix(
diagnostics: &[LspDiagnostic],
fits: impl Fn(&[LspDiagnostic]) -> bool,
) -> usize {
let (mut lo, mut hi) = (0usize, diagnostics.len());
#[allow(
clippy::arithmetic_side_effects,
clippy::indexing_slicing,
reason = "binary search over 0..=len: lo < hi gives mid in 1..=hi"
)]
while lo < hi {
let mid = lo + (hi - lo).div_ceil(2);
if fits(&diagnostics[..mid]) {
lo = mid;
} else {
hi = mid - 1;
}
}
lo
}
pub(super) fn truncate_message(
message: lsp_types::Message,
max_bytes: usize,
) -> lsp_types::Message {
match message {
lsp_types::Message::String(s) => lsp_types::Message::String(truncate_string(s, max_bytes)),
lsp_types::Message::MarkupContent(mut m) => {
m.value = truncate_string(m.value, max_bytes);
lsp_types::Message::MarkupContent(m)
}
}
}
fn cheap_size_estimate(diagnostics: &[LspDiagnostic]) -> Option<usize> {
let cheaply_estimable = diagnostics.iter().all(|d| {
d.data.is_none()
&& d.code_description.is_none()
&& d.related_information.is_none()
&& d.tags.is_none()
});
cheaply_estimable.then(|| {
diagnostics
.iter()
.map(|d| {
let raw_string_bytes = message_as_str(&d.message)
.len()
.saturating_add(d.source.as_deref().map_or(0, str::len))
.saturating_add(match &d.code {
Some(lsp_types::Code::String(s)) => s.len(),
_ => 0,
});
raw_string_bytes
.saturating_mul(JSON_ESCAPE_WORST_CASE_FACTOR)
.saturating_add(DIAGNOSTIC_ESTIMATE_OVERHEAD_BYTES)
})
.sum()
})
}
impl BoundedDiagnostics {
pub(crate) fn buffered_bytes(&self) -> usize {
cheap_size_estimate(&self.0).unwrap_or_else(|| {
serde_json::to_vec(&self.0).map_or(MAX_DIAGNOSTICS_ENTRY_BYTES, |bytes| bytes.len())
})
}
}
pub(super) fn cap_diagnostics_entry_size(uri: &Uri, diagnostics: &mut Vec<LspDiagnostic>) {
let fits = |ds: &[LspDiagnostic]| {
serde_json::to_vec(ds).is_ok_and(|bytes| bytes.len() <= MAX_DIAGNOSTICS_ENTRY_BYTES)
};
if cheap_size_estimate(diagnostics)
.is_some_and(|estimated| estimated <= MAX_DIAGNOSTICS_ENTRY_BYTES)
{
return;
}
if fits(diagnostics) {
return;
}
let original_count = diagnostics.len();
diagnostics.sort_by_key(ReportedSeverity::of);
let keep = largest_fitting_prefix(diagnostics, fits).max(1);
diagnostics.truncate(keep);
if diagnostics.len() < original_count {
warn!(
"diagnostics for {} exceeded the {MAX_DIAGNOSTICS_ENTRY_BYTES}-byte cache cap; kept \
the {} highest-severity of {original_count} diagnostics",
uri.as_ref(),
diagnostics.len(),
);
}
if diagnostics.len() == 1 && !fits(diagnostics) {
#[allow(clippy::indexing_slicing, reason = "len == 1 checked above")]
let diagnostic = &mut diagnostics[0];
let had_data = diagnostic.data.is_some();
diagnostic.data = None;
diagnostic.code_description = None;
diagnostic.related_information = None;
diagnostic.tags = None;
warn!(
"diagnostic for {} exceeded the cache cap; dropped its data/code_description/\
related_information/tags fields{}",
uri.as_ref(),
if had_data {
" (a later code-action request for this diagnostic may not resolve its quick fix)"
} else {
""
},
);
}
if diagnostics.len() == 1 && !fits(diagnostics) {
#[allow(clippy::indexing_slicing, reason = "len == 1 checked above")]
let diagnostic = &mut diagnostics[0];
if let Some(source) = &diagnostic.source {
diagnostic.source = Some(truncate_str(source, MAX_ENTRY_TEXT_BYTES));
}
if let Some(lsp_types::Code::String(code)) = &diagnostic.code {
diagnostic.code = Some(lsp_types::Code::String(truncate_str(
code,
MAX_ENTRY_TEXT_BYTES,
)));
}
}
if !fits(diagnostics) {
diagnostics.truncate(1);
if let Some(diagnostic) = diagnostics.first_mut() {
let placeholder = lsp_types::Message::String(String::new());
diagnostic.message = truncate_message(
std::mem::replace(&mut diagnostic.message, placeholder),
DIAGNOSTIC_TERMINAL_FALLBACK_MESSAGE_BYTES,
);
diagnostic.source = None;
diagnostic.code = None;
diagnostic.code_description = None;
diagnostic.related_information = None;
diagnostic.tags = None;
diagnostic.data = None;
}
warn!(
"diagnostic for {} still exceeded the cache cap after every other mitigation; \
truncated its message to {DIAGNOSTIC_TERMINAL_FALLBACK_MESSAGE_BYTES} bytes and \
cleared all other fields",
uri.as_ref(),
);
}
}