1use std::path::{Path, PathBuf};
2
3use serde_json::{json, Value};
4
5use crate::lsp::diagnostics::{DiagSeverity, DiagnosticStore};
6
7#[must_use]
12pub fn handle_check(
13 path: Option<&Path>,
14 store: &DiagnosticStore,
15 project_root: &Path,
16 errors_only: bool,
17) -> Value {
18 let mut entries: Vec<(PathBuf, DiagSeverity, u32, u32, Option<String>, String)> = vec![];
19
20 if let Some(filter) = path {
21 let abs = if filter.is_absolute() {
22 filter.to_path_buf()
23 } else {
24 project_root.join(filter)
25 };
26 for d in store.get(&abs) {
27 if errors_only && d.severity != DiagSeverity::Error {
28 continue;
29 }
30 entries.push((abs.clone(), d.severity, d.line, d.col, d.code, d.message));
31 }
32 } else {
33 for (file_path, diags) in store.get_all() {
34 for d in diags {
35 if errors_only && d.severity != DiagSeverity::Error {
36 continue;
37 }
38 entries.push((
39 file_path.clone(),
40 d.severity,
41 d.line,
42 d.col,
43 d.code,
44 d.message,
45 ));
46 }
47 }
48 }
49
50 entries.sort_by(|a, b| {
52 a.1.cmp(&b.1)
53 .then_with(|| a.0.cmp(&b.0))
54 .then_with(|| a.2.cmp(&b.2))
55 });
56
57 let mut errors: u64 = 0;
58 let mut warnings: u64 = 0;
59 let items: Vec<Value> = entries
60 .iter()
61 .map(|(file_path, sev, line, col, code, msg)| {
62 match sev {
63 DiagSeverity::Error => errors += 1,
64 DiagSeverity::Warning => warnings += 1,
65 _ => {}
66 }
67 let rel = file_path.strip_prefix(project_root).unwrap_or(file_path);
68 json!({
69 "severity": sev.label(),
70 "path": rel.to_string_lossy(),
71 "line": line + 1,
73 "col": col + 1,
74 "code": code,
75 "message": msg,
76 })
77 })
78 .collect();
79
80 let total = items.len() as u64;
81 json!({
82 "diagnostics": items,
83 "total": total,
84 "errors": errors,
85 "warnings": warnings,
86 })
87}
88
89#[cfg(test)]
92mod tests {
93 use std::path::PathBuf;
94
95 use super::*;
96 use crate::lsp::diagnostics::{DiagSeverity, DiagnosticEntry, DiagnosticStore};
97
98 fn root() -> PathBuf {
99 PathBuf::from("/project")
100 }
101
102 fn make_store() -> DiagnosticStore {
103 let store = DiagnosticStore::new();
104 store.update(
105 PathBuf::from("/project/src/lib.rs"),
106 vec![
107 DiagnosticEntry {
108 severity: DiagSeverity::Warning,
109 line: 2,
110 col: 4,
111 code: None,
112 message: "unused import".to_string(),
113 },
114 DiagnosticEntry {
115 severity: DiagSeverity::Error,
116 line: 41,
117 col: 9,
118 code: Some("E0308".to_string()),
119 message: "mismatched types".to_string(),
120 },
121 ],
122 );
123 store
124 }
125
126 #[test]
127 fn check_formats_errors_first() {
128 let store = make_store();
129 let result = handle_check(None, &store, &root(), false);
130 let diags = result["diagnostics"].as_array().unwrap();
131 assert_eq!(diags[0]["severity"], "error");
133 assert_eq!(diags[1]["severity"], "warn");
134 }
135
136 #[test]
137 fn check_empty_is_clean() {
138 let store = DiagnosticStore::new();
139 let result = handle_check(None, &store, &root(), false);
140 assert_eq!(result["total"], 0);
141 assert!(result["diagnostics"].as_array().unwrap().is_empty());
142 }
143
144 #[test]
145 fn check_filters_by_path() {
146 let store = make_store();
147 store.update(
148 PathBuf::from("/project/src/main.rs"),
149 vec![DiagnosticEntry {
150 severity: DiagSeverity::Error,
151 line: 5,
152 col: 0,
153 code: None,
154 message: "other error".to_string(),
155 }],
156 );
157 let result = handle_check(Some(Path::new("src/lib.rs")), &store, &root(), false);
158 let diags = result["diagnostics"].as_array().unwrap();
159 assert_eq!(diags.len(), 2, "should only return lib.rs diagnostics");
160 for d in diags {
161 assert_eq!(d["path"], "src/lib.rs");
162 }
163 }
164
165 #[test]
166 fn check_line_is_one_indexed() {
167 let store = make_store();
168 let result = handle_check(None, &store, &root(), false);
169 let diags = result["diagnostics"].as_array().unwrap();
170 let error = diags.iter().find(|d| d["severity"] == "error").unwrap();
171 assert_eq!(error["line"], 42);
173 }
174
175 #[test]
176 fn check_counts_errors_and_warnings() {
177 let store = make_store();
178 let result = handle_check(None, &store, &root(), false);
179 assert_eq!(result["errors"], 1);
180 assert_eq!(result["warnings"], 1);
181 assert_eq!(result["total"], 2);
182 }
183
184 #[test]
185 fn check_errors_only_suppresses_warnings() {
186 let store = make_store();
187 let result = handle_check(None, &store, &root(), true);
188 let diags = result["diagnostics"].as_array().unwrap();
189 assert_eq!(diags.len(), 1);
191 assert_eq!(diags[0]["severity"], "error");
192 assert_eq!(result["total"], 1);
193 }
194
195 #[test]
196 fn check_errors_only_clean_project_is_empty() {
197 let store = DiagnosticStore::new();
198 store.update(
199 PathBuf::from("/project/src/lib.rs"),
200 vec![DiagnosticEntry {
201 severity: DiagSeverity::Warning,
202 line: 0,
203 col: 0,
204 code: None,
205 message: "unused import".to_string(),
206 }],
207 );
208 let result = handle_check(None, &store, &root(), true);
210 assert_eq!(result["total"], 0);
211 assert!(result["diagnostics"].as_array().unwrap().is_empty());
212 }
213}