1use std::fmt;
12
13use datavalue::OwnedDataValue;
14use serde::Serialize;
15
16use crate::node::MAX_COMPILE_DEPTH;
17use crate::operators::meta::{ArgsForm, Effect, Extra, Miss};
18use crate::{Engine, OpCode};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum CheckMode {
24 Engine,
27 Strict,
30 Template,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
37#[serde(rename_all = "lowercase")]
38#[non_exhaustive]
39pub enum Severity {
40 Error,
42 Warning,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
48#[non_exhaustive]
49pub enum DiagnosticCode {
50 Unparsable,
52 UnknownOperator,
54 NotAnOperator,
57 ArgumentForm,
59 ArgumentCount,
62 InvalidTimezone,
64 SimilarToOperator,
67 Compile,
70 OperatorCheck,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
77#[non_exhaustive]
78pub struct Diagnostic {
79 pub code: DiagnosticCode,
81 pub severity: Severity,
83 pub message: String,
85 pub pointer: String,
90 pub operator: Option<String>,
92}
93
94impl Diagnostic {
95 pub fn error(message: impl Into<String>) -> Self {
101 Self::from_operator(Severity::Error, message.into())
102 }
103
104 pub fn warning(message: impl Into<String>) -> Self {
108 Self::from_operator(Severity::Warning, message.into())
109 }
110
111 #[must_use]
115 pub fn at_argument(mut self, index: usize) -> Self {
116 self.pointer = format!("/{index}");
117 self
118 }
119
120 fn from_operator(severity: Severity, message: String) -> Self {
121 Diagnostic {
122 code: DiagnosticCode::OperatorCheck,
123 severity,
124 message,
125 pointer: String::new(),
126 operator: None,
127 }
128 }
129}
130
131impl fmt::Display for Diagnostic {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 let severity = match self.severity {
134 Severity::Error => "error",
135 Severity::Warning => "warning",
136 };
137 let at = if self.pointer.is_empty() {
138 "/"
139 } else {
140 &self.pointer
141 };
142 write!(f, "{severity} at {at}: {}", self.message)
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
150#[non_exhaustive]
151pub struct CompileError {
152 pub diagnostics: Vec<Diagnostic>,
154}
155
156impl fmt::Display for CompileError {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 let errors = self
159 .diagnostics
160 .iter()
161 .filter(|d| d.severity == Severity::Error)
162 .count();
163 write!(f, "rule has {errors} error(s)")?;
164 for d in &self.diagnostics {
165 write!(f, "\n {d}")?;
166 }
167 Ok(())
168 }
169}
170
171impl std::error::Error for CompileError {}
172
173impl CompileError {
174 pub(crate) fn from_error(err: crate::Error) -> Self {
175 let code = match err.kind {
176 crate::ErrorKind::ParseError(_) => DiagnosticCode::Unparsable,
177 _ => DiagnosticCode::Compile,
178 };
179 CompileError {
180 diagnostics: vec![Diagnostic {
181 code,
182 severity: Severity::Error,
183 message: crate::Error::new(err.kind).to_string(),
184 pointer: String::new(),
185 operator: None,
186 }],
187 }
188 }
189}
190
191pub(crate) fn check(engine: &Engine, rule: &OwnedDataValue, templating: bool) -> Vec<Diagnostic> {
193 let mut checker = Checker {
194 engine,
195 templating,
196 escape: if templating {
197 engine.template_key_escape()
198 } else {
199 None
200 },
201 out: Vec::new(),
202 depth: 0,
203 too_deep: false,
204 protected: 0,
205 unread: 0,
206 };
207 checker.node(rule, &mut String::new());
208 checker.out
209}
210
211struct Checker<'e> {
212 engine: &'e Engine,
213 templating: bool,
214 escape: Option<char>,
215 out: Vec<Diagnostic>,
216 depth: usize,
219 too_deep: bool,
221 protected: usize,
224 unread: usize,
228}
229
230fn under(pointer: &mut String, token: &str, f: impl FnOnce(&mut String)) {
232 let len = pointer.len();
233 crate::node::push_pointer_token(pointer, token);
234 f(pointer);
235 pointer.truncate(len);
236}
237
238impl Checker<'_> {
239 fn report(
240 &mut self,
241 code: DiagnosticCode,
242 severity: Severity,
243 pointer: &str,
244 operator: &str,
245 message: String,
246 ) {
247 if self.unread > 0 {
248 return;
249 }
250 let severity = if self.protected > 0 {
251 Severity::Warning
252 } else {
253 severity
254 };
255 self.out.push(Diagnostic {
256 code,
257 severity,
258 message,
259 pointer: pointer.to_string(),
260 operator: Some(operator.to_string()),
261 });
262 }
263
264 fn node(&mut self, node: &OwnedDataValue, pointer: &mut String) {
265 if self.depth >= MAX_COMPILE_DEPTH {
266 if !self.too_deep {
267 self.too_deep = true;
268 self.out.push(Diagnostic {
269 code: DiagnosticCode::Compile,
270 severity: Severity::Error,
271 message: format!(
272 "rule nesting exceeds the maximum compile depth of {}",
273 MAX_COMPILE_DEPTH
274 ),
275 pointer: pointer.clone(),
276 operator: None,
277 });
278 }
279 return;
280 }
281 self.depth += 1;
282 self.node_inner(node, pointer);
283 self.depth -= 1;
284 }
285
286 fn node_inner(&mut self, node: &OwnedDataValue, pointer: &mut String) {
287 match node {
288 OwnedDataValue::Object(pairs) if pairs.len() > 1 => {
289 if self.templating {
290 for (key, value) in pairs {
291 under(pointer, key, |p| self.node(value, p));
292 }
293 } else {
294 self.out.push(Diagnostic {
295 code: DiagnosticCode::NotAnOperator,
296 severity: Severity::Error,
297 message: format!(
298 "an object with {} keys is not an operator call; it is only valid as an output template",
299 pairs.len()
300 ),
301 pointer: pointer.clone(),
302 operator: None,
303 });
304 }
305 }
306 OwnedDataValue::Object(pairs) if pairs.len() == 1 => {
307 let (op, argv) = &pairs[0];
308 self.call(op, argv, pointer);
309 }
310 OwnedDataValue::Array(items) => {
311 let mut buf = itoa::Buffer::new();
312 for (i, item) in items.iter().enumerate() {
313 under(pointer, buf.format(i), |p| self.node(item, p));
314 }
315 }
316 _ => {}
317 }
318 }
319
320 fn args(&mut self, op: &str, argv: &OwnedDataValue, pointer: &mut String) {
322 self.args_where(op, argv, pointer, |_| ArgUse::Runs);
323 }
324
325 fn args_where(
330 &mut self,
331 op: &str,
332 argv: &OwnedDataValue,
333 pointer: &mut String,
334 uses: impl Fn(usize) -> ArgUse,
335 ) {
336 let one = |this: &mut Self, i: usize, item: &OwnedDataValue, p: &mut String| match uses(i) {
337 ArgUse::Never => {
340 this.unread += 1;
341 this.node(item, p);
342 this.unread -= 1;
343 }
344 ArgUse::Runs => this.node(item, p),
345 ArgUse::Caught => {
346 this.protected += 1;
347 this.node(item, p);
348 this.protected -= 1;
349 }
350 };
351 under(pointer, op, |p| match argv {
352 OwnedDataValue::Array(items) => {
353 let mut buf = itoa::Buffer::new();
354 for (i, item) in items.iter().enumerate() {
355 under(p, buf.format(i), |p| one(self, i, item, p));
356 }
357 }
358 other => one(self, 0, other, p),
359 });
360 }
361
362 fn call(&mut self, op: &str, argv: &OwnedDataValue, pointer: &mut String) {
363 if self.escape.is_some_and(|c| op.starts_with(c)) {
366 under(pointer, op, |p| self.node(argv, p));
367 return;
368 }
369 let count = match argv {
370 OwnedDataValue::Array(items) => items.len(),
371 _ => 1,
372 };
373 if let Some(opcode) = self.engine.builtin(op) {
374 self.builtin(op, opcode, argv, count, pointer);
375 return;
376 }
377 if let Some(custom) = self.engine.custom_operator(op) {
378 let info = custom.info();
379 if !info.accepts(count) {
380 let message = format!(
381 "`{op}` takes {}, not {count}",
382 describe_count(info.min_args, info.max_args)
383 );
384 self.report(
385 DiagnosticCode::ArgumentCount,
386 Severity::Error,
387 pointer,
388 op,
389 message,
390 );
391 } else {
392 let args = match argv {
393 OwnedDataValue::Array(items) => items.as_slice(),
394 other => std::slice::from_ref(other),
395 };
396 if let Err(d) = custom.check(args) {
397 self.operator_diagnostic(d, op, argv, pointer);
398 }
399 }
400 self.args(op, argv, pointer);
401 return;
402 }
403 if self.templating {
404 if let Some(near) = self.similar_operator(op) {
405 let message =
406 format!("`{op}` is an output field here; did you mean the operator `{near}`?");
407 self.report(
408 DiagnosticCode::SimilarToOperator,
409 Severity::Warning,
410 pointer,
411 op,
412 message,
413 );
414 }
415 under(pointer, op, |p| self.node(argv, p));
416 return;
417 }
418 let message = match self.similar_operator(op) {
419 Some(near) => format!("unknown operator `{op}`; did you mean `{near}`?"),
420 None => format!("unknown operator `{op}`"),
421 };
422 self.report(
423 DiagnosticCode::UnknownOperator,
424 Severity::Error,
425 pointer,
426 op,
427 message,
428 );
429 self.args(op, argv, pointer);
430 }
431
432 fn operator_diagnostic(
435 &mut self,
436 mut d: Diagnostic,
437 op: &str,
438 argv: &OwnedDataValue,
439 pointer: &str,
440 ) {
441 let relative = std::mem::take(&mut d.pointer);
442 let mut placed = pointer.to_string();
443 if !relative.is_empty() {
444 crate::node::push_pointer_token(&mut placed, op);
445 match (argv, relative.strip_prefix("/0")) {
448 (OwnedDataValue::Array(_), _) => placed.push_str(&relative),
449 (_, Some(rest)) if rest.is_empty() || rest.starts_with('/') => {
450 placed.push_str(rest)
451 }
452 _ => placed.push_str(&relative),
453 }
454 }
455 d.pointer = placed;
456 d.operator.get_or_insert_with(|| op.to_string());
457 if self.unread > 0 {
458 return;
459 }
460 if self.protected > 0 {
461 d.severity = Severity::Warning;
462 }
463 self.out.push(d);
464 }
465
466 fn builtin(
467 &mut self,
468 op: &str,
469 opcode: OpCode,
470 argv: &OwnedDataValue,
471 count: usize,
472 pointer: &mut String,
473 ) {
474 let meta = opcode.meta();
475 if meta.args_form == ArgsForm::ArrayOnly && !matches!(argv, OwnedDataValue::Array(_)) {
476 let message = format!("`{op}` takes its arguments as an array");
477 self.report(
478 DiagnosticCode::ArgumentForm,
479 Severity::Error,
480 pointer,
481 op,
482 message,
483 );
484 return;
485 }
486 #[cfg(feature = "tensor")]
487 if opcode == OpCode::TensorMake
488 && let OwnedDataValue::Object(fields) = argv
489 && crate::compile::hooks::is_tensor_wire_body(fields)
490 {
491 return;
492 }
493
494 let arity = opcode.arity();
495 let (min, max) = (arity.min as usize, arity.max.map(usize::from));
496 let mut reads = ..count;
499 if count < min {
500 if let Miss::Return(_) = meta.on_missing {
501 reads = ..0;
502 }
503 if matches!(meta.on_missing, Miss::InvalidArgs | Miss::Err(_)) {
504 let message = format!("`{op}` takes {}, not {count}", describe_count(min, max));
505 self.report(
506 DiagnosticCode::ArgumentCount,
507 Severity::Error,
508 pointer,
509 op,
510 message,
511 );
512 }
513 } else if let Some(max) = max
514 && count > max
515 {
516 let (severity, message) = match meta.on_extra {
517 Extra::InvalidArgs | Extra::Err(_) => (
518 Severity::Error,
519 format!(
520 "`{op}` takes {}, not {count}",
521 describe_count(min, Some(max))
522 ),
523 ),
524 Extra::Ignore => {
525 reads = ..max;
526 (
527 Severity::Warning,
528 format!(
529 "`{op}` reads {}; the other {} never evaluated",
530 describe_count(min, Some(max)),
531 plural(count - max, "argument is", "arguments are")
532 ),
533 )
534 }
535 Extra::Return(_) => {
536 reads = ..0;
537 (
538 Severity::Warning,
539 format!(
540 "`{op}` takes {}; with {count} it returns a fixed value without evaluating them",
541 describe_count(min, Some(max))
542 ),
543 )
544 }
545 };
546 self.report(
547 DiagnosticCode::ArgumentCount,
548 severity,
549 pointer,
550 op,
551 message,
552 );
553 }
554
555 #[cfg(feature = "datetime")]
556 if matches!(opcode, OpCode::ParseDate | OpCode::FormatDate)
557 && let OwnedDataValue::Array(items) = argv
558 && let Some(OwnedDataValue::String(zone)) = items.get(2)
559 && zone.parse::<chrono_tz::Tz>().is_err()
560 {
561 let mut at = pointer.clone();
562 crate::node::push_pointer_token(&mut at, op);
563 crate::node::push_pointer_token(&mut at, "2");
564 let message = format!("unknown timezone `{zone}`");
565 self.report(
566 DiagnosticCode::InvalidTimezone,
567 Severity::Error,
568 &at,
569 op,
570 message,
571 );
572 }
573
574 let caught = if matches!(meta.effect, Effect::Catches) {
577 count.saturating_sub(1)
578 } else {
579 0
580 };
581 self.args_where(op, argv, pointer, |i| {
582 if !reads.contains(&i) {
583 ArgUse::Never
584 } else if i < caught {
585 ArgUse::Caught
586 } else {
587 ArgUse::Runs
588 }
589 });
590 }
591
592 fn similar_operator(&self, key: &str) -> Option<String> {
596 let key_chars = key.chars().count();
597 let close = |name: &str| {
598 let name_chars = name.chars().count();
599 name_chars >= 3
600 && key_chars.abs_diff(name_chars) <= 1
601 && one_edit_apart_with_len(key, key_chars, name, name_chars)
602 };
603 if let Some(name) = self.engine.builtin_operator_names().find(|n| close(n)) {
604 return Some(name.to_string());
605 }
606 let mut custom: Vec<&str> = self
607 .engine
608 .custom_operator_names()
609 .filter(|n| close(n))
610 .collect();
611 custom.sort_unstable();
612 custom.first().map(|n| n.to_string())
613 }
614}
615
616#[derive(Clone, Copy)]
618enum ArgUse {
619 Never,
621 Runs,
623 Caught,
625}
626
627fn plural(n: usize, one: &str, many: &str) -> String {
628 if n == 1 {
629 format!("1 {one}")
630 } else {
631 format!("{n} {many}")
632 }
633}
634
635fn describe_count(min: usize, max: Option<usize>) -> String {
637 let noun = |n: usize| if n == 1 { "argument" } else { "arguments" };
638 match max {
639 Some(max) if max == min => format!("exactly {min} {}", noun(min)),
640 Some(max) => format!("{min} to {max} {}", noun(max)),
641 None => format!("at least {min} {}", noun(min)),
642 }
643}
644
645#[cfg(test)]
649fn one_edit_apart(a: &str, b: &str) -> bool {
650 one_edit_apart_with_len(a, a.chars().count(), b, b.chars().count())
651}
652
653fn one_edit_apart_with_len(a: &str, la: usize, b: &str, lb: usize) -> bool {
654 match la.abs_diff(lb) {
655 0 => a.chars().zip(b.chars()).filter(|(x, y)| x != y).count() == 1,
656 1 => {
657 let (long, short) = if la > lb { (a, b) } else { (b, a) };
658 let mut long = long.chars();
659 let mut skipped = false;
660 for c in short.chars() {
661 loop {
662 match long.next() {
663 Some(x) if x == c => break,
664 Some(_) if !skipped => skipped = true,
665 _ => return false,
666 }
667 }
668 }
669 true
670 }
671 _ => false,
672 }
673}
674
675#[cfg(test)]
676mod tests {
677 use super::{describe_count, one_edit_apart};
678
679 #[test]
680 fn distances() {
681 assert!(one_edit_apart("vr", "var"));
682 assert!(one_edit_apart("var", "vr"));
683 assert!(one_edit_apart("mapp", "map"));
684 assert!(one_edit_apart("mop", "map"));
685 assert!(one_edit_apart("vars", "var"));
686 assert!(!one_edit_apart("map", "map"));
687 assert!(!one_edit_apart("name", "none"));
688 assert!(!one_edit_apart("ab", "ba"));
689 assert!(!one_edit_apart("", "abc"));
690 assert!(!one_edit_apart("mapxy", "map"));
691 }
692
693 #[test]
694 fn counts() {
695 assert_eq!(describe_count(2, Some(2)), "exactly 2 arguments");
696 assert_eq!(describe_count(1, Some(1)), "exactly 1 argument");
697 assert_eq!(describe_count(1, None), "at least 1 argument");
698 assert_eq!(describe_count(1, Some(3)), "1 to 3 arguments");
699 }
700}