1use serde::{Deserialize, Serialize};
2#[cfg(feature = "compiler")]
4use std::fmt;
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum DiagnosticPhase {
9 Parse,
10 TypeCheck,
11 Load,
12 Lower,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum Severity {
18 Error,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(deny_unknown_fields)]
31pub struct ResourceLimitInfo {
32 pub resource: String,
33 pub limit: u64,
34 pub observed: u64,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(try_from = "RawSourceSpan")]
58pub struct SourceSpan {
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub source_name: Option<String>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub start_byte: Option<u64>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub end_byte: Option<u64>,
65}
66
67#[derive(Deserialize)]
70#[serde(deny_unknown_fields)]
71struct RawSourceSpan {
72 #[serde(default)]
73 source_name: Option<String>,
74 #[serde(default)]
75 start_byte: Option<u64>,
76 #[serde(default)]
77 end_byte: Option<u64>,
78}
79
80impl TryFrom<RawSourceSpan> for SourceSpan {
81 type Error = String;
82
83 fn try_from(raw: RawSourceSpan) -> Result<Self, String> {
84 match (raw.start_byte.is_some(), raw.end_byte.is_some()) {
85 (true, true) => {}
86 (false, false) if raw.source_name.is_some() => {}
87 (false, false) => {
88 return Err("a source span names a source, carries offsets, or both".to_string())
89 }
90 _ => return Err("start_byte and end_byte are set together or not at all".to_string()),
91 }
92 Ok(Self {
93 source_name: raw.source_name,
94 start_byte: raw.start_byte,
95 end_byte: raw.end_byte,
96 })
97 }
98}
99
100impl SourceSpan {
101 pub fn exact(source_name: Option<String>, start_byte: u64, end_byte: u64) -> Self {
103 Self {
104 source_name,
105 start_byte: Some(start_byte),
106 end_byte: Some(end_byte),
107 }
108 }
109
110 pub fn source_only(source_name: impl Into<String>) -> Self {
112 Self {
113 source_name: Some(source_name.into()),
114 start_byte: None,
115 end_byte: None,
116 }
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(deny_unknown_fields)]
124pub struct RelatedLocation {
125 pub message: String,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub span: Option<SourceSpan>,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
147#[serde(deny_unknown_fields)]
148pub struct Diagnostic {
149 pub code: String,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub phase: Option<DiagnosticPhase>,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub severity: Option<Severity>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub path: Option<String>,
156 pub message: String,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub span: Option<SourceSpan>,
159 #[serde(default, skip_serializing_if = "Vec::is_empty")]
160 pub related: Vec<RelatedLocation>,
161 #[serde(default, skip_serializing_if = "Vec::is_empty")]
162 pub notes: Vec<String>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub resource_limit: Option<ResourceLimitInfo>,
165}
166
167impl Diagnostic {
168 pub fn compiler(
170 code: impl Into<String>,
171 phase: DiagnosticPhase,
172 message: impl Into<String>,
173 ) -> Self {
174 Self {
175 code: code.into(),
176 phase: Some(phase),
177 severity: Some(Severity::Error),
178 path: None,
179 message: message.into(),
180 span: None,
181 related: Vec::new(),
182 notes: Vec::new(),
183 resource_limit: None,
184 }
185 }
186
187 pub fn violation(
190 code: impl Into<String>,
191 path: impl Into<String>,
192 message: impl Into<String>,
193 ) -> Self {
194 Self {
195 code: code.into(),
196 phase: None,
197 severity: None,
198 path: Some(path.into()),
199 message: message.into(),
200 span: None,
201 related: Vec::new(),
202 notes: Vec::new(),
203 resource_limit: None,
204 }
205 }
206
207 pub fn resource_violation(resource: &str, limit: u64, observed: u64) -> Self {
211 Self::violation(
212 "resource_limit_exceeded",
213 "$",
214 format!("resource {resource} exceeded limit {limit}; observed {observed}"),
215 )
216 .with_resource_limit(ResourceLimitInfo {
217 resource: resource.to_string(),
218 limit,
219 observed,
220 })
221 }
222
223 pub fn with_path(mut self, path: impl Into<String>) -> Self {
224 self.path = Some(path.into());
225 self
226 }
227
228 pub fn with_span(mut self, span: SourceSpan) -> Self {
229 self.span = Some(span);
230 self
231 }
232
233 pub fn with_related(mut self, related: Vec<RelatedLocation>) -> Self {
234 self.related = related;
235 self
236 }
237
238 pub fn with_notes(mut self, notes: Vec<String>) -> Self {
239 self.notes = notes;
240 self
241 }
242
243 pub fn with_resource_limit(mut self, resource_limit: ResourceLimitInfo) -> Self {
244 self.resource_limit = Some(resource_limit);
245 self
246 }
247}
248
249#[cfg(feature = "compiler")]
257#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct CompileError {
259 pub diagnostics: Vec<Diagnostic>,
260}
261
262#[cfg(feature = "compiler")]
263impl CompileError {
264 pub(crate) fn single(
265 code: impl Into<String>,
266 phase: DiagnosticPhase,
267 message: impl Into<String>,
268 ) -> Self {
269 Self {
270 diagnostics: vec![Diagnostic::compiler(code, phase, message)],
271 }
272 }
273
274 pub(crate) fn resource_limit(
275 resource: &str,
276 limit: usize,
277 observed: usize,
278 message: impl Into<String>,
279 ) -> Self {
280 Self {
281 diagnostics: vec![Diagnostic::compiler(
282 "resource_limit_exceeded",
283 DiagnosticPhase::Load,
284 message,
285 )
286 .with_resource_limit(ResourceLimitInfo {
287 resource: resource.to_string(),
288 limit: crate::limits::portable_count(limit),
289 observed: crate::limits::portable_count(observed),
290 })],
291 }
292 }
293}
294
295#[cfg(feature = "compiler")]
296impl fmt::Display for CompileError {
297 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
298 if let Some(diagnostic) = self.diagnostics.first() {
299 write!(formatter, "{}: {}", diagnostic.code, diagnostic.message)
300 } else {
301 write!(formatter, "Candid compilation failed")
302 }
303 }
304}
305
306#[cfg(feature = "compiler")]
307impl std::error::Error for CompileError {}