Skip to main content

nvrim/
diagnostics.rs

1//! Diagnostic processing utilities for LSP diagnostics.
2//!
3//! This module provides functionality to filter, format, and sort LSP diagnostics
4//! received from language servers in Nvim.
5
6use core::fmt;
7
8use nvim_oxi::Dictionary;
9use serde::de::Deserializer;
10use serde::de::Visitor;
11use strum::EnumCount;
12use strum::EnumIter;
13
14mod config;
15mod filter;
16mod filters;
17mod formatter;
18mod sorter;
19
20/// [`Dictionary`] of diagnostic processing helpers.
21pub fn dict() -> Dictionary {
22    dict! {
23        "filter": fn_from!(filter::filter),
24        "sort": fn_from!(sorter::sort),
25        "format": fn_from!(formatter::format),
26        "config": config::get()
27    }
28}
29
30/// Diagnostic severity levels.
31///
32/// Variant order defines iteration order via [`EnumIter`] for stable rendering.
33#[derive(Clone, Copy, Debug, EnumCount, EnumIter, Eq, Hash, PartialEq)]
34#[allow(clippy::upper_case_acronyms)]
35pub enum DiagnosticSeverity {
36    Error,
37    Warn,
38    Info,
39    Hint,
40    Other,
41}
42
43impl DiagnosticSeverity {
44    /// Number of declared severity variants.
45    pub const VARIANT_COUNT: usize = <Self as strum::EnumCount>::COUNT;
46
47    /// Returns the canonical LSP severity number (1-4, or 0 for Other).
48    pub const fn to_number(self) -> u8 {
49        match self {
50            Self::Error => 1,
51            Self::Warn => 2,
52            Self::Info => 3,
53            Self::Hint => 4,
54            Self::Other => 0,
55        }
56    }
57
58    /// Returns the canonical single-character symbol for this severity.
59    pub const fn symbol(self) -> &'static str {
60        match self {
61            Self::Error => "E",
62            Self::Warn => "W",
63            Self::Info => "I",
64            Self::Hint => "H",
65            Self::Other => "",
66        }
67    }
68}
69
70/// Deserializes numeric, string, or text alias severity representations.
71impl<'de> serde::Deserialize<'de> for DiagnosticSeverity {
72    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
73    where
74        D: Deserializer<'de>,
75    {
76        struct SevVisitor;
77
78        impl Visitor<'_> for SevVisitor {
79            type Value = DiagnosticSeverity;
80
81            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
82                write!(f, "a severity: 1-4 or (error|warn|info|hint) or short alias")
83            }
84
85            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
86            where
87                E: serde::de::Error,
88            {
89                Ok(match v {
90                    1 => DiagnosticSeverity::Error,
91                    2 => DiagnosticSeverity::Warn,
92                    3 => DiagnosticSeverity::Info,
93                    4 => DiagnosticSeverity::Hint,
94                    _ => DiagnosticSeverity::Other,
95                })
96            }
97
98            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
99            where
100                E: serde::de::Error,
101            {
102                if v < 0 {
103                    return Ok(DiagnosticSeverity::Other);
104                }
105                self.visit_u64(v.cast_unsigned())
106            }
107
108            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
109            where
110                E: serde::de::Error,
111            {
112                let norm = s.trim().to_ascii_lowercase();
113                if let Ok(n) = norm.parse::<u64>() {
114                    return self.visit_u64(n);
115                }
116                Ok(match norm.as_str() {
117                    "error" | "err" | "e" => DiagnosticSeverity::Error,
118                    "warn" | "warning" | "w" => DiagnosticSeverity::Warn,
119                    "info" | "information" | "i" => DiagnosticSeverity::Info,
120                    "hint" | "h" => DiagnosticSeverity::Hint,
121                    _ => DiagnosticSeverity::Other,
122                })
123            }
124        }
125
126        deserializer.deserialize_any(SevVisitor)
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use rstest::rstest;
133    use strum::IntoEnumIterator;
134
135    use super::*;
136
137    #[rstest]
138    #[case("1", DiagnosticSeverity::Error)]
139    #[case("2", DiagnosticSeverity::Warn)]
140    #[case("3", DiagnosticSeverity::Info)]
141    #[case("4", DiagnosticSeverity::Hint)]
142    #[case("\"1\"", DiagnosticSeverity::Error)]
143    #[case("\"2\"", DiagnosticSeverity::Warn)]
144    #[case("\"3\"", DiagnosticSeverity::Info)]
145    #[case("\"4\"", DiagnosticSeverity::Hint)]
146    #[case("\"error\"", DiagnosticSeverity::Error)]
147    #[case("\"err\"", DiagnosticSeverity::Error)]
148    #[case("\"e\"", DiagnosticSeverity::Error)]
149    #[case("\"warn\"", DiagnosticSeverity::Warn)]
150    #[case("\"warning\"", DiagnosticSeverity::Warn)]
151    #[case("\"w\"", DiagnosticSeverity::Warn)]
152    #[case("\"info\"", DiagnosticSeverity::Info)]
153    #[case("\"information\"", DiagnosticSeverity::Info)]
154    #[case("\"i\"", DiagnosticSeverity::Info)]
155    #[case("\"hint\"", DiagnosticSeverity::Hint)]
156    #[case("\"h\"", DiagnosticSeverity::Hint)]
157    #[case("\" Error \"", DiagnosticSeverity::Error)]
158    #[case("\"WARNING\"", DiagnosticSeverity::Warn)]
159    #[case("\" Info \"", DiagnosticSeverity::Info)]
160    #[case("\"H\"", DiagnosticSeverity::Hint)]
161    #[case("-1", DiagnosticSeverity::Other)]
162    #[case("0", DiagnosticSeverity::Other)]
163    #[case("5", DiagnosticSeverity::Other)]
164    #[case("\"unknown\"", DiagnosticSeverity::Other)]
165    fn diagnostic_severity_deserializes_strings_as_expected(#[case] input: &str, #[case] expected: DiagnosticSeverity) {
166        assert2::assert!(let Ok(sev) = serde_json::from_str::<DiagnosticSeverity>(input));
167        assert_eq!(sev, expected);
168    }
169
170    #[test]
171    fn diagnostic_severity_when_iterated_via_enumiter_yields_declared_order_and_matches_variant_count() {
172        let expected = [
173            DiagnosticSeverity::Error,
174            DiagnosticSeverity::Warn,
175            DiagnosticSeverity::Info,
176            DiagnosticSeverity::Hint,
177            DiagnosticSeverity::Other,
178        ];
179        let collected: Vec<DiagnosticSeverity> = DiagnosticSeverity::iter().collect();
180        pretty_assertions::assert_eq!(collected.as_slice(), expected.as_slice());
181        pretty_assertions::assert_eq!(collected.len(), DiagnosticSeverity::VARIANT_COUNT);
182    }
183
184    #[rstest]
185    #[case(1_i64, DiagnosticSeverity::Error)]
186    #[case(2_i64, DiagnosticSeverity::Warn)]
187    #[case(3_i64, DiagnosticSeverity::Info)]
188    #[case(4_i64, DiagnosticSeverity::Hint)]
189    #[case(-1_i64, DiagnosticSeverity::Other)]
190    #[case(0_i64, DiagnosticSeverity::Other)]
191    #[case(5_i64, DiagnosticSeverity::Other)]
192    fn diagnostic_severity_deserializes_numeric_values_as_expected(
193        #[case] input: i64,
194        #[case] expected: DiagnosticSeverity,
195    ) {
196        let json = input.to_string();
197        assert2::assert!(let Ok(sev) = serde_json::from_str::<DiagnosticSeverity>(&json));
198        assert_eq!(sev, expected);
199    }
200
201    #[test]
202    fn diagnostic_severity_deserializes_invalid_json_errors() {
203        assert2::assert!(let Err(err) = serde_json::from_str::<DiagnosticSeverity>("error"));
204        let msg = err.to_string();
205        assert!(msg.contains("expected value"), "unexpected error message: {msg}");
206    }
207}