#![warn(missing_docs)]
use serde::{Deserialize, Serialize};
pub mod render;
pub const SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Error,
Warning,
Info,
Hint,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Location {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub file: Option<String>,
pub line: u32,
pub col: u32,
pub span: [u32; 2],
}
impl Location {
pub fn new(file: Option<String>, line: u32, col: u32, span_start: u32, span_end: u32) -> Self {
Self {
file,
line,
col,
span: [span_start, span_end],
}
}
pub fn synthetic() -> Self {
Self::new(None, 0, 0, 0, 0)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TypeWitness {
#[serde(rename = "type")]
pub r#type: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub witness: Option<serde_json::Value>,
}
impl TypeWitness {
pub fn new(type_name: impl Into<String>, witness: Option<serde_json::Value>) -> Self {
Self {
r#type: type_name.into(),
witness,
}
}
pub fn type_only(type_name: impl Into<String>) -> Self {
Self::new(type_name, None)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SuggestedFix {
pub label: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub diff: Option<String>,
pub confidence: f32,
}
impl SuggestedFix {
pub fn new(label: impl Into<String>, confidence: f32) -> Self {
Self {
label: label.into(),
diff: None,
confidence,
}
}
pub fn with_diff(mut self, diff: impl Into<String>) -> Self {
self.diff = Some(diff.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContextWindow {
pub tokens: u32,
pub spans: Vec<ContextSpan>,
}
impl ContextWindow {
pub fn empty() -> Self {
Self {
tokens: 0,
spans: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContextSpan {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub file: Option<String>,
pub lines: [u32; 2],
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Diagnostic {
pub diagnostic_id: String,
pub severity: Severity,
pub location: Location,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub expected: Option<TypeWitness>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub found: Option<TypeWitness>,
pub message: String,
#[serde(default)]
pub fixes: Vec<SuggestedFix>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub context_window: Option<ContextWindow>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub rule: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub notes: Vec<DiagnosticNote>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DiagnosticNote {
pub message: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub location: Option<Location>,
}
impl DiagnosticNote {
pub fn new(message: impl Into<String>, location: Option<Location>) -> Self {
Self {
message: message.into(),
location,
}
}
}
#[derive(Debug)]
pub struct DiagnosticBuilder {
diagnostic_id: String,
severity: Severity,
location: Location,
expected: Option<TypeWitness>,
found: Option<TypeWitness>,
message: String,
fixes: Vec<SuggestedFix>,
context_window: Option<ContextWindow>,
rule: Option<String>,
notes: Vec<DiagnosticNote>,
}
impl DiagnosticBuilder {
pub fn new(
diagnostic_id: impl Into<String>,
severity: Severity,
location: Location,
message: impl Into<String>,
) -> Self {
Self {
diagnostic_id: diagnostic_id.into(),
severity,
location,
expected: None,
found: None,
message: message.into(),
fixes: Vec::new(),
context_window: None,
rule: None,
notes: Vec::new(),
}
}
pub fn expected(mut self, witness: TypeWitness) -> Self {
self.expected = Some(witness);
self
}
pub fn found(mut self, witness: TypeWitness) -> Self {
self.found = Some(witness);
self
}
pub fn with_fix(mut self, fix: SuggestedFix) -> Self {
self.fixes.push(fix);
self
}
pub fn context_window(mut self, window: ContextWindow) -> Self {
self.context_window = Some(window);
self
}
pub fn rule(mut self, rule: impl Into<String>) -> Self {
self.rule = Some(rule.into());
self
}
pub fn with_note(mut self, note: DiagnosticNote) -> Self {
self.notes.push(note);
self
}
pub fn build(self) -> Diagnostic {
Diagnostic {
diagnostic_id: self.diagnostic_id,
severity: self.severity,
location: self.location,
expected: self.expected,
found: self.found,
message: self.message,
fixes: self.fixes,
context_window: self.context_window,
rule: self.rule,
notes: self.notes,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn schema_version_is_one() {
assert_eq!(SCHEMA_VERSION, 1);
}
#[test]
fn diagnostic_round_trips_through_json() {
let diag = DiagnosticBuilder::new(
"B0013",
Severity::Error,
Location::new(Some("src/main.shape".into()), 12, 4, 102, 145),
"expected int, found string",
)
.expected(TypeWitness::new("int", Some(json!(42))))
.found(TypeWitness::new("string", Some(json!("hello"))))
.with_fix(
SuggestedFix::new("convert string to int", 0.85)
.with_diff("let x: int = parse_int(value)?"),
)
.rule("ADR-006-§1.1")
.build();
let s = serde_json::to_string(&diag).expect("serialize");
let back: Diagnostic = serde_json::from_str(&s).expect("deserialize");
assert_eq!(diag, back);
}
#[test]
fn omitted_optional_fields_round_trip() {
let diag = DiagnosticBuilder::new(
"E0100",
Severity::Error,
Location::synthetic(),
"type mismatch",
)
.build();
let s = serde_json::to_string(&diag).expect("serialize");
assert!(!s.contains("\"expected\""));
assert!(!s.contains("\"found\""));
assert!(s.contains("\"fixes\":[]"));
assert!(!s.contains("\"context_window\""));
assert!(!s.contains("\"rule\""));
assert!(!s.contains("\"notes\""));
let back: Diagnostic = serde_json::from_str(&s).expect("deserialize");
assert_eq!(diag, back);
}
#[test]
fn severity_serializes_lowercase() {
let s = serde_json::to_string(&Severity::Error).unwrap();
assert_eq!(s, "\"error\"");
let s = serde_json::to_string(&Severity::Warning).unwrap();
assert_eq!(s, "\"warning\"");
let s = serde_json::to_string(&Severity::Info).unwrap();
assert_eq!(s, "\"info\"");
let s = serde_json::to_string(&Severity::Hint).unwrap();
assert_eq!(s, "\"hint\"");
}
}