1use crate::span::Span;
6use serde::{Deserialize, Serialize};
7use std::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub enum Severity {
12 Error,
14 Warning,
16 Info,
18 Note,
20}
21
22impl fmt::Display for Severity {
23 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24 match self {
25 Self::Error => write!(f, "error"),
26 Self::Warning => write!(f, "warning"),
27 Self::Info => write!(f, "info"),
28 Self::Note => write!(f, "note"),
29 }
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct Label {
36 pub span: Span,
37 pub message: Option<String>,
38 pub style: LabelStyle,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42pub enum LabelStyle {
43 Primary,
44 Secondary,
45}
46
47impl Label {
48 pub fn primary(span: Span, message: impl Into<String>) -> Self {
49 Self {
50 span,
51 message: Some(message.into()),
52 style: LabelStyle::Primary,
53 }
54 }
55
56 pub fn secondary(span: Span, message: impl Into<String>) -> Self {
57 Self {
58 span,
59 message: Some(message.into()),
60 style: LabelStyle::Secondary,
61 }
62 }
63
64 pub const fn primary_unlabeled(span: Span) -> Self {
65 Self {
66 span,
67 message: None,
68 style: LabelStyle::Primary,
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct Diagnostic {
76 pub severity: Severity,
77 pub message: String,
78 pub labels: Vec<Label>,
79 pub notes: Vec<String>,
80 pub code: Option<String>,
81}
82
83impl Diagnostic {
84 pub fn error(message: impl Into<String>) -> Self {
86 Self {
87 severity: Severity::Error,
88 message: message.into(),
89 labels: Vec::new(),
90 notes: Vec::new(),
91 code: None,
92 }
93 }
94
95 pub fn warning(message: impl Into<String>) -> Self {
97 Self {
98 severity: Severity::Warning,
99 message: message.into(),
100 labels: Vec::new(),
101 notes: Vec::new(),
102 code: None,
103 }
104 }
105
106 pub fn info(message: impl Into<String>) -> Self {
108 Self {
109 severity: Severity::Info,
110 message: message.into(),
111 labels: Vec::new(),
112 notes: Vec::new(),
113 code: None,
114 }
115 }
116
117 pub fn with_label(mut self, label: Label) -> Self {
119 self.labels.push(label);
120 self
121 }
122
123 pub fn with_labels(mut self, labels: impl IntoIterator<Item = Label>) -> Self {
125 self.labels.extend(labels);
126 self
127 }
128
129 pub fn with_note(mut self, note: impl Into<String>) -> Self {
131 self.notes.push(note.into());
132 self
133 }
134
135 pub fn with_code(mut self, code: impl Into<String>) -> Self {
137 self.code = Some(code.into());
138 self
139 }
140
141 pub fn is_error(&self) -> bool {
143 self.severity == Severity::Error
144 }
145
146 pub fn is_warning(&self) -> bool {
148 self.severity == Severity::Warning
149 }
150}
151
152impl fmt::Display for Diagnostic {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 if let Some(code) = &self.code {
155 write!(f, "{} [{}]: {}", self.severity, code, self.message)?;
156 } else {
157 write!(f, "{}: {}", self.severity, self.message)?;
158 }
159
160 for note in &self.notes {
161 write!(f, "\n note: {note}")?;
162 }
163
164 Ok(())
165 }
166}
167
168pub fn edit_distance(a: &str, b: &str) -> usize {
170 let a_len = a.len();
171 let b_len = b.len();
172 if a_len == 0 {
173 return b_len;
174 }
175 if b_len == 0 {
176 return a_len;
177 }
178
179 let mut prev: Vec<usize> = (0..=b_len).collect();
180 let mut curr = vec![0; b_len + 1];
181
182 for (i, ca) in a.chars().enumerate() {
183 curr[0] = i + 1;
184 for (j, cb) in b.chars().enumerate() {
185 let cost = if ca == cb { 0 } else { 1 };
186 curr[j + 1] = (prev[j] + cost).min(prev[j + 1] + 1).min(curr[j] + 1);
187 }
188 std::mem::swap(&mut prev, &mut curr);
189 }
190 prev[b_len]
191}
192
193pub fn suggest_similar<'a>(query: &str, candidates: &[&'a str]) -> Option<&'a str> {
199 if query.is_empty() || candidates.is_empty() {
200 return None;
201 }
202 let max_distance = if query.len() <= 5 { 2 } else { 3 };
203 candidates
204 .iter()
205 .filter(|c| **c != query) .filter_map(|c| {
207 let d = edit_distance(query, c);
208 if d <= max_distance && d > 0 {
209 Some((*c, d))
210 } else {
211 None
212 }
213 })
214 .min_by_key(|(_, d)| *d)
215 .map(|(c, _)| c)
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[test]
223 fn test_diagnostic_error() {
224 let diag = Diagnostic::error("type mismatch")
225 .with_code("E0308")
226 .with_label(Label::primary(Span::new(10, 20, 0), "expected i32"))
227 .with_note("this is a note");
228
229 assert!(diag.is_error());
230 assert_eq!(diag.code, Some("E0308".to_string()));
231 assert_eq!(diag.labels.len(), 1);
232 assert_eq!(diag.notes.len(), 1);
233 }
234
235 #[test]
236 fn test_diagnostic_warning() {
237 let diag = Diagnostic::warning("unused variable");
238 assert!(diag.is_warning());
239 assert!(!diag.is_error());
240 }
241
242 #[test]
243 fn test_diagnostic_display() {
244 let diag = Diagnostic::error("test error").with_code("E0001");
245 let display = format!("{}", diag);
246 assert!(display.contains("error"));
247 assert!(display.contains("E0001"));
248 assert!(display.contains("test error"));
249 }
250
251 #[test]
252 fn test_edit_distance() {
253 assert_eq!(edit_distance("", ""), 0);
254 assert_eq!(edit_distance("abc", "abc"), 0);
255 assert_eq!(edit_distance("abc", "abd"), 1);
256 assert_eq!(edit_distance("abc", "ab"), 1);
257 assert_eq!(edit_distance("kitten", "sitting"), 3);
258 }
259
260 #[test]
261 fn test_suggest_similar() {
262 let candidates = &["count", "counter", "amount", "total"];
263 assert_eq!(suggest_similar("cont", candidates), Some("count"));
264 assert_eq!(suggest_similar("amout", candidates), Some("amount"));
265 assert_eq!(suggest_similar("xyz", candidates), None);
266 assert_eq!(suggest_similar("count", candidates), Some("counter")); }
268}