1use std::fmt;
7
8use serde::Serialize;
9use serde_json::{Map, Value as JsonValue};
10
11use crate::ast::Span;
12
13pub type Result<T, E = Error> = std::result::Result<T, E>;
15
16#[derive(Debug, Clone, PartialEq, thiserror::Error)]
22pub struct Error(Box<ErrorInner>);
23
24#[derive(Debug, Clone, PartialEq, Serialize)]
25struct ErrorInner {
26 code: String,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 position: Option<usize>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 span: Option<Span>,
31 #[serde(flatten)]
32 details: Map<String, JsonValue>,
33}
34
35impl Serialize for Error {
36 fn serialize<S: serde::Serializer>(
37 &self,
38 serializer: S,
39 ) -> std::result::Result<S::Ok, S::Error> {
40 #[derive(Serialize)]
41 struct Repr<'a> {
42 #[serde(flatten)]
43 inner: &'a ErrorInner,
44 message: String,
45 }
46 Repr {
47 inner: &self.0,
48 message: self.message(),
49 }
50 .serialize(serializer)
51 }
52}
53
54impl Error {
55 pub fn new(code: impl Into<String>) -> Self {
57 Error(Box::new(ErrorInner {
58 code: code.into(),
59 position: None,
60 span: None,
61 details: Map::new(),
62 }))
63 }
64
65 pub fn with(mut self, key: &str, value: impl Into<JsonValue>) -> Self {
67 self.0.details.insert(key.to_owned(), value.into());
68 self
69 }
70
71 pub(crate) fn located(mut self, source: &str, span: Span, position_byte: usize) -> Self {
74 self.0.position = Some(utf16_offset(source, position_byte));
75 self.0.span = Some(span);
76 self
77 }
78
79 pub(crate) fn at(mut self, span: Span, position: usize) -> Self {
81 self.0.position = Some(position);
82 self.0.span = Some(span);
83 self
84 }
85
86 pub(crate) fn at_if_missing(self, span: Span, position: usize) -> Self {
87 if self.0.position.is_none() {
88 self.at(span, position)
89 } else {
90 self
91 }
92 }
93
94 pub(crate) fn with_token_if_missing(self, token: &str) -> Self {
95 if self.0.details.contains_key("token") {
96 self
97 } else {
98 self.with("token", token)
99 }
100 }
101
102 pub fn code(&self) -> &str {
104 &self.0.code
105 }
106
107 pub fn message(&self) -> String {
109 match template(&self.0.code) {
110 Some(template) => populate(template, &self.0.details),
111 None => self
112 .0
113 .details
114 .get("message")
115 .and_then(JsonValue::as_str)
116 .unwrap_or("Unknown error")
117 .to_owned(),
118 }
119 }
120
121 pub fn position(&self) -> Option<usize> {
126 self.0.position
127 }
128
129 pub fn span(&self) -> Option<Span> {
131 self.0.span
132 }
133
134 pub fn token(&self) -> Option<String> {
136 self.0.details.get("token").map(|t| match t {
137 JsonValue::String(s) => s.clone(),
138 other => other.to_string(),
139 })
140 }
141
142 pub fn value(&self) -> Option<&JsonValue> {
144 self.0.details.get("value")
145 }
146
147 pub fn detail(&self, key: &str) -> Option<&JsonValue> {
149 self.0.details.get(key)
150 }
151}
152
153impl fmt::Display for Error {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 write!(f, "{}: {}", self.0.code, self.message())?;
156 if let Some(pos) = self.0.position {
157 write!(f, " (at position {pos})")?;
158 }
159 Ok(())
160 }
161}
162
163pub(crate) fn utf16_offset(source: &str, byte: usize) -> usize {
165 let byte = byte.min(source.len());
166 let mut end = byte;
167 while !source.is_char_boundary(end) {
168 end -= 1;
169 }
170 source[..end].encode_utf16().count()
171}
172
173fn populate(template: &str, details: &Map<String, JsonValue>) -> String {
176 let mut out = String::with_capacity(template.len() + 16);
177 let mut rest = template;
178 while let Some(start) = rest.find("{{") {
179 out.push_str(&rest[..start]);
180 let triple = rest[start..].starts_with("{{{");
181 let (open, close) = if triple { (3, "}}}") } else { (2, "}}") };
182 let Some(len) = rest[start + open..].find(close) else {
183 out.push_str(&rest[start..]);
184 return out;
185 };
186 let key = &rest[start + open..start + open + len];
187 match (details.get(key), triple) {
188 (Some(JsonValue::String(s)), true) => out.push_str(s),
189 (Some(v), _) => out.push_str(&v.to_string()),
190 (None, _) => out.push_str("undefined"),
191 }
192 rest = &rest[start + open + len + close.len()..];
193 }
194 out.push_str(rest);
195 out
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
200#[serde(rename_all = "lowercase")]
201pub enum Severity {
202 Error,
204 Warning,
207}
208
209#[derive(Debug, Clone, PartialEq, Serialize)]
211pub struct Diagnostic {
212 pub severity: Severity,
214 pub code: String,
216 pub message: String,
218 pub span: Span,
220}
221
222impl Diagnostic {
223 pub fn new(severity: Severity, code: &str, span: Span, details: &[(&str, &str)]) -> Self {
226 let mut d = Diagnostic::warning(code, span, details);
227 d.severity = severity;
228 d
229 }
230
231 pub(crate) fn warning(code: &str, span: Span, details: &[(&str, &str)]) -> Self {
232 let mut map = Map::new();
233 for (k, v) in details {
234 map.insert((*k).to_owned(), JsonValue::String((*v).to_owned()));
235 }
236 Diagnostic {
237 severity: Severity::Warning,
238 code: code.to_owned(),
239 message: populate(template(code).unwrap_or("{{{message}}}"), &map),
240 span,
241 }
242 }
243}
244
245impl From<&Error> for Diagnostic {
246 fn from(error: &Error) -> Self {
247 Diagnostic {
248 severity: Severity::Error,
249 code: error.code().to_owned(),
250 message: error.message(),
251 span: error.span().unwrap_or_default(),
252 }
253 }
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
258pub enum Category {
259 Static,
261 Type,
263 Dynamic,
265 Guardrail,
268 Warning,
270}
271
272#[derive(Debug, Clone, Copy, Serialize)]
274pub struct CatalogEntry {
275 pub code: &'static str,
277 pub template: &'static str,
279}
280
281impl CatalogEntry {
282 pub fn category(&self) -> Category {
284 match self.code.as_bytes()[0] {
285 b'S' => Category::Static,
286 b'T' => Category::Type,
287 b'D' => Category::Dynamic,
288 b'U' => Category::Guardrail,
289 _ => Category::Warning,
290 }
291 }
292}
293
294pub fn catalog() -> &'static [CatalogEntry] {
296 CATALOG
297}
298
299fn template(code: &str) -> Option<&'static str> {
300 CATALOG.iter().find(|e| e.code == code).map(|e| e.template)
301}
302
303macro_rules! catalog {
304 ($($code:literal => $template:literal,)*) => {
305 static CATALOG: &[CatalogEntry] = &[$(CatalogEntry { code: $code, template: $template },)*];
306 };
307}
308
309catalog! {
310 "S0101" => "String literal must be terminated by a matching quote",
311 "S0102" => "Number out of range: {{token}}",
312 "S0103" => "Unsupported escape sequence: \\{{token}}",
313 "S0104" => "The escape sequence \\u must be followed by 4 hex digits",
314 "S0105" => "Quoted property name must be terminated with a backquote (`)",
315 "S0106" => "Comment has no closing tag",
316 "S0201" => "Syntax error: {{token}}",
317 "S0202" => "Expected {{value}}, got {{token}}",
318 "S0203" => "Expected {{value}} before end of expression",
319 "S0204" => "Unknown operator: {{token}}",
320 "S0205" => "Unexpected token: {{token}}",
321 "S0206" => "Unknown expression type: {{token}}",
322 "S0207" => "Unexpected end of expression",
323 "S0208" => "Parameter {{value}} of function definition must be a variable name (start with $)",
324 "S0209" => "A predicate cannot follow a grouping expression in a step",
325 "S0210" => "Each step can only have one grouping expression",
326 "S0211" => "The symbol {{token}} cannot be used as a unary operator",
327 "S0212" => "The left side of := must be a variable name (start with $)",
328 "S0213" => "The literal value {{value}} cannot be used as a step within a path expression",
329 "S0214" => "The right side of {{token}} must be a variable name (start with $)",
330 "S0215" => "A context variable binding must precede any predicates on a step",
331 "S0216" => "A context variable binding must precede the 'order-by' clause on a step",
332 "S0217" => "The object representing the 'parent' cannot be derived from this expression",
333 "S0301" => "Empty regular expressions are not allowed",
334 "S0302" => "No terminating / in regular expression",
335 "S0402" => "Choice groups containing parameterized types are not supported",
336 "S0401" => "Type parameters can only be applied to functions and arrays",
337 "S0500" => "Attempted to evaluate an expression containing syntax error(s)",
338 "T0410" => "Argument {{index}} of function {{token}} does not match function signature",
339 "T0411" => "Context value is not a compatible type with argument {{index}} of function {{token}}",
340 "T0412" => "Argument {{index}} of function {{token}} must be an array of {{type}}",
341 "D1001" => "Number out of range: {{value}}",
342 "D1002" => "Cannot negate a non-numeric value: {{value}}",
343 "T1003" => "Key in object structure must evaluate to a string; got: {{value}}",
344 "D1004" => "Regular expression matches zero length string",
345 "T1005" => "Attempted to invoke a non-function. Did you mean ${{{token}}}?",
346 "T1006" => "Attempted to invoke a non-function",
347 "T1007" => "Attempted to partially apply a non-function. Did you mean ${{{token}}}?",
348 "T1008" => "Attempted to partially apply a non-function",
349 "D1009" => "Multiple key definitions evaluate to same key: {{value}}",
350 "T1010" => "The matcher function argument passed to function {{token}} does not return the correct object structure",
351 "D1011" => "Stack overflow. Check for non-terminating recursive function. Consider rewriting as tail-recursive",
352 "D1012" => "Evaluation timeout after {{value}} milliseconds. Check for infinite loop",
353 "D1013" => "Object property names starting with _jsonata_ are reserved for internal use: {{value}}",
354 "T2001" => "The left side of the {{token}} operator must evaluate to a number",
355 "T2002" => "The right side of the {{token}} operator must evaluate to a number",
356 "T2003" => "The left side of the range operator (..) must evaluate to an integer",
357 "T2004" => "The right side of the range operator (..) must evaluate to an integer",
358 "D2005" => "The left side of := must be a variable name (start with $)",
359 "T2006" => "The right side of the function application operator ~> must be a function",
360 "T2007" => "Type mismatch when comparing values {{value}} and {{value2}} in order-by clause",
361 "T2008" => "The expressions within an order-by clause must evaluate to numeric or string values",
362 "T2009" => "The values {{value}} and {{value2}} either side of operator {{token}} must be of the same data type",
363 "T2010" => "The expressions either side of operator {{token}} must evaluate to numeric or string values",
364 "T2011" => "The insert/update clause of the transform expression must evaluate to an object: {{value}}",
365 "T2012" => "The delete clause of the transform expression must evaluate to a string or array of strings: {{value}}",
366 "T2013" => "The transform expression clones the input object using the $clone() function. This has been overridden in the current scope by a non-function.",
367 "D2014" => "The size of the sequence allocated by the range operator (..) must not exceed 1e7. Attempted to allocate {{value}}.",
368 "D2015" => "The maximum sequence length of {{value}} was exceeded.",
369 "D2016" => "The size of the string requested by the $pad function must not exceed 1e7. Attempted to allocate {{value}}.",
370 "D3001" => "Attempting to invoke string function on Infinity or NaN",
371 "D3010" => "Second argument of replace function cannot be an empty string",
372 "D3011" => "Fourth argument of replace function must evaluate to a positive number",
373 "D3012" => "Attempted to replace a matched string with a non-string value",
374 "D3020" => "Third argument of split function must evaluate to a positive number",
375 "D3030" => "Unable to cast value to a number: {{value}}",
376 "D3040" => "Third argument of match function must evaluate to a positive number",
377 "D3050" => "The second argument of reduce function must be a function with at least two arguments",
378 "D3060" => "The sqrt function cannot be applied to a negative number: {{value}}",
379 "D3061" => "The power function has resulted in a value that cannot be represented as a JSON number: base={{value}}, exponent={{exp}}",
380 "D3070" => "The single argument form of the sort function can only be applied to an array of strings or an array of numbers. Use the second argument to specify a comparison function",
381 "D3080" => "The picture string must only contain a maximum of two sub-pictures",
382 "D3081" => "The sub-picture must not contain more than one instance of the 'decimal-separator' character",
383 "D3082" => "The sub-picture must not contain more than one instance of the 'percent' character",
384 "D3083" => "The sub-picture must not contain more than one instance of the 'per-mille' character",
385 "D3084" => "The sub-picture must not contain both a 'percent' and a 'per-mille' character",
386 "D3085" => "The mantissa part of a sub-picture must contain at least one character that is either an 'optional digit character' or a member of the 'decimal digit family'",
387 "D3086" => "The sub-picture must not contain a passive character that is preceded by an active character and that is followed by another active character",
388 "D3087" => "The sub-picture must not contain a 'grouping-separator' character that appears adjacent to a 'decimal-separator' character",
389 "D3088" => "The sub-picture must not contain a 'grouping-separator' at the end of the integer part",
390 "D3089" => "The sub-picture must not contain two adjacent instances of the 'grouping-separator' character",
391 "D3090" => "The integer part of the sub-picture must not contain a member of the 'decimal digit family' that is followed by an instance of the 'optional digit character'",
392 "D3091" => "The fractional part of the sub-picture must not contain an instance of the 'optional digit character' that is followed by a member of the 'decimal digit family'",
393 "D3092" => "A sub-picture that contains a 'percent' or 'per-mille' character must not contain a character treated as an 'exponent-separator'",
394 "D3093" => "The exponent part of the sub-picture must comprise only of one or more characters that are members of the 'decimal digit family'",
395 "D3100" => "The radix of the formatBase function must be between 2 and 36. It was given {{value}}",
396 "D3110" => "The argument of the toMillis function must be an ISO 8601 formatted timestamp. Given {{value}}",
397 "D3120" => "Syntax error in expression passed to function eval: {{value}}",
398 "D3121" => "Dynamic error evaluating the expression passed to function eval: {{value}}",
399 "D3130" => "Formatting or parsing an integer as a sequence starting with {{value}} is not supported by this implementation",
400 "D3131" => "In a decimal digit pattern, all digits must be from the same decimal group",
401 "D3132" => "Unknown component specifier {{value}} in date/time picture string",
402 "D3133" => "The 'name' modifier can only be applied to months and days in the date/time picture string, not {{value}}",
403 "D3134" => "The timezone integer format specifier cannot have more than four digits",
404 "D3135" => "No matching closing bracket ']' in date/time picture string",
405 "D3136" => "The date/time picture string is missing specifiers required to parse the timestamp",
406 "D3137" => "{{{message}}}",
407 "D3138" => "The $single() function expected exactly 1 matching result. Instead it matched more.",
408 "D3139" => "The $single() function expected exactly 1 matching result. Instead it matched 0.",
409 "D3140" => "Malformed URL passed to ${{{functionName}}}(): {{value}}",
410 "D3141" => "{{{message}}}",
411 "D3150" => "The timezone must be a number of hours and minutes, such as +0530: {{value}}",
412 "U1001" => "{{{message}}}",
413 "U1002" => "The function ${{{token}}} returned a promise: asynchronous functions are not supported",
414 "U1003" => "The evaluation was cancelled",
415 "U1004" => "The input could not be read: {{{message}}}",
416 "U1005" => "A function passed to ${{{token}}} was called after ${{{token}}} returned",
417 "U2001" => "The input is not valid JSON: {{{message}}}",
418 "W0101" => "This annotation is not attached to an expression and will be ignored",
419 "W0102" => "Unknown annotation tag @{{{tag}}}; expected @label, @message or @id",
420 "W0103" => "The value of annotation tag @{{{tag}}} should be a JSON string literal",
421 "W0201" => "Field {{token}} is not in the schema{{{hint}}}",
422 "W0202" => "Unknown function ${{{token}}}{{{hint}}}",
423 "W0203" => "Function ${{{token}}} takes {{{expected}}} but is given {{{count}}}",
424 "W0204" => "Variable ${{{token}}} is not declared{{{hint}}}",
425 "W0205" => "Argument {{{index}}} of ${{{token}}} is {{{type}}}, which does not match the signature {{{signature}}}",
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
433 fn messages_follow_jsonata_templates() {
434 let err = Error::new("S0202").with("token", ";").with("value", "]");
435 assert_eq!(err.message(), r#"Expected "]", got ";""#);
436 let err = Error::new("T1005").with("token", "sum");
437 assert_eq!(
438 err.message(),
439 "Attempted to invoke a non-function. Did you mean $sum?"
440 );
441 assert_eq!(
442 Error::new("S0211").message(),
443 "The symbol undefined cannot be used as a unary operator"
444 );
445 }
446
447 #[test]
448 fn positions_are_utf16_offsets() {
449 assert_eq!(utf16_offset("aé𝄞b", 1), 1);
450 assert_eq!(utf16_offset("aé𝄞b", 3), 2);
451 assert_eq!(utf16_offset("aé𝄞b", 7), 4);
452 }
453
454 #[test]
455 fn catalog_codes_are_unique() {
456 let mut codes: Vec<_> = catalog().iter().map(|e| e.code).collect();
457 codes.sort_unstable();
458 codes.dedup();
459 assert_eq!(codes.len(), catalog().len());
460 }
461}