use std::cmp::Ordering;
use std::path::PathBuf;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, clap::ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Info,
#[default]
Warning,
Error,
}
impl Severity {
pub const fn rank(self) -> u8 {
match self {
Self::Info => 0,
Self::Warning => 1,
Self::Error => 2,
}
}
}
impl std::fmt::Display for Severity {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Info => write!(formatter, "info"),
Self::Warning => write!(formatter, "warning"),
Self::Error => write!(formatter, "error"),
}
}
}
impl FromStr for Severity {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.to_ascii_lowercase().as_str() {
"info" | "suggestion" => Ok(Self::Info),
"warning" | "warn" => Ok(Self::Warning),
"error" => Ok(Self::Error),
_ => Err(format!("unknown severity `{value}`")),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Span {
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Diagnostic {
pub rule: String,
pub severity: Severity,
pub file: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub column: Option<usize>,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub suggestion: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reference: Option<String>,
}
impl Diagnostic {
pub fn new(
rule: &str,
severity: Severity,
file: impl Into<PathBuf>,
message: impl Into<String>,
) -> Self {
Self {
rule: rule.to_owned(),
severity,
file: file.into(),
line: None,
column: None,
message: message.into(),
suggestion: None,
reference: None,
}
}
#[must_use]
pub const fn at(mut self, line: usize, column: usize) -> Self {
self.line = Some(line);
self.column = Some(column);
self
}
#[must_use]
pub fn suggest(mut self, suggestion: impl Into<String>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
#[must_use]
pub fn reference(mut self, reference: impl Into<String>) -> Self {
self.reference = Some(reference.into());
self
}
}
pub fn sort_diagnostics(items: &mut [Diagnostic]) {
items.sort_by(|left, right| {
left.file
.cmp(&right.file)
.then_with(|| left.line.cmp(&right.line))
.then_with(|| left.column.cmp(&right.column))
.then_with(|| match right.severity.rank().cmp(&left.severity.rank()) {
Ordering::Equal => left.rule.cmp(&right.rule),
ordering => ordering,
})
.then_with(|| left.message.cmp(&right.message))
});
}