use super::code::{Code, Severity, Strictness};
use crate::span::ByteSpan;
use std::fmt;
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct ContextItem {
label: &'static str,
value: Box<str>,
}
impl ContextItem {
#[must_use]
pub fn new(label: &'static str, value: impl Into<Box<str>>) -> Self {
Self {
label,
value: value.into(),
}
}
#[must_use]
pub const fn label(&self) -> &'static str {
self.label
}
#[must_use]
pub fn value(&self) -> &str {
&self.value
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Diagnostic {
code: Code,
severity: Severity,
row_present: bool,
row: u64,
message: Option<Box<str>>,
span: Option<Box<ByteSpan>>,
category: Option<Box<str>>,
field: Option<Box<str>>,
context: Vec<ContextItem>,
}
impl Diagnostic {
#[must_use]
pub fn retained_bytes(&self) -> usize {
let text = self.message.as_ref().map_or(0, |value| value.len())
+ self.category.as_ref().map_or(0, |value| value.len())
+ self.field.as_ref().map_or(0, |value| value.len())
+ self
.context
.iter()
.map(|item| item.value.len())
.sum::<usize>();
text.saturating_add(
self.context
.capacity()
.saturating_mul(std::mem::size_of::<ContextItem>()),
)
.saturating_add(
self.span
.as_ref()
.map_or(0, |_| std::mem::size_of::<ByteSpan>()),
)
}
#[must_use]
pub fn new(code: Code) -> Self {
Self {
code,
severity: code.severity(),
row_present: false,
row: 0,
message: None,
span: None,
category: None,
field: None,
context: Vec::new(),
}
}
#[must_use]
pub fn with_message(mut self, message: impl Into<Box<str>>) -> Self {
self.message = Some(message.into());
self
}
#[must_use]
pub const fn with_severity(mut self, severity: Severity) -> Self {
self.severity = severity;
self
}
#[must_use]
pub fn at(mut self, span: ByteSpan) -> Self {
self.span = Some(Box::new(span));
self
}
#[must_use]
pub fn in_category(mut self, category: impl Into<Box<str>>) -> Self {
self.category = Some(category.into());
self
}
#[must_use]
pub fn about_field(mut self, field: impl Into<Box<str>>) -> Self {
self.field = Some(field.into());
self
}
#[must_use]
pub const fn at_row(mut self, row: u64) -> Self {
self.row = row;
self.row_present = true;
self
}
#[must_use]
pub fn with_context(mut self, label: &'static str, value: impl Into<Box<str>>) -> Self {
self.context.push(ContextItem::new(label, value));
self
}
#[must_use]
pub const fn code(&self) -> Code {
self.code
}
#[must_use]
pub const fn severity(&self) -> Severity {
self.severity
}
#[must_use]
pub fn message(&self) -> &str {
match &self.message {
Some(message) => message,
None => self.code.cause(),
}
}
#[must_use]
pub fn remedy(&self) -> &'static str {
self.code.remedy()
}
#[must_use]
pub fn span(&self) -> Option<ByteSpan> {
self.span.as_deref().copied()
}
#[must_use]
pub fn category(&self) -> Option<&str> {
self.category.as_deref()
}
#[must_use]
pub fn field(&self) -> Option<&str> {
self.field.as_deref()
}
#[must_use]
pub const fn row(&self) -> Option<u64> {
if self.row_present {
Some(self.row)
} else {
None
}
}
#[must_use]
pub fn context(&self) -> &[ContextItem] {
&self.context
}
#[must_use]
pub const fn is_error(&self, strictness: Strictness) -> bool {
self.severity.is_error(strictness)
}
fn order_key(&self) -> (std::cmp::Reverse<Severity>, u64, Code) {
let offset = self
.span
.as_deref()
.map_or(u64::MAX, |span| span.start.byte_offset);
(std::cmp::Reverse(self.severity), offset, self.code)
}
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}[{}]: {}",
self.severity.label(),
self.code,
self.message()
)
}
}
#[derive(Clone, Debug)]
pub struct Diagnostics {
findings: Vec<Diagnostic>,
worst: Option<Severity>,
ceiling: usize,
suppressed: u64,
}
impl Default for Diagnostics {
fn default() -> Self {
Self::new()
}
}
impl Diagnostics {
pub const DEFAULT_CEILING: usize = 10_000;
#[must_use]
pub const fn new() -> Self {
Self {
findings: Vec::new(),
worst: None,
ceiling: Self::DEFAULT_CEILING,
suppressed: 0,
}
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
findings: Vec::with_capacity(capacity),
worst: None,
ceiling: capacity.max(Self::DEFAULT_CEILING),
suppressed: 0,
}
}
#[must_use]
pub const fn with_ceiling(mut self, ceiling: usize) -> Self {
self.ceiling = ceiling;
self
}
#[must_use]
pub const fn ceiling(&self) -> usize {
self.ceiling
}
#[must_use]
pub const fn suppressed(&self) -> u64 {
self.suppressed
}
pub fn push(&mut self, finding: Diagnostic) {
self.worst = Some(match self.worst {
Some(worst) if worst >= finding.severity => worst,
_ => finding.severity,
});
if self.findings.len() >= self.ceiling {
self.suppressed = self.suppressed.saturating_add(1);
return;
}
self.findings.push(finding);
}
#[must_use]
pub const fn worst(&self) -> Option<Severity> {
self.worst
}
#[must_use]
pub fn has_error(&self, strictness: Strictness) -> bool {
self.worst.is_some_and(|worst| worst.is_error(strictness))
}
#[must_use]
pub fn as_slice(&self) -> &[Diagnostic] {
&self.findings
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.findings.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.findings.len()
}
#[must_use]
pub fn finish(mut self) -> Vec<Diagnostic> {
self.findings.sort_by_key(Diagnostic::order_key);
if self.suppressed != 0 {
self.findings.push(
Diagnostic::new(Code::W1901)
.with_context("suppressed", self.suppressed.to_string())
.with_context("retained", self.findings.len().to_string()),
);
}
self.findings
}
}
impl Extend<Diagnostic> for Diagnostics {
fn extend<T: IntoIterator<Item = Diagnostic>>(&mut self, iter: T) {
for finding in iter {
self.push(finding);
}
}
}
#[cfg(test)]
#[path = "finding_tests.rs"]
mod tests;