use crate::position::Range;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Severity {
Error,
Warning,
Information,
Hint,
}
impl serde::Serialize for Severity {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let value: i32 = match self {
Self::Error => 1,
Self::Warning => 2,
Self::Information => 3,
Self::Hint => 4,
};
serializer.serialize_i32(value)
}
}
impl<'de> serde::Deserialize<'de> for Severity {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Ok(match i32::deserialize(deserializer)? {
..=1 => Self::Error,
2 => Self::Warning,
3 => Self::Information,
4.. => Self::Hint,
})
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodeDescription {
pub href: url::Url,
}
impl CodeDescription {
#[must_use]
pub const fn new(href: url::Url) -> Self {
Self { href }
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RelatedInformation {
pub uri: url::Url,
pub range: Range,
pub message: String,
}
impl RelatedInformation {
#[must_use]
pub fn new(uri: url::Url, range: Range, message: impl Into<String>) -> Self {
Self {
uri,
range,
message: message.into(),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Diagnostic {
pub range: Range,
pub severity: Option<Severity>,
pub code: Option<String>,
pub code_description: Option<CodeDescription>,
pub message: String,
pub related_information: Option<Vec<RelatedInformation>>,
}
impl Diagnostic {
#[must_use]
pub fn new(range: Range, message: impl Into<String>) -> Self {
Self {
range,
severity: None,
code: None,
code_description: None,
message: message.into(),
related_information: None,
}
}
#[must_use]
pub const fn with_severity(mut self, severity: Severity) -> Self {
self.severity = Some(severity);
self
}
#[must_use]
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
#[must_use]
pub fn with_code_description(mut self, code_description: CodeDescription) -> Self {
self.code_description = Some(code_description);
self
}
#[must_use]
pub fn with_related_information(
mut self,
related_information: Vec<RelatedInformation>,
) -> Self {
self.related_information = Some(related_information);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diagnostic_builder_sets_all_fields() {
use crate::position::Position;
let href = url::Url::parse("https://osv.dev/GHSA-xxxx").unwrap();
let related_uri = url::Url::parse("file:///Cargo.toml").unwrap();
let diagnostic = Diagnostic::new(
Range::new(Position::new(0, 0), Position::new(0, 4)),
"vulnerable",
)
.with_severity(Severity::Error)
.with_code("GHSA-xxxx")
.with_code_description(CodeDescription::new(href.clone()))
.with_related_information(vec![RelatedInformation::new(
related_uri.clone(),
Range::new(Position::new(1, 0), Position::new(1, 4)),
"also here",
)]);
assert_eq!(diagnostic.severity, Some(Severity::Error));
assert_eq!(diagnostic.code.as_deref(), Some("GHSA-xxxx"));
assert_eq!(diagnostic.code_description.as_ref().unwrap().href, href);
let related = diagnostic.related_information.as_ref().unwrap();
assert_eq!(related.len(), 1);
assert_eq!(related[0].uri, related_uri);
assert_eq!(related[0].message, "also here");
}
#[test]
fn test_diagnostic_new_leaves_optional_fields_unset() {
use crate::position::Position;
let diagnostic = Diagnostic::new(
Range::new(Position::new(0, 0), Position::new(0, 1)),
"message",
);
assert_eq!(diagnostic.severity, None);
assert_eq!(diagnostic.code, None);
assert_eq!(diagnostic.code_description, None);
assert_eq!(diagnostic.related_information, None);
}
#[test]
fn test_severity_serialize_matches_lsp_wire_encoding() {
assert_eq!(serde_json::to_string(&Severity::Error).unwrap(), "1");
assert_eq!(serde_json::to_string(&Severity::Warning).unwrap(), "2");
assert_eq!(serde_json::to_string(&Severity::Information).unwrap(), "3");
assert_eq!(serde_json::to_string(&Severity::Hint).unwrap(), "4");
}
#[test]
fn test_severity_deserialize_accepts_every_valid_value() {
assert_eq!(
serde_json::from_str::<Severity>("1").unwrap(),
Severity::Error
);
assert_eq!(
serde_json::from_str::<Severity>("2").unwrap(),
Severity::Warning
);
assert_eq!(
serde_json::from_str::<Severity>("3").unwrap(),
Severity::Information
);
assert_eq!(
serde_json::from_str::<Severity>("4").unwrap(),
Severity::Hint
);
}
#[test]
fn test_severity_roundtrips_through_serialize_deserialize() {
for severity in [
Severity::Error,
Severity::Warning,
Severity::Information,
Severity::Hint,
] {
let json = serde_json::to_string(&severity).unwrap();
assert_eq!(serde_json::from_str::<Severity>(&json).unwrap(), severity);
}
}
#[test]
fn test_severity_deserialize_clamps_out_of_range_values_instead_of_failing() {
assert_eq!(
serde_json::from_str::<Severity>("0").unwrap(),
Severity::Error
);
assert_eq!(
serde_json::from_str::<Severity>("-5").unwrap(),
Severity::Error
);
assert_eq!(
serde_json::from_str::<Severity>("5").unwrap(),
Severity::Hint
);
assert_eq!(
serde_json::from_str::<Severity>("999").unwrap(),
Severity::Hint
);
}
}