1use std::{
4 collections::HashMap,
5 fs, io,
6 path::{Path, PathBuf},
7 process::{Output, Stdio},
8};
9
10use lsp_types::{Diagnostic, DiagnosticRelatedInformation, Location, Position, Range, Uri};
11use regex::Regex;
12use serde::{Deserialize, Serialize};
13use tokio::process::Command;
14use tree_sitter::Point;
15
16mod config;
17mod error;
18mod lsp;
19mod runner;
20mod workspace;
21
22pub use lsp::server;
23
24mod go;
26mod javascript;
27mod php;
28mod rust;
29
30use config::{AdapterConfig, Config};
32
33pub(crate) const MAX_CHAR_LENGTH: u32 = 10000;
36
37pub(crate) async fn run_command(cmd: &mut Command) -> io::Result<Output> {
41 cmd.stdout(Stdio::piped())
42 .stderr(Stdio::piped())
43 .output()
44 .await
45}
46
47pub(crate) fn write_result_log(file_name: &str, output: &Output) -> io::Result<()> {
49 let stdout_str = String::from_utf8(output.stdout.clone()).unwrap_or_default();
50 let stderr_str = String::from_utf8(output.stderr.clone()).unwrap_or_default();
51 let content = format!("stdout:\n{stdout_str}\nstderr:\n{stderr_str}");
52 let cache = &config::CONFIG.cache_dir;
53 fs::create_dir_all(cache)?;
54 let log_path = cache.join(file_name);
55 fs::write(&log_path, content)?;
56 Ok(())
57}
58
59#[must_use]
61pub(crate) fn clean_ansi(input: &str) -> String {
62 let re = Regex::new(r"\x1B\[([0-9]{1,2}(;[0-9]{1,2})*)?[m|K]").unwrap();
63 re.replace_all(input, "").to_string()
64}
65
66#[must_use]
69pub(crate) fn path_to_uri_string(path: &Path) -> Option<String> {
70 if !path.is_absolute() {
71 return None;
72 }
73 let path_str = path.to_string_lossy();
74 #[cfg(windows)]
77 let uri_string = format!("file:///{}", path_str.replace('\\', "/"));
78 #[cfg(not(windows))]
79 let uri_string = format!("file://{path_str}");
80 Some(uri_string)
81}
82
83#[must_use]
86fn path_to_uri(path: &Path) -> Option<Uri> {
87 path_to_uri_string(path).and_then(|s| s.parse().ok())
88}
89
90#[must_use]
92pub(crate) const fn point_to_start_range(point: Point) -> Range {
93 Range {
94 start: Position {
95 line: point.row as u32,
96 character: point.column as u32,
97 },
98 end: Position {
99 line: point.row as u32,
100 character: MAX_CHAR_LENGTH,
101 },
102 }
103}
104
105#[must_use]
107pub(crate) const fn point_to_end_range(point: Point) -> Range {
108 Range {
109 start: Position {
110 line: point.row as u32,
111 character: 0,
112 },
113 end: Position {
114 line: point.row as u32,
115 character: point.column as u32,
116 },
117 }
118}
119
120#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
124pub(crate) struct TestItem {
125 pub(crate) id: String,
126 pub(crate) name: String,
127 pub(crate) path: PathBuf,
128 pub(crate) start_position: Range,
129 pub(crate) end_position: Range,
130}
131
132#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
134pub(crate) struct SourceSpan {
135 pub(crate) path: PathBuf,
136 pub(crate) range: Range,
137}
138
139#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
141pub(crate) struct BacktraceFrame {
142 pub(crate) function_name: String,
143 pub(crate) path: PathBuf,
144 pub(crate) range: Range,
145}
146
147#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)]
149pub(crate) struct TestFailure {
150 pub(crate) assertion: Option<SourceSpan>,
152 pub(crate) test_function: Option<SourceSpan>,
154 pub(crate) backtrace: Vec<BacktraceFrame>,
156 pub(crate) message: String,
158 pub(crate) test_name: String,
160 pub(crate) source: String,
162 pub(crate) code: String,
164}
165
166impl TestFailure {
167 #[must_use]
170 pub(crate) fn failure_id(&self) -> String {
171 let location = self
172 .assertion
173 .as_ref()
174 .or(self.test_function.as_ref())
175 .map(|s| format!("{}:{}", s.path.display(), s.range.start.line))
176 .unwrap_or_default();
177 format!("{}@{}", self.test_name, location)
178 }
179
180 #[must_use]
183 pub(crate) fn all_spans(&self) -> Vec<SourceSpan> {
184 let mut spans = Vec::new();
185 if let Some(assertion) = &self.assertion {
186 spans.push(assertion.clone());
187 }
188 spans.extend(self.backtrace.iter().map(|frame| SourceSpan {
189 path: frame.path.clone(),
190 range: frame.range,
191 }));
192 if let Some(test_fn) = &self.test_function {
193 spans.push(test_fn.clone());
194 }
195 spans
196 }
197}
198
199impl TestFailure {
200 #[must_use]
204 fn to_diagnostics(&self) -> Vec<(PathBuf, Diagnostic)> {
205 use lsp_types::{DiagnosticSeverity, NumberOrString};
206
207 let failure_id = self.failure_id();
208 let mut result = Vec::new();
209
210 let related_info = self.build_related_info();
212
213 if let Some(span) = &self.assertion {
215 let related = filter_related_info(&related_info, span);
216 result.push((
217 span.path.clone(),
218 Diagnostic {
219 range: span.range,
220 message: self.message.clone(),
221 severity: Some(DiagnosticSeverity::ERROR),
222 source: Some(self.source.clone()),
223 code: Some(NumberOrString::String(self.code.clone())),
224 related_information: if related.is_empty() {
225 None
226 } else {
227 Some(related)
228 },
229 data: Some(serde_json::json!({
230 "failureId": failure_id,
231 "spanType": "assertion",
232 "spanIndex": 0
233 })),
234 ..Diagnostic::default()
235 },
236 ));
237 }
238
239 for (idx, frame) in self.backtrace.iter().enumerate() {
241 let msg = format!("frame {}: {}", idx + 1, frame.function_name);
242 let frame_span = SourceSpan {
243 path: frame.path.clone(),
244 range: frame.range,
245 };
246 let related = filter_related_info(&related_info, &frame_span);
247 result.push((
248 frame.path.clone(),
249 Diagnostic {
250 range: frame.range,
251 message: msg,
252 severity: Some(DiagnosticSeverity::INFORMATION),
253 source: Some(self.source.clone()),
254 code: Some(NumberOrString::String("backtrace".to_string())),
255 related_information: if related.is_empty() {
256 None
257 } else {
258 Some(related)
259 },
260 data: Some(serde_json::json!({
261 "failureId": failure_id,
262 "spanType": "backtrace",
263 "spanIndex": idx + 1 })),
265 ..Diagnostic::default()
266 },
267 ));
268 }
269
270 if let Some(span) = &self.test_function {
272 let msg = format!("test `{}` failed", self.test_name);
273 let related = filter_related_info(&related_info, span);
274 let span_index = 1 + self.backtrace.len(); result.push((
276 span.path.clone(),
277 Diagnostic {
278 range: span.range,
279 message: msg,
280 severity: Some(DiagnosticSeverity::WARNING),
281 source: Some(self.source.clone()),
282 code: Some(NumberOrString::String("test-failed".to_string())),
283 related_information: if related.is_empty() {
284 None
285 } else {
286 Some(related)
287 },
288 data: Some(serde_json::json!({
289 "failureId": failure_id,
290 "spanType": "test_function",
291 "spanIndex": span_index
292 })),
293 ..Diagnostic::default()
294 },
295 ));
296 }
297
298 result
299 }
300
301 fn build_related_info(&self) -> Vec<DiagnosticRelatedInformation> {
302 let mut info = Vec::new();
303
304 if let Some(span) = &self.assertion
305 && let Some(uri) = path_to_uri(&span.path)
306 {
307 info.push(DiagnosticRelatedInformation {
308 location: Location {
309 uri,
310 range: span.range,
311 },
312 message: "assertion".to_string(),
313 });
314 }
315
316 for (idx, frame) in self.backtrace.iter().enumerate() {
317 if let Some(uri) = path_to_uri(&frame.path) {
318 info.push(DiagnosticRelatedInformation {
319 location: Location {
320 uri,
321 range: frame.range,
322 },
323 message: format!("frame {}: {}", idx + 1, frame.function_name),
324 });
325 }
326 }
327
328 if let Some(span) = &self.test_function
329 && let Some(uri) = path_to_uri(&span.path)
330 {
331 info.push(DiagnosticRelatedInformation {
332 location: Location {
333 uri,
334 range: span.range,
335 },
336 message: format!("test `{}`", self.test_name),
337 });
338 }
339
340 info
341 }
342}
343
344fn filter_related_info(
345 all: &[lsp_types::DiagnosticRelatedInformation],
346 current_span: &SourceSpan,
347) -> Vec<lsp_types::DiagnosticRelatedInformation> {
348 let current_uri = path_to_uri(¤t_span.path);
349 all.iter()
350 .filter(|info| {
351 current_uri.as_ref() != Some(&info.location.uri)
353 || info.location.range != current_span.range
354 })
355 .cloned()
356 .collect()
357}
358
359#[derive(Debug, Clone)]
361pub(crate) struct FileDiagnostics {
362 pub(crate) path: PathBuf,
363 pub(crate) diagnostics: Vec<Diagnostic>,
364}
365
366pub(crate) fn failures_to_file_diagnostics(
368 failures: impl IntoIterator<Item = TestFailure>,
369) -> impl Iterator<Item = FileDiagnostics> {
370 use std::collections::HashMap;
371
372 let mut by_file: HashMap<PathBuf, Vec<Diagnostic>> = HashMap::new();
373
374 for failure in failures {
375 for (path, diag) in failure.to_diagnostics() {
376 by_file.entry(path).or_default().push(diag);
377 }
378 }
379
380 by_file
381 .into_iter()
382 .map(|(path, diagnostics)| FileDiagnostics { path, diagnostics })
383}
384
385#[derive(Debug, Serialize, Clone, Deserialize, Default)]
387pub(crate) struct Workspaces {
388 pub(crate) map: HashMap<PathBuf, Vec<PathBuf>>,
389}
390
391#[derive(Debug, Serialize, Deserialize, Clone)]
393pub struct WorkspaceAnalysis {
394 pub adapter_config: AdapterConfig,
395 pub workspaces: Workspaces,
396}
397
398impl WorkspaceAnalysis {
399 #[must_use]
400 pub(crate) const fn new(adapter_config: AdapterConfig, workspaces: Workspaces) -> Self {
401 Self {
402 adapter_config,
403 workspaces,
404 }
405 }
406}