1use std::borrow::Cow;
2use std::fmt::Write as _;
3
4use serde::{Deserialize, Serialize};
5
6use super::serde_cow::{de_cow_static, de_opt_cow_static};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11#[non_exhaustive]
12pub enum Severity {
13 Error,
15 Warning,
17 Note,
19}
20
21impl Severity {
22 #[must_use]
24 pub const fn label(self) -> &'static str {
25 match self {
26 Severity::Error => "error",
27 Severity::Warning => "warning",
28 Severity::Note => "note",
29 }
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(transparent)]
36pub struct DiagnosticCode(#[serde(deserialize_with = "de_cow_static")] pub Cow<'static, str>);
37
38impl DiagnosticCode {
39 #[must_use]
41 pub const fn new(code: &'static str) -> Self {
42 Self(Cow::Borrowed(code))
43 }
44
45 #[must_use]
47 pub fn as_str(&self) -> &str {
48 &self.0
49 }
50}
51
52impl std::fmt::Display for DiagnosticCode {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.write_str(&self.0)
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct OpLocation {
61 #[serde(deserialize_with = "de_cow_static")]
63 pub op_id: Cow<'static, str>,
64 #[serde(skip_serializing_if = "Option::is_none", default)]
66 pub operand_idx: Option<u32>,
67 #[serde(
69 skip_serializing_if = "Option::is_none",
70 default,
71 deserialize_with = "de_opt_cow_static"
72 )]
73 pub attr_name: Option<Cow<'static, str>>,
74}
75
76impl OpLocation {
77 #[must_use]
79 pub fn op(op_id: impl Into<Cow<'static, str>>) -> Self {
80 Self {
81 op_id: op_id.into(),
82 operand_idx: None,
83 attr_name: None,
84 }
85 }
86
87 #[must_use]
89 pub fn with_operand(mut self, idx: u32) -> Self {
90 self.operand_idx = Some(idx);
91 self
92 }
93
94 #[must_use]
96 pub fn with_attr(mut self, name: impl Into<Cow<'static, str>>) -> Self {
97 self.attr_name = Some(name.into());
98 self
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub struct Diagnostic {
105 pub severity: Severity,
107 pub code: DiagnosticCode,
109 #[serde(deserialize_with = "de_cow_static")]
111 pub message: Cow<'static, str>,
112 #[serde(skip_serializing_if = "Option::is_none", default)]
114 pub location: Option<OpLocation>,
115 #[serde(
117 skip_serializing_if = "Option::is_none",
118 default,
119 deserialize_with = "de_opt_cow_static"
120 )]
121 pub suggested_fix: Option<Cow<'static, str>>,
122 #[serde(
124 skip_serializing_if = "Option::is_none",
125 default,
126 deserialize_with = "de_opt_cow_static"
127 )]
128 pub doc_url: Option<Cow<'static, str>>,
129}
130
131impl Diagnostic {
132 #[must_use]
134 pub fn error(code: &'static str, message: impl Into<Cow<'static, str>>) -> Self {
135 Self {
136 severity: Severity::Error,
137 code: DiagnosticCode::new(code),
138 message: message.into(),
139 location: None,
140 suggested_fix: None,
141 doc_url: None,
142 }
143 }
144
145 #[must_use]
147 pub fn warning(code: &'static str, message: impl Into<Cow<'static, str>>) -> Self {
148 Self {
149 severity: Severity::Warning,
150 code: DiagnosticCode::new(code),
151 message: message.into(),
152 location: None,
153 suggested_fix: None,
154 doc_url: None,
155 }
156 }
157
158 #[must_use]
160 pub fn note(code: &'static str, message: impl Into<Cow<'static, str>>) -> Self {
161 Self {
162 severity: Severity::Note,
163 code: DiagnosticCode::new(code),
164 message: message.into(),
165 location: None,
166 suggested_fix: None,
167 doc_url: None,
168 }
169 }
170
171 #[must_use]
173 pub fn with_location(mut self, loc: OpLocation) -> Self {
174 self.location = Some(loc);
175 self
176 }
177
178 #[must_use]
180 pub fn with_fix(mut self, fix: impl Into<Cow<'static, str>>) -> Self {
181 self.suggested_fix = Some(fix.into());
182 self
183 }
184
185 #[must_use]
187 pub fn with_doc_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
188 self.doc_url = Some(url.into());
189 self
190 }
191
192 #[must_use]
194 pub fn render_human(&self) -> String {
195 let mut out = String::with_capacity(256);
196 let _ = write!(
197 out,
198 "{}[{}]: {}",
199 self.severity.label(),
200 self.code,
201 self.message
202 );
203 if let Some(loc) = &self.location {
204 out.push_str("\n --> op `");
205 out.push_str(&loc.op_id);
206 out.push('`');
207 if let Some(idx) = loc.operand_idx {
208 let _ = write!(out, " operand[{idx}]");
209 }
210 if let Some(attr) = &loc.attr_name {
211 out.push_str(" attr `");
212 out.push_str(attr);
213 out.push('`');
214 }
215 }
216 if let Some(fix) = &self.suggested_fix {
217 out.push_str("\n = help: ");
218 out.push_str(fix);
219 }
220 if let Some(url) = &self.doc_url {
221 out.push_str("\n = note: ");
222 out.push_str(url);
223 }
224 out
225 }
226
227 #[must_use]
229 pub fn to_json(&self) -> String {
230 match serde_json::to_string(self) {
231 Ok(json) => json,
232 Err(e) => format!(
233 r#"{{"error":"Diagnostic::to_json serialization failed","code":"{code}","message":"{message}","serde_error":"{serde_error}","fix":"Fix: inspect Diagnostic fields for non-serializable types; every field must implement Serialize."}}"#,
234 code = self.code.as_str().replace('"', "\\\""),
235 message = self.message.replace('"', "\\\""),
236 serde_error = e.to_string().replace('"', "\\\""),
237 ),
238 }
239 }
240}
241
242impl std::fmt::Display for Diagnostic {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 f.write_str(&self.render_human())
245 }
246}