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 std::fmt::Formatter;
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)]
34pub enum DiagnosticSeverity {
35    Error,
36    Warn,
37    Info,
38    Hint,
39    Other,
40}
41
42impl DiagnosticSeverity {
43    /// Number of declared severity variants.
44    pub const VARIANT_COUNT: usize = <Self as strum::EnumCount>::COUNT;
45
46    /// Returns the canonical LSP severity number (1-4, or 0 for Other).
47    pub const fn to_number(self) -> u8 {
48        match self {
49            Self::Error => 1,
50            Self::Warn => 2,
51            Self::Info => 3,
52            Self::Hint => 4,
53            Self::Other => 0,
54        }
55    }
56
57    /// Returns the canonical single-character symbol for this severity.
58    pub const fn symbol(self) -> &'static str {
59        match self {
60            Self::Error => "E",
61            Self::Warn => "W",
62            Self::Info => "I",
63            Self::Hint => "H",
64            Self::Other => "",
65        }
66    }
67}
68
69/// Deserializes numeric, string, or text alias severity representations.
70impl<'de> serde::Deserialize<'de> for DiagnosticSeverity {
71    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
72    where
73        D: Deserializer<'de>,
74    {
75        struct SevVisitor;
76
77        impl Visitor<'_> for SevVisitor {
78            type Value = DiagnosticSeverity;
79
80            fn expecting(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
81                write!(f, "a severity: 1-4 or (error|warn|info|hint) or short alias")
82            }
83
84            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
85            where
86                E: serde::de::Error,
87            {
88                Ok(match v {
89                    1 => DiagnosticSeverity::Error,
90                    2 => DiagnosticSeverity::Warn,
91                    3 => DiagnosticSeverity::Info,
92                    4 => DiagnosticSeverity::Hint,
93                    _ => DiagnosticSeverity::Other,
94                })
95            }
96
97            fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
98            where
99                E: serde::de::Error,
100            {
101                if v < 0 {
102                    return Ok(DiagnosticSeverity::Other);
103                }
104                self.visit_u64(v.cast_unsigned())
105            }
106
107            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
108            where
109                E: serde::de::Error,
110            {
111                let norm = s.trim().to_ascii_lowercase();
112                if let Ok(n) = norm.parse::<u64>() {
113                    return self.visit_u64(n);
114                }
115                Ok(match norm.as_str() {
116                    "error" | "err" | "e" => DiagnosticSeverity::Error,
117                    "warn" | "warning" | "w" => DiagnosticSeverity::Warn,
118                    "info" | "information" | "i" => DiagnosticSeverity::Info,
119                    "hint" | "h" => DiagnosticSeverity::Hint,
120                    _ => DiagnosticSeverity::Other,
121                })
122            }
123        }
124
125        deserializer.deserialize_any(SevVisitor)
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use rstest::rstest;
132    use strum::IntoEnumIterator;
133
134    use super::*;
135
136    #[rstest]
137    #[case("1", DiagnosticSeverity::Error)]
138    #[case("2", DiagnosticSeverity::Warn)]
139    #[case("3", DiagnosticSeverity::Info)]
140    #[case("4", DiagnosticSeverity::Hint)]
141    #[case("\"1\"", DiagnosticSeverity::Error)]
142    #[case("\"2\"", DiagnosticSeverity::Warn)]
143    #[case("\"3\"", DiagnosticSeverity::Info)]
144    #[case("\"4\"", DiagnosticSeverity::Hint)]
145    #[case("\"error\"", DiagnosticSeverity::Error)]
146    #[case("\"err\"", DiagnosticSeverity::Error)]
147    #[case("\"e\"", DiagnosticSeverity::Error)]
148    #[case("\"warn\"", DiagnosticSeverity::Warn)]
149    #[case("\"warning\"", DiagnosticSeverity::Warn)]
150    #[case("\"w\"", DiagnosticSeverity::Warn)]
151    #[case("\"info\"", DiagnosticSeverity::Info)]
152    #[case("\"information\"", DiagnosticSeverity::Info)]
153    #[case("\"i\"", DiagnosticSeverity::Info)]
154    #[case("\"hint\"", DiagnosticSeverity::Hint)]
155    #[case("\"h\"", DiagnosticSeverity::Hint)]
156    #[case("\" Error \"", DiagnosticSeverity::Error)]
157    #[case("\"WARNING\"", DiagnosticSeverity::Warn)]
158    #[case("\" Info \"", DiagnosticSeverity::Info)]
159    #[case("\"H\"", DiagnosticSeverity::Hint)]
160    #[case("-1", DiagnosticSeverity::Other)]
161    #[case("0", DiagnosticSeverity::Other)]
162    #[case("5", DiagnosticSeverity::Other)]
163    #[case("\"unknown\"", DiagnosticSeverity::Other)]
164    fn test_diagnostic_severity_deserializes_strings_as_expected(
165        #[case] input: &str,
166        #[case] expected: DiagnosticSeverity,
167    ) {
168        assert2::assert!(let Ok(sev) = serde_json::from_str::<DiagnosticSeverity>(input));
169        assert_eq!(sev, expected);
170    }
171
172    #[test]
173    fn test_diagnostic_severity_when_iterated_via_enumiter_yields_declared_order_and_matches_variant_count() {
174        let expected = [
175            DiagnosticSeverity::Error,
176            DiagnosticSeverity::Warn,
177            DiagnosticSeverity::Info,
178            DiagnosticSeverity::Hint,
179            DiagnosticSeverity::Other,
180        ];
181        let collected: Vec<DiagnosticSeverity> = DiagnosticSeverity::iter().collect();
182        pretty_assertions::assert_eq!(collected.as_slice(), expected.as_slice());
183        pretty_assertions::assert_eq!(collected.len(), DiagnosticSeverity::VARIANT_COUNT);
184    }
185
186    #[rstest]
187    #[case(1_i64, DiagnosticSeverity::Error)]
188    #[case(2_i64, DiagnosticSeverity::Warn)]
189    #[case(3_i64, DiagnosticSeverity::Info)]
190    #[case(4_i64, DiagnosticSeverity::Hint)]
191    #[case(-1_i64, DiagnosticSeverity::Other)]
192    #[case(0_i64, DiagnosticSeverity::Other)]
193    #[case(5_i64, DiagnosticSeverity::Other)]
194    fn test_diagnostic_severity_deserializes_numeric_values_as_expected(
195        #[case] input: i64,
196        #[case] expected: DiagnosticSeverity,
197    ) {
198        let json = input.to_string();
199        assert2::assert!(let Ok(sev) = serde_json::from_str::<DiagnosticSeverity>(&json));
200        assert_eq!(sev, expected);
201    }
202
203    #[test]
204    fn test_diagnostic_severity_deserializes_invalid_json_errors() {
205        assert2::assert!(let Err(err) = serde_json::from_str::<DiagnosticSeverity>("error"));
206        let msg = err.to_string();
207        assert!(msg.contains("expected value"), "unexpected error message: {msg}");
208    }
209}