1use nvim_oxi::Dictionary;
4use serde::Deserialize;
5
6use crate::diagnostics::DiagnosticSeverity;
7
8pub fn dict() -> Dictionary {
10 dict! {
11 "sqruff": dict! {
12 "parser": fn_from!(parser)
13 },
14 }
15}
16
17#[expect(
19 clippy::needless_pass_by_value,
20 reason = "nvim function binding requires owned Lua-converted arguments"
21)]
22fn parser(maybe_output: Option<nvim_oxi::String>) -> Vec<Dictionary> {
23 let Some(output) = &maybe_output else {
24 ytil_noxi::notify::warn(format!("sqruff output missing output={maybe_output:?}"));
25 return vec![];
26 };
27 let output = output.to_string_lossy();
28
29 if output.trim().is_empty() {
30 ytil_noxi::notify::warn(format!("sqruff output is an empty string output={maybe_output:?}"));
31 return vec![];
32 }
33
34 let parsed_output = match serde_json::from_str::<SqruffOutput>(&output) {
35 Ok(parsed_output) => parsed_output,
36 Err(err) => {
37 ytil_noxi::notify::error(format!(
38 "error parsing sqruff output | output={output:?} error={err:#?}"
39 ));
40 return vec![];
41 }
42 };
43
44 parsed_output
45 .messages
46 .into_iter()
47 .map(diagnostic_dict_from_msg)
48 .collect()
49}
50
51#[derive(Debug, Deserialize)]
53#[cfg_attr(test, derive(Eq, PartialEq))]
54struct SqruffOutput {
55 #[serde(rename = "<string>", default)]
56 messages: Vec<SqruffMessage>,
57}
58
59#[derive(Debug, Deserialize)]
61#[cfg_attr(test, derive(Eq, PartialEq))]
62struct SqruffMessage {
63 code: Option<String>,
64 message: String,
65 range: Range,
66 severity: DiagnosticSeverity,
67 source: String,
68}
69
70#[derive(Debug, Deserialize)]
72#[cfg_attr(test, derive(Eq, PartialEq))]
73struct Range {
74 start: Position,
75 end: Position,
76}
77
78#[derive(Debug, Deserialize)]
80#[cfg_attr(test, derive(Eq, PartialEq))]
81struct Position {
82 character: u32,
83 line: u32,
84}
85
86fn diagnostic_dict_from_msg(msg: SqruffMessage) -> Dictionary {
88 dict! {
89 "lnum": msg.range.start.line.saturating_sub(1),
90 "end_lnum": msg.range.end.line.saturating_sub(1),
91 "col": msg.range.start.character.saturating_sub(1),
92 "end_col": msg.range.end.character.saturating_sub(1),
93 "message": msg.message,
94 "code": msg.code.map_or_else(nvim_oxi::Object::nil, nvim_oxi::Object::from),
95 "source": msg.source,
96 "severity": msg.severity.to_number(),
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use nvim_oxi::Object;
103
104 use super::*;
105
106 #[test]
107 fn test_diagnostic_dict_from_msg_returns_the_expected_dict_from_msg() {
108 let msg = SqruffMessage {
109 code: Some("R001".to_string()),
110 message: "Example message".to_string(),
111 range: Range {
112 start: Position { line: 3, character: 7 },
113 end: Position { line: 4, character: 10 },
114 },
115 severity: DiagnosticSeverity::Warn,
116 source: "sqruff".to_string(),
117 };
118
119 let res = diagnostic_dict_from_msg(msg);
120
121 let expected = dict! {
122 "lnum": 2,
123 "end_lnum": 3,
124 "col": 6,
125 "end_col": 9,
126 "message": "Example message".to_string(),
127 "code": Object::from(nvim_oxi::String::from("R001")),
128 "source": "sqruff".to_string(),
129 "severity": DiagnosticSeverity::Warn.to_number(),
130 };
131 pretty_assertions::assert_eq!(res, expected);
132 }
133
134 #[test]
135 fn test_sqruff_output_deserializes_empty_messages() {
136 let value = serde_json::json!({
137 "<string>": []
138 });
139
140 assert2::assert!(let Ok(parsed) = serde_json::from_value::<SqruffOutput>(value));
141 pretty_assertions::assert_eq!(parsed, SqruffOutput { messages: vec![] });
142 }
143
144 #[test]
145 fn test_sqruff_output_deserializes_single_message_with_code() {
146 let value = serde_json::json!({
147 "<string>": [
148 {
149 "code": "R001",
150 "message": "Msg",
151 "range": {"start": {"line": 2, "character": 5}, "end": {"line": 2, "character": 10}},
152 "severity": "2",
153 "source": "sqruff"
154 }
155 ]
156 });
157
158 assert2::assert!(let Ok(res) = serde_json::from_value::<SqruffOutput>(value));
159 pretty_assertions::assert_eq!(
160 res,
161 SqruffOutput {
162 messages: vec![SqruffMessage {
163 code: Some("R001".into()),
164 message: "Msg".into(),
165 range: Range {
166 start: Position { line: 2, character: 5 },
167 end: Position { line: 2, character: 10 },
168 },
169 severity: DiagnosticSeverity::Warn,
170 source: "sqruff".into(),
171 }],
172 }
173 );
174 }
175
176 #[test]
177 fn test_sqruff_output_deserializes_multiple_messages_mixed_code() {
178 let value = serde_json::json!({
179 "<string>": [
180 {
181 "code": "R001",
182 "message": "HasCode",
183 "range": {"start": {"line": 3, "character": 7}, "end": {"line": 3, "character": 12}},
184 "severity": "2",
185 "source": "sqruff"
186 },
187 {
188 "code": null,
189 "message": "NoCode",
190 "range": {"start": {"line": 1, "character": 1}, "end": {"line": 1, "character": 2}},
191 "severity": "1",
192 "source": "sqruff"
193 }
194 ]
195 });
196
197 assert2::assert!(let Ok(res) = serde_json::from_value::<SqruffOutput>(value));
198 pretty_assertions::assert_eq!(
199 res,
200 SqruffOutput {
201 messages: vec![
202 SqruffMessage {
203 code: Some("R001".into()),
204 message: "HasCode".into(),
205 range: Range {
206 start: Position { line: 3, character: 7 },
207 end: Position { line: 3, character: 12 },
208 },
209 severity: DiagnosticSeverity::Warn,
210 source: "sqruff".into(),
211 },
212 SqruffMessage {
213 code: None,
214 message: "NoCode".into(),
215 range: Range {
216 start: Position { line: 1, character: 1 },
217 end: Position { line: 1, character: 2 },
218 },
219 severity: DiagnosticSeverity::Error,
220 source: "sqruff".into(),
221 },
222 ],
223 }
224 );
225 }
226}