use agnix_core::{Diagnostic, DiagnosticLevel, Fix};
use serde_json::Value as JsonValue;
use tower_lsp::lsp_types::{
Diagnostic as LspDiagnostic, DiagnosticSeverity, NumberOrString, Position, Range,
};
fn serialize_fixes(fixes: &[Fix]) -> Option<JsonValue> {
if fixes.is_empty() {
return None;
}
serde_json::to_value(fixes).ok()
}
pub fn deserialize_fixes(data: Option<&JsonValue>) -> Vec<Fix> {
data.and_then(|v| serde_json::from_value(v.clone()).ok())
.unwrap_or_default()
}
pub fn to_lsp_diagnostic(diag: &Diagnostic) -> LspDiagnostic {
let severity = match diag.level {
DiagnosticLevel::Error => DiagnosticSeverity::ERROR,
DiagnosticLevel::Warning => DiagnosticSeverity::WARNING,
DiagnosticLevel::Info => DiagnosticSeverity::INFORMATION,
};
let line = diag.line.saturating_sub(1) as u32;
let column = diag.column.saturating_sub(1) as u32;
let message = if let Some(ref suggestion) = diag.suggestion {
format!("{}\n\nSuggestion: {}", diag.message, suggestion)
} else {
diag.message.clone()
};
let data = serialize_fixes(&diag.fixes);
LspDiagnostic {
range: Range {
start: Position {
line,
character: column,
},
end: Position {
line,
character: column,
},
},
severity: Some(severity),
code: Some(NumberOrString::String(diag.rule.clone())),
code_description: None,
source: Some("agnix".to_string()),
message,
related_information: None,
tags: None,
data,
}
}
pub fn to_lsp_diagnostics(diagnostics: Vec<Diagnostic>) -> Vec<LspDiagnostic> {
diagnostics.iter().map(to_lsp_diagnostic).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn make_diagnostic(
level: DiagnosticLevel,
message: &str,
line: usize,
column: usize,
rule: &str,
suggestion: Option<&str>,
) -> Diagnostic {
Diagnostic {
level,
message: message.to_string(),
file: PathBuf::from("test.md"),
line,
column,
rule: rule.to_string(),
suggestion: suggestion.map(String::from),
fixes: vec![],
assumption: None,
}
}
fn make_diagnostic_with_fixes(
level: DiagnosticLevel,
message: &str,
line: usize,
column: usize,
rule: &str,
fixes: Vec<Fix>,
) -> Diagnostic {
Diagnostic {
level,
message: message.to_string(),
file: PathBuf::from("test.md"),
line,
column,
rule: rule.to_string(),
suggestion: None,
fixes,
assumption: None,
}
}
fn make_fix(start: usize, end: usize, replacement: &str, description: &str, safe: bool) -> Fix {
Fix {
start_byte: start,
end_byte: end,
replacement: replacement.to_string(),
description: description.to_string(),
safe,
}
}
#[test]
fn test_error_severity_mapping() {
let diag = make_diagnostic(
DiagnosticLevel::Error,
"Error message",
1,
1,
"AS-001",
None,
);
let lsp_diag = to_lsp_diagnostic(&diag);
assert_eq!(lsp_diag.severity, Some(DiagnosticSeverity::ERROR));
}
#[test]
fn test_warning_severity_mapping() {
let diag = make_diagnostic(
DiagnosticLevel::Warning,
"Warning message",
1,
1,
"AS-002",
None,
);
let lsp_diag = to_lsp_diagnostic(&diag);
assert_eq!(lsp_diag.severity, Some(DiagnosticSeverity::WARNING));
}
#[test]
fn test_info_severity_mapping() {
let diag = make_diagnostic(DiagnosticLevel::Info, "Info message", 1, 1, "AS-003", None);
let lsp_diag = to_lsp_diagnostic(&diag);
assert_eq!(lsp_diag.severity, Some(DiagnosticSeverity::INFORMATION));
}
#[test]
fn test_line_column_conversion() {
let diag = make_diagnostic(DiagnosticLevel::Error, "Test", 10, 5, "AS-001", None);
let lsp_diag = to_lsp_diagnostic(&diag);
assert_eq!(lsp_diag.range.start.line, 9);
assert_eq!(lsp_diag.range.start.character, 4);
}
#[test]
fn test_line_zero_saturates() {
let diag = make_diagnostic(DiagnosticLevel::Error, "Test", 0, 0, "AS-001", None);
let lsp_diag = to_lsp_diagnostic(&diag);
assert_eq!(lsp_diag.range.start.line, 0);
assert_eq!(lsp_diag.range.start.character, 0);
}
#[test]
fn test_rule_code() {
let diag = make_diagnostic(DiagnosticLevel::Error, "Test", 1, 1, "CC-SK-001", None);
let lsp_diag = to_lsp_diagnostic(&diag);
assert_eq!(
lsp_diag.code,
Some(NumberOrString::String("CC-SK-001".to_string()))
);
}
#[test]
fn test_source_is_agnix() {
let diag = make_diagnostic(DiagnosticLevel::Error, "Test", 1, 1, "AS-001", None);
let lsp_diag = to_lsp_diagnostic(&diag);
assert_eq!(lsp_diag.source, Some("agnix".to_string()));
}
#[test]
fn test_message_without_suggestion() {
let diag = make_diagnostic(
DiagnosticLevel::Error,
"Error message",
1,
1,
"AS-001",
None,
);
let lsp_diag = to_lsp_diagnostic(&diag);
assert_eq!(lsp_diag.message, "Error message");
}
#[test]
fn test_message_with_suggestion() {
let diag = make_diagnostic(
DiagnosticLevel::Error,
"Error message",
1,
1,
"AS-001",
Some("Try doing this instead"),
);
let lsp_diag = to_lsp_diagnostic(&diag);
assert!(lsp_diag.message.contains("Error message"));
assert!(lsp_diag
.message
.contains("Suggestion: Try doing this instead"));
}
#[test]
fn test_to_lsp_diagnostics_empty() {
let diagnostics: Vec<Diagnostic> = vec![];
let lsp_diagnostics = to_lsp_diagnostics(diagnostics);
assert!(lsp_diagnostics.is_empty());
}
#[test]
fn test_to_lsp_diagnostics_multiple() {
let diagnostics = vec![
make_diagnostic(DiagnosticLevel::Error, "Error 1", 1, 1, "AS-001", None),
make_diagnostic(DiagnosticLevel::Warning, "Warning 1", 2, 1, "AS-002", None),
make_diagnostic(DiagnosticLevel::Info, "Info 1", 3, 1, "AS-003", None),
];
let lsp_diagnostics = to_lsp_diagnostics(diagnostics);
assert_eq!(lsp_diagnostics.len(), 3);
assert_eq!(lsp_diagnostics[0].severity, Some(DiagnosticSeverity::ERROR));
assert_eq!(
lsp_diagnostics[1].severity,
Some(DiagnosticSeverity::WARNING)
);
assert_eq!(
lsp_diagnostics[2].severity,
Some(DiagnosticSeverity::INFORMATION)
);
}
#[test]
fn test_diagnostic_with_fixes_has_data() {
let fixes = vec![make_fix(0, 5, "hello", "Replace text", true)];
let diag =
make_diagnostic_with_fixes(DiagnosticLevel::Error, "Error", 1, 1, "AS-001", fixes);
let lsp_diag = to_lsp_diagnostic(&diag);
assert!(lsp_diag.data.is_some());
}
#[test]
fn test_diagnostic_without_fixes_no_data() {
let diag = make_diagnostic(DiagnosticLevel::Error, "Error", 1, 1, "AS-001", None);
let lsp_diag = to_lsp_diagnostic(&diag);
assert!(lsp_diag.data.is_none());
}
#[test]
fn test_serialize_deserialize_fixes_roundtrip() {
let fixes = vec![
make_fix(0, 5, "hello", "Replace text", true),
make_fix(10, 15, "world", "Another fix", false),
];
let serialized = serialize_fixes(&fixes);
assert!(serialized.is_some());
let deserialized = deserialize_fixes(serialized.as_ref());
assert_eq!(deserialized.len(), 2);
assert_eq!(deserialized[0].start_byte, 0);
assert_eq!(deserialized[0].end_byte, 5);
assert_eq!(deserialized[0].replacement, "hello");
assert_eq!(deserialized[0].description, "Replace text");
assert!(deserialized[0].safe);
assert_eq!(deserialized[1].start_byte, 10);
assert!(!deserialized[1].safe);
}
#[test]
fn test_deserialize_fixes_none() {
let fixes = deserialize_fixes(None);
assert!(fixes.is_empty());
}
#[test]
fn test_deserialize_fixes_invalid_json() {
let invalid = JsonValue::String("not an array".to_string());
let fixes = deserialize_fixes(Some(&invalid));
assert!(fixes.is_empty());
}
#[test]
fn test_serialize_empty_fixes() {
let fixes: Vec<Fix> = vec![];
let serialized = serialize_fixes(&fixes);
assert!(serialized.is_none());
}
}