1use serde::Serialize;
8
9pub const SCHEMA_VERSION: u32 = 1;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
12#[serde(rename_all = "lowercase")]
13pub enum Severity {
14 Error,
15 Warning,
16 Fail,
17 Hint,
18}
19
20#[derive(Clone, Debug, Serialize)]
21pub struct Span {
22 pub file: String,
23 pub line: usize,
24 pub col: usize,
25}
26
27#[derive(Clone, Debug, Serialize)]
28pub struct SourceLine {
29 pub line_num: usize,
30 pub text: String,
31}
32
33#[derive(Clone, Debug, Serialize)]
34pub struct Underline {
35 pub col: usize,
36 pub len: usize,
37 pub label: String,
38}
39
40#[derive(Clone, Debug, Serialize)]
41pub struct AnnotatedRegion {
42 pub source_lines: Vec<SourceLine>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub underline: Option<Underline>,
45}
46
47impl AnnotatedRegion {
48 pub fn single(source_lines: Vec<SourceLine>, underline: Option<Underline>) -> Vec<Self> {
49 vec![Self {
50 source_lines,
51 underline,
52 }]
53 }
54}
55
56#[derive(Clone, Debug, Serialize)]
57pub struct RelatedSpan {
58 pub span: Span,
59 pub label: String,
60}
61
62#[derive(Clone, Debug, Default, Serialize)]
63pub struct Repair {
64 #[serde(skip_serializing_if = "Option::is_none")]
65 pub primary: Option<String>,
66 #[serde(skip_serializing_if = "Vec::is_empty")]
67 pub alternatives: Vec<String>,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub example: Option<String>,
70}
71
72impl Repair {
73 pub fn primary(text: impl Into<String>) -> Self {
74 Self {
75 primary: Some(text.into()),
76 alternatives: Vec::new(),
77 example: None,
78 }
79 }
80
81 pub fn is_empty(&self) -> bool {
82 self.primary.is_none() && self.alternatives.is_empty() && self.example.is_none()
83 }
84}
85
86#[derive(Clone, Debug, Serialize)]
87pub struct Diagnostic {
88 pub severity: Severity,
89 pub slug: &'static str,
90 pub summary: String,
91 pub span: Span,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub fn_name: Option<String>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub intent: Option<String>,
96 #[serde(skip_serializing_if = "Vec::is_empty")]
97 pub fields: Vec<(&'static str, String)>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub conflict: Option<String>,
100 #[serde(skip_serializing_if = "Repair::is_empty")]
101 pub repair: Repair,
102 #[serde(skip_serializing_if = "Vec::is_empty")]
103 pub regions: Vec<AnnotatedRegion>,
104 #[serde(skip_serializing_if = "Vec::is_empty")]
105 pub related: Vec<RelatedSpan>,
106}
107
108impl Diagnostic {
109 pub fn is_warning(&self) -> bool {
110 matches!(self.severity, Severity::Warning)
111 }
112
113 pub fn is_error(&self) -> bool {
114 matches!(self.severity, Severity::Error | Severity::Fail)
115 }
116}
117
118#[derive(Clone, Debug, Serialize)]
119pub struct AnalysisReport {
120 pub schema_version: u32,
121 pub kind: &'static str,
122 pub file_label: String,
123 #[serde(skip_serializing_if = "Vec::is_empty")]
124 pub diagnostics: Vec<Diagnostic>,
125 #[serde(skip_serializing_if = "Option::is_none")]
128 pub why_summary: Option<crate::diagnostics::why::WhySummary>,
129 #[serde(skip_serializing_if = "Option::is_none")]
132 pub context_summary: Option<crate::diagnostics::context::ContextSummary>,
133 #[serde(skip_serializing_if = "Option::is_none")]
137 pub verify_summary: Option<VerifySummary>,
138}
139
140#[derive(Clone, Debug, Serialize)]
144pub struct VerifySummary {
145 pub blocks: Vec<VerifyBlockResult>,
146}
147
148#[derive(Clone, Debug, Serialize)]
157pub struct FormatViolation {
158 pub line: usize,
159 pub col: usize,
160 pub rule: &'static str,
161 pub message: String,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub before: Option<String>,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub after: Option<String>,
166}
167
168#[derive(Clone, Debug, Serialize)]
169pub struct VerifyBlockResult {
170 pub name: String,
171 pub passed: usize,
172 pub failed: usize,
173 pub skipped: usize,
174 pub total: usize,
175}
176
177impl AnalysisReport {
178 pub fn new(file_label: impl Into<String>) -> Self {
179 Self {
180 schema_version: SCHEMA_VERSION,
181 kind: "analysis",
182 file_label: file_label.into(),
183 diagnostics: Vec::new(),
184 why_summary: None,
185 context_summary: None,
186 verify_summary: None,
187 }
188 }
189
190 pub fn with_diagnostics(file_label: impl Into<String>, diagnostics: Vec<Diagnostic>) -> Self {
191 Self {
192 schema_version: SCHEMA_VERSION,
193 kind: "analysis",
194 file_label: file_label.into(),
195 diagnostics,
196 why_summary: None,
197 context_summary: None,
198 verify_summary: None,
199 }
200 }
201
202 pub fn to_json(&self) -> String {
203 serde_json::to_string(self).unwrap_or_else(|_| "{}".to_string())
204 }
205}
206
207pub fn json_escape(s: &str) -> String {
211 let mut out = String::with_capacity(s.len() + 2);
212 out.push('"');
213 for ch in s.chars() {
214 match ch {
215 '"' => out.push_str("\\\""),
216 '\\' => out.push_str("\\\\"),
217 '\n' => out.push_str("\\n"),
218 '\r' => out.push_str("\\r"),
219 '\t' => out.push_str("\\t"),
220 c => out.push(c),
221 }
222 }
223 out.push('"');
224 out
225}