1use std::cmp::Ordering;
2use std::path::PathBuf;
3use std::str::FromStr;
4
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, clap::ValueEnum)]
8#[serde(rename_all = "lowercase")]
9pub enum Severity {
10 Info,
11 #[default]
12 Warning,
13 Error,
14}
15
16impl Severity {
17 pub const fn rank(self) -> u8 {
18 match self {
19 Self::Info => 0,
20 Self::Warning => 1,
21 Self::Error => 2,
22 }
23 }
24}
25
26impl std::fmt::Display for Severity {
27 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 Self::Info => write!(formatter, "info"),
30 Self::Warning => write!(formatter, "warning"),
31 Self::Error => write!(formatter, "error"),
32 }
33 }
34}
35
36impl FromStr for Severity {
37 type Err = String;
38
39 fn from_str(value: &str) -> Result<Self, Self::Err> {
40 match value.to_ascii_lowercase().as_str() {
41 "info" | "suggestion" => Ok(Self::Info),
42 "warning" | "warn" => Ok(Self::Warning),
43 "error" => Ok(Self::Error),
44 _ => Err(format!("unknown severity `{value}`")),
45 }
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50pub struct Span {
51 pub line: usize,
52 pub column: usize,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct Diagnostic {
57 pub rule: String,
58 pub severity: Severity,
59 pub file: PathBuf,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub line: Option<usize>,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub column: Option<usize>,
64 pub message: String,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub suggestion: Option<String>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub reference: Option<String>,
69}
70
71impl Diagnostic {
72 pub fn new(
73 rule: &str,
74 severity: Severity,
75 file: impl Into<PathBuf>,
76 message: impl Into<String>,
77 ) -> Self {
78 Self {
79 rule: rule.to_owned(),
80 severity,
81 file: file.into(),
82 line: None,
83 column: None,
84 message: message.into(),
85 suggestion: None,
86 reference: None,
87 }
88 }
89
90 #[must_use]
91 pub const fn at(mut self, line: usize, column: usize) -> Self {
92 self.line = Some(line);
93 self.column = Some(column);
94 self
95 }
96
97 #[must_use]
98 pub fn suggest(mut self, suggestion: impl Into<String>) -> Self {
99 self.suggestion = Some(suggestion.into());
100 self
101 }
102
103 #[must_use]
104 pub fn reference(mut self, reference: impl Into<String>) -> Self {
105 self.reference = Some(reference.into());
106 self
107 }
108}
109
110pub fn sort_diagnostics(items: &mut [Diagnostic]) {
111 items.sort_by(|left, right| {
112 left.file
113 .cmp(&right.file)
114 .then_with(|| left.line.cmp(&right.line))
115 .then_with(|| left.column.cmp(&right.column))
116 .then_with(|| match right.severity.rank().cmp(&left.severity.rank()) {
117 Ordering::Equal => left.rule.cmp(&right.rule),
118 ordering => ordering,
119 })
120 .then_with(|| left.message.cmp(&right.message))
121 });
122}