cel_parser/error.rs
1//! Parse error type with message and source span for CEL.
2//!
3//! Uses a [`SourceSpan`] (line/column only) so errors are `Send + Sync` and can
4//! be used from async execution. Use [`SourceSpan::from_proc_macro2`] to extract
5//! location from a `proc_macro2::Span` when building errors in the parser.
6
7use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet};
8use proc_macro2::LineColumn;
9use std::str::FromStr;
10
11/// Source region as start/end line and column.
12///
13/// Uses [`proc_macro2::LineColumn`] for positions (1-based line, 0-based column).
14/// This type is `Send + Sync`. Build it from a `proc_macro2::Span` via
15/// [`SourceSpan::from_proc_macro2`] when you have one (e.g. in the parser).
16///
17/// # Examples
18/// See [`Self::new`] and [`Self::to_byte_range`].
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub struct SourceSpan {
21 /// Start position (inclusive).
22 pub start: LineColumn,
23 /// End position (exclusive; one past the last character, matching `proc_macro2::Span::end()`).
24 pub end: LineColumn,
25}
26
27impl Default for SourceSpan {
28 /// Returns an empty region at line one, column zero.
29 fn default() -> Self {
30 SourceSpan {
31 start: LineColumn { line: 1, column: 0 },
32 end: LineColumn { line: 1, column: 0 },
33 }
34 }
35}
36
37impl SourceSpan {
38 /// Builds a span from raw line/column values.
39 ///
40 /// Lines are 1-based, columns are 0-based (matching [`proc_macro2::LineColumn`]).
41 ///
42 /// # Examples
43 ///
44 /// ```rust
45 /// use cel_parser::SourceSpan;
46 ///
47 /// let span = SourceSpan::new(1, 0, 1, 5);
48 /// assert_eq!(span.start.line, 1);
49 /// assert_eq!(span.end.column, 5);
50 /// ```
51 pub fn new(start_line: usize, start_column: usize, end_line: usize, end_column: usize) -> Self {
52 SourceSpan {
53 start: LineColumn {
54 line: start_line,
55 column: start_column,
56 },
57 end: LineColumn {
58 line: end_line,
59 column: end_column,
60 },
61 }
62 }
63
64 /// Extracts start/end line and column from a `proc_macro2::Span`.
65 ///
66 /// Use this when creating errors in the parser or other code that has a
67 /// `proc_macro2::Span`; the result is `Send + Sync` and can be stored in
68 /// [`CELError`] for use from async or other threads.
69 ///
70 /// # Examples
71 ///
72 /// ```rust
73 /// use proc_macro2::Span;
74 /// use cel_parser::SourceSpan;
75 ///
76 /// let span = SourceSpan::from_proc_macro2(Span::call_site());
77 /// assert_eq!(span.start.line, span.end.line);
78 /// ```
79 pub fn from_proc_macro2(span: proc_macro2::Span) -> Self {
80 SourceSpan {
81 start: span.start(),
82 end: span.end(),
83 }
84 }
85
86 /// Builds a span from two `proc_macro2::Span` values (start of `start`, end of `end`).
87 ///
88 /// Use this when an expression spans multiple tokens and you want a range span.
89 ///
90 /// # Examples
91 ///
92 /// ```rust
93 /// use proc_macro2::Span;
94 /// use cel_parser::SourceSpan;
95 ///
96 /// let start = Span::call_site();
97 /// let end = Span::call_site();
98 /// let span = SourceSpan::from_proc_macro2_range(start, end);
99 /// assert_eq!(span.start.line, span.end.line);
100 /// ```
101 pub fn from_proc_macro2_range(start: proc_macro2::Span, end: proc_macro2::Span) -> Self {
102 SourceSpan {
103 start: start.start(),
104 end: end.end(),
105 }
106 }
107
108 /// Converts this span (1-based lines, 0-based character columns) to a byte-offset range
109 /// within `source`.
110 ///
111 /// - Precondition: `source` is the exact original text this span's line/column positions
112 /// were recorded against.
113 /// - Precondition: `self.start.line >= 1` (spans use 1-based line numbers).
114 /// - Precondition: `self.end` does not precede `self.start` (`self.end.line > self.start.line`,
115 /// or they're equal and `self.end.column >= self.start.column`).
116 ///
117 /// - Complexity: O(n) in the length of `source`.
118 ///
119 /// # Examples
120 ///
121 /// ```rust
122 /// use cel_parser::SourceSpan;
123 ///
124 /// let span = SourceSpan::new(1, 0, 1, 5);
125 /// let range = span.to_byte_range("hello world");
126 /// assert_eq!(&"hello world"[range], "hello");
127 /// ```
128 pub fn to_byte_range(&self, source: &str) -> std::ops::Range<usize> {
129 span_to_byte_range(source, *self)
130 }
131}
132
133/// A CEL parse error with a message and source location.
134///
135/// Uses a [`SourceSpan`] (line/column only) so the error is `Send + Sync` and
136/// can be used from async execution or reported across thread boundaries.
137///
138/// # Examples
139/// See [`Self::new`] and [`Self::format_rustc_style`].
140#[derive(Clone, Debug)]
141pub struct CELError {
142 message: String,
143 span: SourceSpan,
144}
145
146/// Converts a [`SourceSpan`] (1-based lines, 0-based character columns) to a byte-offset
147/// range within `source`.
148///
149/// - Complexity: O(n) time and O(1) auxiliary space for n source bytes.
150pub(crate) fn span_to_byte_range(source: &str, span: SourceSpan) -> std::ops::Range<usize> {
151 debug_assert!(
152 span.start.line >= 1,
153 "`span.start.line` must be 1-based (≥ 1)"
154 );
155 debug_assert!(
156 span.end.line > span.start.line
157 || (span.end.line == span.start.line && span.end.column >= span.start.column),
158 "`span.end` must not precede `span.start`"
159 );
160 let start_line_byte: usize = source
161 .split_inclusive('\n')
162 .take(span.start.line - 1)
163 .map(str::len)
164 .sum();
165 let start_byte = start_line_byte
166 + source[start_line_byte..]
167 .chars()
168 .take(span.start.column)
169 .map(char::len_utf8)
170 .sum::<usize>();
171 let end_byte = if span.end.line == span.start.line {
172 start_byte
173 + source[start_byte..]
174 .chars()
175 .take(span.end.column - span.start.column)
176 .map(char::len_utf8)
177 .sum::<usize>()
178 } else {
179 let end_line_byte = start_byte
180 + source[start_byte..]
181 .split_inclusive('\n')
182 .take(span.end.line - span.start.line)
183 .map(str::len)
184 .sum::<usize>();
185 end_line_byte
186 + source[end_line_byte..]
187 .chars()
188 .take(span.end.column)
189 .map(char::len_utf8)
190 .sum::<usize>()
191 };
192 debug_assert!(
193 end_byte <= source.len(),
194 "computed end_byte exceeds source.len()"
195 );
196 start_byte..end_byte
197}
198
199/// One labelled span in a multi-span diagnostic.
200///
201/// # Examples
202/// See [`format_multi_span`].
203#[derive(Clone, Debug)]
204pub struct SpanLabel {
205 /// The source region to underline.
206 pub span: SourceSpan,
207 /// The label printed beside the caret.
208 pub label: String,
209}
210
211/// Renders `title` with several labelled annotations over `source_code`, the first as the
212/// primary caret and the rest as secondary context, in rustc style.
213///
214/// - Precondition: `labels` is non-empty.
215/// - Complexity: O(k * s + l) annotation work for k labels, s source bytes and l label bytes,
216/// plus renderer layout; O(k + s + b) auxiliary/output space for b output bytes.
217///
218/// # Examples
219///
220/// ```rust
221/// use annotate_snippets::Renderer;
222/// use cel_parser::{SourceSpan, SpanLabel, format_multi_span};
223///
224/// let labels = vec![SpanLabel { span: SourceSpan::new(1, 0, 1, 1), label: "here".into() }];
225/// let out = format_multi_span("oops", &labels, "x", "f.adm2", 1, &Renderer::plain());
226/// assert!(out.contains("oops"));
227/// ```
228pub fn format_multi_span(
229 title: &str,
230 labels: &[SpanLabel],
231 source_code: &str,
232 filename: &str,
233 start_line: u32,
234 renderer: &Renderer,
235) -> String {
236 debug_assert!(!labels.is_empty(), "`labels` must be non-empty");
237 let mut snippet = Snippet::source(source_code)
238 .path(filename)
239 .line_start(start_line as usize);
240 for (i, l) in labels.iter().enumerate() {
241 let range = span_to_byte_range(source_code, l.span);
242 let kind = if i == 0 {
243 AnnotationKind::Primary
244 } else {
245 AnnotationKind::Context
246 };
247 snippet = snippet.annotation(kind.span(range).label(l.label.as_str()));
248 }
249 let report = [Group::with_title(Level::ERROR.primary_title(title)).element(snippet)];
250 renderer.render(&report)
251}
252
253/// Returns the closing delimiter character that pairs with opening bracket `open`.
254///
255/// - Precondition: `open` is one of `(`, `[`, `{`.
256fn matching_close(open: char) -> char {
257 match open {
258 '(' => ')',
259 '[' => ']',
260 '{' => '}',
261 _ => {
262 debug_assert!(false, "`open` must be one of `(`, `[`, `{{`");
263 open
264 }
265 }
266}
267
268/// Finds the innermost bracket delimiter still open at the point where an unmatched or
269/// mismatched closing delimiter appears at `close_span` in `source`.
270///
271/// `close_span` is the first lex failure in `source`, so everything before it lexed as a
272/// self-contained run of valid tokens: literals and comments already closed, and nested
273/// bracket groups already balanced. The only way that prefix can itself fail to lex on its
274/// own is an unclosed group, and `proc_macro2` reports the position of its innermost
275/// still-open delimiter — exactly the opener `close_span`'s delimiter failed to match.
276///
277/// Returns `None` when the prefix lexes cleanly (no opener at all to match).
278///
279/// - Complexity: O(n) in `source`'s length (re-lexes the prefix before `close_span`).
280/// Prefix lexing uses O(n) auxiliary space.
281fn innermost_open_delimiter(
282 source: &str,
283 close_span: proc_macro2::Span,
284) -> Option<(char, proc_macro2::Span)> {
285 let close_start = span_to_byte_range(source, SourceSpan::from_proc_macro2(close_span)).start;
286 let prefix_err = proc_macro2::TokenStream::from_str(&source[..close_start]).err()?;
287 let open_span = prefix_err.span();
288 let open_start = span_to_byte_range(source, SourceSpan::from_proc_macro2(open_span)).start;
289 match source[open_start..].chars().next()? {
290 ch @ ('(' | '[' | '{') => Some((ch, open_span)),
291 _ => None,
292 }
293}
294
295/// Classifies a lex failure by inspecting the character at `span`'s start in `source`.
296///
297/// Returns the message plus an optional secondary label: for a closing delimiter that
298/// doesn't match (or has no) currently-open opener, the secondary points back at the
299/// unmatched opener when one exists.
300///
301/// Returns `None` when the span doesn't resolve to a character (e.g. it points past the end of
302/// `source`), so the caller can fall back to a generic message.
303///
304/// - Complexity: O(s) time and auxiliary space for s source bytes, including prefix re-lexing.
305fn lex_failure_message(
306 source: &str,
307 span: proc_macro2::Span,
308) -> Option<(String, Option<SpanLabel>)> {
309 let byte_range = span_to_byte_range(source, SourceSpan::from_proc_macro2(span));
310 let rest = &source[byte_range.start..];
311 Some(match rest.chars().next()? {
312 '(' => (
313 "unclosed delimiter `(`: expected a matching `)`".to_string(),
314 None,
315 ),
316 '[' => (
317 "unclosed delimiter `[`: expected a matching `]`".to_string(),
318 None,
319 ),
320 '{' => (
321 "unclosed delimiter `{`: expected a matching `}`".to_string(),
322 None,
323 ),
324 ch @ (')' | ']' | '}') => match innermost_open_delimiter(source, span) {
325 Some((open_ch, open_span)) => (
326 format!(
327 "mismatched closing delimiter: found `{ch}`, expected `{}`",
328 matching_close(open_ch)
329 ),
330 Some(SpanLabel {
331 span: SourceSpan::from_proc_macro2(open_span),
332 label: format!("unclosed delimiter `{open_ch}`"),
333 }),
334 ),
335 None => (format!("unexpected closing delimiter `{ch}`"), None),
336 },
337 '"' => ("unterminated string literal".to_string(), None),
338 '\'' => (
339 "invalid or unterminated character literal".to_string(),
340 None,
341 ),
342 '`' => ("invalid character `` ` ``".to_string(), None),
343 '/' if rest.starts_with("/*") => ("unterminated block comment".to_string(), None),
344 ch => (format!("invalid character `{ch}`"), None),
345 })
346}
347
348impl CELError {
349 /// Creates a new error with the given message and source span.
350 ///
351 /// - Complexity: O(m) time and space for m borrowed message bytes; transferring a `String`
352 /// is O(1). Other message types contribute their `Into<String>` conversion cost.
353 ///
354 /// # Examples
355 ///
356 /// ```rust
357 /// use cel_parser::CELError;
358 /// use cel_parser::SourceSpan;
359 ///
360 /// let span = SourceSpan::default();
361 /// let e = CELError::new("unexpected token", span);
362 /// assert_eq!(e.message(), "unexpected token");
363 /// ```
364 pub fn new(message: impl Into<String>, span: SourceSpan) -> Self {
365 CELError {
366 message: message.into(),
367 span,
368 }
369 }
370
371 /// Returns the error message.
372 ///
373 /// # Examples
374 ///
375 /// ```rust
376 /// use cel_parser::CELError;
377 /// use cel_parser::SourceSpan;
378 ///
379 /// let e = CELError::new("bad input", SourceSpan::default());
380 /// assert_eq!(e.message(), "bad input");
381 /// ```
382 pub fn message(&self) -> &str {
383 &self.message
384 }
385
386 /// Returns the source span for this error.
387 ///
388 /// # Examples
389 ///
390 /// ```rust
391 /// use cel_parser::CELError;
392 /// use cel_parser::SourceSpan;
393 ///
394 /// let span = SourceSpan::default();
395 /// let e = CELError::new("bad input", span);
396 /// assert_eq!(e.span(), span);
397 /// ```
398 pub fn span(&self) -> SourceSpan {
399 self.span
400 }
401
402 /// Formats this error in rustc diagnostic style with source context.
403 ///
404 /// Produces a multi-line string matching Rust compiler diagnostic output,
405 /// including the source file location, error message, and a caret indicating
406 /// the error position. Uses [annotate-snippets](https://docs.rs/annotate-snippets)
407 /// for rendering.
408 ///
409 /// Pass [`Renderer::plain`] for tests and non-ANSI contexts; pass
410 /// [`Renderer::styled`] for terminal output.
411 ///
412 /// - Complexity: O(s) source traversal for s source bytes, plus renderer layout;
413 /// rendering uses O(s + b) auxiliary/output space for b output bytes.
414 ///
415 /// # Examples
416 ///
417 /// ```rust
418 /// use annotate_snippets::Renderer;
419 /// use cel_parser::CELParser;
420 /// use cel_parser::op_table::OpLookup;
421 /// use cel_parser::CELError;
422 ///
423 /// let line = line!() + 1;
424 /// let source = r#"
425 /// 10 + 20 30
426 /// "#;
427 /// let mut parser = CELParser::new(OpLookup::new());
428 /// if let Err(e) = parser.parse_str(source) {
429 /// let e: CELError = e.into();
430 /// println!("{}", e.format_rustc_style(source, file!(), line, &Renderer::styled()));
431 /// }
432 /// ```
433 pub fn format_rustc_style(
434 &self,
435 source_code: &str,
436 filename: &str,
437 start_line: u32,
438 renderer: &Renderer,
439 ) -> String {
440 let byte_range = span_to_byte_range(source_code, self.span);
441 let report = [
442 Group::with_title(Level::ERROR.primary_title(self.message.as_str())).element(
443 Snippet::source(source_code)
444 .path(filename)
445 .line_start(start_line as usize)
446 .annotation(AnnotationKind::Primary.span(byte_range)),
447 ),
448 ];
449 renderer.render(&report)
450 }
451}
452
453impl std::fmt::Display for CELError {
454 /// Writes m message bytes in O(m) time without allocating an intermediate string.
455 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
456 f.write_str(&self.message)
457 }
458}
459
460impl std::error::Error for CELError {}
461
462/// A parse error carrying the original `proc_macro2::Span` of the offending token or
463/// expression start, plus an optional end span for range errors.
464///
465/// Used as the return type of all parser methods. Not `Send + Sync` because
466/// `proc_macro2::Span` wraps a compiler-internal handle that is only valid on
467/// the proc-macro thread. Convert to [`CELError`] via `From` when the error
468/// must cross thread boundaries or be stored for async reporting.
469///
470/// # Examples
471/// See [`Self::new`], [`Self::new_range`] and [`Self::with_cause`].
472#[derive(Clone, Debug)]
473pub struct ParseError {
474 message: String,
475 span: proc_macro2::Span,
476 end_span: Option<proc_macro2::Span>,
477 secondary: Vec<SpanLabel>,
478 cause: Option<std::rc::Rc<dyn std::error::Error>>,
479}
480
481impl ParseError {
482 /// Retains an immutable frontend cause without replacing its concrete error type.
483 ///
484 /// Cloning the parse error shares this cause. Conversion to [`CELError`] retains
485 /// only the message and primary span for thread-safe reporting.
486 ///
487 /// # Examples
488 /// ```
489 /// use std::error::Error;
490 /// let error = cel_parser::ParseError::new("frontend", proc_macro2::Span::call_site())
491 /// .with_cause(std::io::Error::other("original"));
492 /// assert!(error.source().unwrap().is::<std::io::Error>());
493 /// ```
494 pub fn with_cause(mut self, cause: impl std::error::Error + 'static) -> Self {
495 self.cause = Some(std::rc::Rc::new(cause));
496 self
497 }
498
499 /// Creates a new parse error with the given message and token span.
500 ///
501 /// - Complexity: O(m) time and space for m borrowed message bytes; transferring a `String`
502 /// is O(1). Other message types contribute their `Into<String>` conversion cost.
503 ///
504 /// # Examples
505 ///
506 /// ```rust
507 /// use proc_macro2::Span;
508 /// use cel_parser::ParseError;
509 ///
510 /// let e = ParseError::new("unexpected token", Span::call_site());
511 /// assert_eq!(e.message(), "unexpected token");
512 /// ```
513 pub fn new(message: impl Into<String>, span: proc_macro2::Span) -> Self {
514 ParseError {
515 message: message.into(),
516 span,
517 end_span: None,
518 secondary: Vec::new(),
519 cause: None,
520 }
521 }
522
523 /// Creates a parse error spanning a sub-expression from `start` to `end`.
524 ///
525 /// Use this for op-lookup failures where the error implicates a full
526 /// sub-expression rather than a single token. At runtime the two spans
527 /// are merged into a contiguous underline; at compile time two separate
528 /// `compile_error!()` diagnostics are emitted.
529 ///
530 /// - Complexity: Matches the message conversion in [`Self::new`].
531 ///
532 /// # Examples
533 ///
534 /// ```rust
535 /// use proc_macro2::Span;
536 /// use cel_parser::ParseError;
537 ///
538 /// let start = Span::call_site();
539 /// let end = Span::call_site();
540 /// let e = ParseError::new_range("type mismatch", start, end);
541 /// assert_eq!(e.message(), "type mismatch");
542 /// assert!(e.end_span().is_some());
543 /// ```
544 pub fn new_range(
545 message: impl Into<String>,
546 start: proc_macro2::Span,
547 end: proc_macro2::Span,
548 ) -> Self {
549 ParseError {
550 message: message.into(),
551 span: start,
552 end_span: Some(end),
553 secondary: Vec::new(),
554 cause: None,
555 }
556 }
557
558 /// Returns the error message.
559 ///
560 /// # Examples
561 /// See [`Self::new`].
562 pub fn message(&self) -> &str {
563 &self.message
564 }
565
566 /// Returns the `proc_macro2::Span` of the offending token.
567 ///
568 /// Use this span with `quote_spanned!` in proc-macro code to attach the
569 /// `compile_error!` to the exact source location.
570 ///
571 /// # Examples
572 /// ```
573 /// use cel_parser::ParseError;
574 /// let error = ParseError::new("bad name", proc_macro2::Span::call_site());
575 /// assert_eq!(error.span().start().column, 0);
576 /// ```
577 pub fn span(&self) -> proc_macro2::Span {
578 self.span
579 }
580
581 /// Returns the end span of this error, or `None` for single-point errors.
582 ///
583 /// `Some` for errors created with [`new_range`](Self::new_range);
584 /// `None` for errors created with [`new`](Self::new).
585 ///
586 /// # Examples
587 /// See [`Self::new_range`].
588 pub fn end_span(&self) -> Option<proc_macro2::Span> {
589 self.end_span
590 }
591
592 /// Attaches secondary labelled spans, rendered as extra carets alongside the primary.
593 ///
594 /// - Complexity: O(k) to release k previously attached labels; transfers the new vector.
595 ///
596 /// # Examples
597 ///
598 /// ```rust
599 /// use proc_macro2::Span;
600 /// use cel_parser::{ParseError, SourceSpan, SpanLabel};
601 ///
602 /// let e = ParseError::new("bad", Span::call_site()).with_secondary(vec![SpanLabel {
603 /// span: SourceSpan::new(1, 4, 1, 7),
604 /// label: "also here".into(),
605 /// }]);
606 /// assert_eq!(e.message(), "bad");
607 /// ```
608 pub fn with_secondary(mut self, secondary: Vec<SpanLabel>) -> Self {
609 self.secondary = secondary;
610 self
611 }
612
613 /// Returns the secondary labelled spans attached to this error.
614 ///
615 /// # Examples
616 ///
617 /// ```rust
618 /// use cel_parser::{ParseError, SourceSpan, SpanLabel};
619 /// use proc_macro2::Span;
620 ///
621 /// let e = ParseError::new("bad", Span::call_site()).with_secondary(vec![SpanLabel {
622 /// span: SourceSpan::new(1, 0, 1, 3),
623 /// label: "related".into(),
624 /// }]);
625 /// assert_eq!(e.secondary().len(), 1);
626 /// ```
627 pub fn secondary(&self) -> &[SpanLabel] {
628 &self.secondary
629 }
630
631 /// Converts a lex failure (e.g. from `proc_macro2::TokenStream::from_str`) into a
632 /// `ParseError` with a message describing the likely cause.
633 ///
634 /// `proc_macro2::LexError`'s `Display` is the same generic string ("cannot parse string
635 /// into token stream") regardless of cause, but its span reliably points at the offending
636 /// character. This inspects that character in `source` to report *why* lexing failed —
637 /// unclosed/unexpected delimiter, unterminated string or character literal, unterminated
638 /// block comment, or another invalid character — falling back to `err`'s own message only
639 /// when the span doesn't resolve to a character in `source` (e.g. it points past the end).
640 ///
641 /// - Complexity: O(s) inspection for s source bytes, plus lexer error display when needed;
642 /// O(s) auxiliary/output space in delimiter classification.
643 ///
644 /// # Examples
645 ///
646 /// ```rust
647 /// use cel_parser::ParseError;
648 /// use proc_macro2::TokenStream;
649 /// use std::str::FromStr;
650 ///
651 /// let source = "(1 + 2";
652 /// let lex_err = TokenStream::from_str(source).unwrap_err();
653 /// let err = ParseError::from_lex_error(source, lex_err);
654 /// assert_eq!(err.message(), "unclosed delimiter `(`: expected a matching `)`");
655 /// ```
656 pub fn from_lex_error(source: &str, err: proc_macro2::LexError) -> Self {
657 let span = err.span();
658 match lex_failure_message(source, span) {
659 Some((message, Some(secondary))) => {
660 ParseError::new(message, span).with_secondary(vec![secondary])
661 }
662 Some((message, None)) => ParseError::new(message, span),
663 None => ParseError::new(err.to_string(), span),
664 }
665 }
666
667 /// Formats this error in rustc diagnostic style with source context.
668 ///
669 /// Identical contract to [`CELError::format_rustc_style`] when no secondary spans are
670 /// attached; prefer calling this directly on a `ParseError` rather than converting to
671 /// `CELError` first when you have the source text at hand. When secondaries have been
672 /// added via [`with_secondary`](Self::with_secondary), renders a multi-span diagnostic
673 /// (the primary span plus one context caret per secondary) via [`format_multi_span`].
674 ///
675 /// - Complexity: O(k * s + l) annotation work for k total labels, s source bytes and
676 /// l secondary-label bytes, plus renderer layout; O(k + l + s + b) auxiliary/output
677 /// space for b output bytes.
678 ///
679 /// # Examples
680 ///
681 /// ```rust
682 /// use annotate_snippets::Renderer;
683 /// use cel_parser::CELParser;
684 /// use cel_parser::OpLookup;
685 ///
686 /// let line = line!() + 1;
687 /// let source = "10 + 20 30";
688 /// let mut parser = CELParser::new(OpLookup::new());
689 /// if let Err(e) = parser.parse_str(source) {
690 /// println!("{}", e.format_rustc_style(source, file!(), line, &Renderer::plain()));
691 /// }
692 /// ```
693 pub fn format_rustc_style(
694 &self,
695 source_code: &str,
696 filename: &str,
697 start_line: u32,
698 renderer: &Renderer,
699 ) -> String {
700 let source_span = SourceSpan {
701 start: self.span.start(),
702 end: self.end_span.unwrap_or(self.span).end(),
703 };
704 if !self.secondary.is_empty() {
705 let mut labels = vec![SpanLabel {
706 span: source_span,
707 label: String::new(),
708 }];
709 labels.extend(self.secondary.iter().cloned());
710 return format_multi_span(
711 &self.message,
712 &labels,
713 source_code,
714 filename,
715 start_line,
716 renderer,
717 );
718 }
719 let byte_range = span_to_byte_range(source_code, source_span);
720 let report = [
721 Group::with_title(Level::ERROR.primary_title(self.message.as_str())).element(
722 Snippet::source(source_code)
723 .path(filename)
724 .line_start(start_line as usize)
725 .annotation(AnnotationKind::Primary.span(byte_range)),
726 ),
727 ];
728 renderer.render(&report)
729 }
730}
731
732/// Converts a [`ParseError`] to a [`CELError`] by extracting the
733/// [`SourceSpan`] from the token span.
734///
735/// Use this at async/runtime boundaries where `Send + Sync` is required.
736impl From<ParseError> for CELError {
737 /// Transfers the message and complete primary range into a thread-safe error.
738 ///
739 /// - Complexity: O(k) to release k secondary labels, plus releasing the retained cause.
740 fn from(e: ParseError) -> Self {
741 let source_span = SourceSpan {
742 start: e.span.start(),
743 end: e.end_span.unwrap_or(e.span).end(),
744 };
745 CELError::new(e.message, source_span)
746 }
747}
748
749impl std::fmt::Display for ParseError {
750 /// Writes m message bytes in O(m) time without allocating an intermediate string.
751 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
752 f.write_str(&self.message)
753 }
754}
755
756impl std::error::Error for ParseError {
757 /// Borrows the retained original frontend cause, if present.
758 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
759 self.cause.as_deref()
760 }
761}
762
763/// A runtime error context carrying the source span of the failing operation.
764///
765/// Add this as anyhow context with `.context(SpanContext::new(span))` when wrapping
766/// an op closure. Retrieve it from an `anyhow::Error` with `e.downcast_ref::<SpanContext>()`.
767///
768/// # Examples
769/// See [`Self::new`] and [`FormatRustcStyle::format_rustc_style`].
770pub struct SpanContext {
771 span: SourceSpan,
772}
773
774impl SpanContext {
775 /// Creates a new span context for the given source region.
776 ///
777 /// # Examples
778 ///
779 /// ```rust
780 /// use cel_parser::{SourceSpan, SpanContext};
781 ///
782 /// let span = SourceSpan::new(1, 0, 1, 5);
783 /// let ctx = SpanContext::new(span);
784 /// assert_eq!(ctx.span(), span);
785 /// ```
786 pub fn new(span: SourceSpan) -> Self {
787 SpanContext { span }
788 }
789
790 /// Returns the source span.
791 ///
792 /// # Examples
793 ///
794 /// ```rust
795 /// use cel_parser::{SourceSpan, SpanContext};
796 ///
797 /// let span = SourceSpan::new(2, 3, 2, 7);
798 /// let ctx = SpanContext::new(span);
799 /// assert_eq!(ctx.span(), span);
800 /// ```
801 pub fn span(&self) -> SourceSpan {
802 self.span
803 }
804
805 /// Formats a runtime error message with rustc-style source annotation.
806 ///
807 /// Delegates to `CELError::format_rustc_style` using `self.span` and `message`.
808 ///
809 /// - Complexity: O(s + m) source/message work for s source bytes and m message bytes,
810 /// plus renderer layout; O(s + m + b) auxiliary/output space for b output bytes.
811 ///
812 /// # Examples
813 ///
814 /// ```rust
815 /// use annotate_snippets::Renderer;
816 /// use cel_parser::{SourceSpan, SpanContext};
817 ///
818 /// let source = "1i32 + 2i32";
819 /// let span = SourceSpan::new(1, 5, 1, 6);
820 /// let ctx = SpanContext::new(span);
821 /// let output = ctx.format_rustc_style("arithmetic overflow", source, "test.cel", 1, &Renderer::plain());
822 /// assert!(output.contains("arithmetic overflow"));
823 /// ```
824 pub fn format_rustc_style(
825 &self,
826 message: &str,
827 source_code: &str,
828 filename: &str,
829 start_line: u32,
830 renderer: &Renderer,
831 ) -> String {
832 CELError::new(message, self.span).format_rustc_style(
833 source_code,
834 filename,
835 start_line,
836 renderer,
837 )
838 }
839}
840
841impl std::fmt::Display for SpanContext {
842 /// Writes the start and end line/column coordinates.
843 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
844 write!(
845 f,
846 "at {}:{}-{}:{}",
847 self.span.start.line, self.span.start.column, self.span.end.line, self.span.end.column
848 )
849 }
850}
851
852impl std::fmt::Debug for SpanContext {
853 /// Writes the context's source region as a named debug field.
854 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
855 f.debug_struct("SpanContext")
856 .field("span", &self.span)
857 .finish()
858 }
859}
860
861impl std::error::Error for SpanContext {}
862
863/// Extension trait that adds rustc-style formatting to `anyhow::Error`.
864///
865/// Import this trait to call `.format_rustc_style(...)` directly on an `anyhow::Error`.
866/// If the error carries a [`SpanContext`] (added during op execution with the
867/// `span-diagnostics` feature), the output includes a source-location annotation.
868/// Otherwise it falls back to `self.to_string()`.
869///
870/// # Examples
871///
872/// ```rust
873/// use annotate_snippets::Renderer;
874/// use cel_parser::FormatRustcStyle;
875///
876/// let err = anyhow::anyhow!("something went wrong");
877/// let output = err.format_rustc_style("1 + 2", "example.cel", 1, &Renderer::plain());
878/// assert_eq!(output, "something went wrong");
879/// ```
880pub trait FormatRustcStyle {
881 /// Formats in rustc diagnostic style.
882 ///
883 /// If the error carries a [`SpanContext`], produces a multi-line caret diagnostic.
884 /// Uses the root failure as the diagnostic title, regardless of textual context wrappers.
885 /// Otherwise returns `self.to_string()`.
886 ///
887 /// - Complexity: O(c + s) traversal work for c error-chain links and s source bytes,
888 /// plus error-display and renderer-layout costs; O(c + s + b) auxiliary/output
889 /// space for b output bytes. Unlocated errors do not inspect the source.
890 ///
891 /// # Examples
892 /// ```
893 /// use annotate_snippets::Renderer;
894 /// use cel_parser::{FormatRustcStyle, SourceSpan, SpanContext};
895 /// let error = anyhow::anyhow!("division by zero")
896 /// .context(SpanContext::new(SourceSpan::new(1, 0, 1, 1)));
897 /// let text = error.format_rustc_style("1", "input.cel", 1, &Renderer::plain());
898 /// assert!(text.contains("error: division by zero"));
899 /// assert!(text.contains("input.cel:1:1"));
900 /// ```
901 /// See the [`FormatRustcStyle`] example for the unlocated fallback.
902 fn format_rustc_style(
903 &self,
904 source_code: &str,
905 filename: &str,
906 start_line: u32,
907 renderer: &Renderer,
908 ) -> String;
909}
910
911impl FormatRustcStyle for anyhow::Error {
912 /// Renders the located root failure, or the outer display text when no location exists.
913 ///
914 /// - Complexity: Matches [`FormatRustcStyle::format_rustc_style`].
915 fn format_rustc_style(
916 &self,
917 source_code: &str,
918 filename: &str,
919 start_line: u32,
920 renderer: &Renderer,
921 ) -> String {
922 if let Some(ctx) = self.downcast_ref::<SpanContext>() {
923 let message = self.root_cause().to_string();
924 ctx.format_rustc_style(&message, source_code, filename, start_line, renderer)
925 } else {
926 self.to_string()
927 }
928 }
929}
930
931#[cfg(test)]
932mod tests {
933 /// Retains an immutable concrete cause through cloning and error-source traversal.
934 #[test]
935 fn parse_error_retains_owning_cause() {
936 use std::error::Error;
937 let error = ParseError::new("construction failed", proc_macro2::Span::call_site())
938 .with_cause(std::io::Error::other("original"));
939 let copy = error.clone();
940 assert_eq!(
941 copy.source()
942 .unwrap()
943 .downcast_ref::<std::io::Error>()
944 .unwrap()
945 .to_string(),
946 "original"
947 );
948 assert!(
949 ParseError::new("syntax", proc_macro2::Span::call_site())
950 .source()
951 .is_none()
952 );
953 }
954
955 use super::*;
956 use annotate_snippets::Renderer;
957 use proc_macro2::Span;
958
959 #[test]
960 fn parse_error_message() {
961 let e = ParseError::new("bad token", Span::call_site());
962 assert_eq!(e.message(), "bad token");
963 }
964
965 #[test]
966 fn parse_error_display() {
967 let e = ParseError::new("bad token", Span::call_site());
968 assert_eq!(e.to_string(), "bad token");
969 }
970
971 #[test]
972 fn parse_error_into_cel_error() {
973 let e = ParseError::new("bad token", Span::call_site());
974 let cel: CELError = e.into();
975 assert_eq!(cel.message(), "bad token");
976 assert_eq!(cel.span(), SourceSpan::from_proc_macro2(Span::call_site()));
977 }
978
979 #[test]
980 fn parse_error_format_rustc_style() {
981 let source = "10 + 20 30";
982 let e = ParseError::new("unexpected token", Span::call_site());
983 let formatted = e.format_rustc_style(source, "test.cel", 1, &Renderer::plain());
984 assert!(formatted.contains("error: unexpected token"));
985 assert!(formatted.contains("test.cel"));
986 }
987
988 #[test]
989 fn parse_error_new_range_has_end_span() {
990 let start = Span::call_site();
991 let end = Span::call_site();
992 let e = ParseError::new_range("type mismatch", start, end);
993 assert_eq!(e.message(), "type mismatch");
994 assert!(e.end_span().is_some());
995 }
996
997 #[test]
998 fn from_lex_error_unclosed_paren_names_expected_close() {
999 let source = "(1 + 2";
1000 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
1001 let e = ParseError::from_lex_error(source, lex_err);
1002 assert_eq!(
1003 e.message(),
1004 "unclosed delimiter `(`: expected a matching `)`"
1005 );
1006 }
1007
1008 #[test]
1009 fn from_lex_error_unclosed_bracket_names_expected_close() {
1010 let source = "[1, 2";
1011 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
1012 let e = ParseError::from_lex_error(source, lex_err);
1013 assert_eq!(
1014 e.message(),
1015 "unclosed delimiter `[`: expected a matching `]`"
1016 );
1017 }
1018
1019 #[test]
1020 fn from_lex_error_unclosed_brace_names_expected_close() {
1021 let source = "{1: 2";
1022 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
1023 let e = ParseError::from_lex_error(source, lex_err);
1024 assert_eq!(
1025 e.message(),
1026 "unclosed delimiter `{`: expected a matching `}`"
1027 );
1028 }
1029
1030 #[test]
1031 fn from_lex_error_unexpected_closing_delimiter() {
1032 let source = "1 + 2)";
1033 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
1034 let e = ParseError::from_lex_error(source, lex_err);
1035 assert_eq!(e.message(), "unexpected closing delimiter `)`");
1036 }
1037
1038 #[test]
1039 fn from_lex_error_mismatched_delimiter_names_found_and_expected() {
1040 let source = "(1 + 2]";
1041 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
1042 let e = ParseError::from_lex_error(source, lex_err);
1043 assert_eq!(
1044 e.message(),
1045 "mismatched closing delimiter: found `]`, expected `)`"
1046 );
1047 let out = e.format_rustc_style(source, "t.cel", 1, &Renderer::plain());
1048 assert!(
1049 out.contains("unclosed delimiter `(`"),
1050 "expected a secondary annotation on the unclosed `(`:\n{out}"
1051 );
1052 }
1053
1054 #[test]
1055 fn from_lex_error_mismatched_delimiter_issue_77_example() {
1056 // From issue #77: the real problem is the unclosed `(`, not the `}` the lex error
1057 // itself points at.
1058 let source = "{a: (1 + 2}";
1059 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
1060 let e = ParseError::from_lex_error(source, lex_err);
1061 assert_eq!(
1062 e.message(),
1063 "mismatched closing delimiter: found `}`, expected `)`"
1064 );
1065 let out = e.format_rustc_style(source, "t.cel", 1, &Renderer::plain());
1066 assert!(
1067 out.contains("unclosed delimiter `(`"),
1068 "expected a secondary annotation on the unclosed `(`:\n{out}"
1069 );
1070 }
1071
1072 #[test]
1073 fn from_lex_error_mismatched_delimiter_reports_innermost_opener() {
1074 // Innermost open delimiter is `{`, not the outer `[` or `(`.
1075 let source = "([{1)]}";
1076 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
1077 let e = ParseError::from_lex_error(source, lex_err);
1078 assert_eq!(
1079 e.message(),
1080 "mismatched closing delimiter: found `)`, expected `}`"
1081 );
1082 let out = e.format_rustc_style(source, "t.cel", 1, &Renderer::plain());
1083 assert!(
1084 out.contains("unclosed delimiter `{`"),
1085 "expected a secondary annotation on the unclosed `{{`:\n{out}"
1086 );
1087 }
1088
1089 #[test]
1090 fn from_lex_error_unterminated_string_literal() {
1091 let source = "1 + \"abc";
1092 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
1093 let e = ParseError::from_lex_error(source, lex_err);
1094 assert_eq!(e.message(), "unterminated string literal");
1095 }
1096
1097 #[test]
1098 fn from_lex_error_unterminated_block_comment() {
1099 let source = "/* unterminated";
1100 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
1101 let e = ParseError::from_lex_error(source, lex_err);
1102 assert_eq!(e.message(), "unterminated block comment");
1103 }
1104
1105 #[test]
1106 fn from_lex_error_invalid_character_names_the_character() {
1107 let source = "1 + `";
1108 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
1109 let e = ParseError::from_lex_error(source, lex_err);
1110 assert_eq!(e.message(), "invalid character `` ` ``");
1111 }
1112
1113 #[test]
1114 fn from_lex_error_preserves_lex_error_span() {
1115 let source = "(1 + 2";
1116 let lex_err = source.parse::<proc_macro2::TokenStream>().unwrap_err();
1117 let expected_span = SourceSpan::from_proc_macro2(lex_err.span());
1118 let e = ParseError::from_lex_error(source, lex_err);
1119 assert_eq!(SourceSpan::from_proc_macro2(e.span()), expected_span);
1120 }
1121
1122 #[test]
1123 fn parse_error_new_range_cel_error_merges_spans() {
1124 let ts: proc_macro2::TokenStream = r#""Hello" 32.0"#.parse().unwrap();
1125 let mut iter = ts.into_iter();
1126 let start_tok = iter.next().unwrap();
1127 let end_tok = iter.next().unwrap();
1128 let start = start_tok.span();
1129 let end = end_tok.span();
1130
1131 let e = ParseError::new_range("type mismatch", start, end);
1132 let cel: CELError = e.into();
1133
1134 assert_eq!(cel.message(), "type mismatch");
1135 assert_eq!(
1136 cel.span().start,
1137 start.start(),
1138 "CELError span should start at expression start"
1139 );
1140 assert_eq!(
1141 cel.span().end,
1142 end.end(),
1143 "CELError span should end at expression end"
1144 );
1145 }
1146
1147 #[test]
1148 fn parse_error_new_has_no_end_span() {
1149 let e = ParseError::new("bad token", Span::call_site());
1150 assert!(e.end_span().is_none());
1151 }
1152
1153 #[test]
1154 fn span_to_byte_range_basic() {
1155 let source = "hello world";
1156 assert_eq!(
1157 span_to_byte_range(source, SourceSpan::new(1, 6, 1, 11)),
1158 6..11
1159 );
1160 }
1161
1162 #[test]
1163 fn span_to_byte_range_empty_span() {
1164 let source = "hello";
1165 assert_eq!(
1166 span_to_byte_range(source, SourceSpan::new(1, 2, 1, 2)),
1167 2..2
1168 );
1169 }
1170
1171 #[test]
1172 fn span_to_byte_range_start_past_end_of_source() {
1173 let source = "hi";
1174 let r = span_to_byte_range(source, SourceSpan::new(5, 0, 5, 3));
1175 assert_eq!(r, source.len()..source.len());
1176 }
1177
1178 #[test]
1179 fn span_to_byte_range_end_past_end_of_source() {
1180 let source = "hi";
1181 let r = span_to_byte_range(source, SourceSpan::new(1, 0, 1, 100));
1182 assert!(r.start <= r.end && r.end <= source.len());
1183 }
1184
1185 #[test]
1186 fn span_context_display_shows_span_location() {
1187 let span = SourceSpan::new(1, 0, 1, 5);
1188 let ctx = SpanContext::new(span);
1189 let s = ctx.to_string();
1190 assert_eq!(s, "at 1:0-1:5");
1191 }
1192
1193 #[test]
1194 fn span_context_span_roundtrip() {
1195 let span = SourceSpan::new(2, 3, 2, 7);
1196 let ctx = SpanContext::new(span);
1197 assert_eq!(ctx.span(), span);
1198 }
1199
1200 #[test]
1201 fn span_context_format_rustc_style_contains_message_and_location() {
1202 let source = "1i32 + 2i32";
1203 let span = SourceSpan::new(1, 5, 1, 6);
1204 let ctx = SpanContext::new(span);
1205 let output = ctx.format_rustc_style(
1206 "arithmetic overflow",
1207 source,
1208 "test.cel",
1209 1,
1210 &Renderer::plain(),
1211 );
1212 assert!(
1213 output.contains("arithmetic overflow"),
1214 "expected message in output:\n{output}"
1215 );
1216 assert!(
1217 output.contains("test.cel"),
1218 "expected filename in output:\n{output}"
1219 );
1220 }
1221
1222 #[test]
1223 fn format_rustc_style_with_span_context_uses_span() {
1224 let source = "1i32 + 2i32";
1225 let span = SourceSpan::new(1, 5, 1, 6);
1226 let inner = anyhow::anyhow!("arithmetic overflow");
1227 let wrapped = inner.context(SpanContext::new(span));
1228 let output = FormatRustcStyle::format_rustc_style(
1229 &wrapped,
1230 source,
1231 "test.cel",
1232 1,
1233 &Renderer::plain(),
1234 );
1235 assert!(
1236 output.contains("arithmetic overflow"),
1237 "expected message:\n{output}"
1238 );
1239 assert!(output.contains("test.cel"), "expected filename:\n{output}");
1240 }
1241
1242 #[test]
1243 fn format_rustc_style_without_span_context_falls_back_to_to_string() {
1244 let err = anyhow::anyhow!("something went wrong");
1245 let output = FormatRustcStyle::format_rustc_style(
1246 &err,
1247 "unused source",
1248 "unused.cel",
1249 1,
1250 &Renderer::plain(),
1251 );
1252 assert_eq!(output, "something went wrong");
1253 }
1254
1255 #[test]
1256 fn parse_error_with_secondary_renders_all_spans() {
1257 let source = "aaa bbb";
1258 let e = ParseError::new_range("bad", Span::call_site(), Span::call_site()).with_secondary(
1259 vec![SpanLabel {
1260 span: SourceSpan::new(1, 4, 1, 7),
1261 label: "also here".into(),
1262 }],
1263 );
1264 // primary span is call_site (line 1 col 0..0); secondary underlines "bbb".
1265 let out = e.format_rustc_style(source, "t.cel", 1, &Renderer::plain());
1266 assert!(out.contains("bad"), "{out}");
1267 assert!(out.contains("also here"), "{out}");
1268 }
1269
1270 #[test]
1271 fn parse_error_without_secondary_renders_single_span_as_before() {
1272 let e = ParseError::new("bad", Span::call_site());
1273 let out = e.format_rustc_style("10 + 20 30", "t.cel", 1, &Renderer::plain());
1274 assert!(out.contains("error: bad"), "{out}");
1275 }
1276
1277 #[test]
1278 fn format_multi_span_underlines_every_span_and_prints_labels() {
1279 let source = "aaa bbb ccc";
1280 let labels = vec![
1281 SpanLabel {
1282 span: SourceSpan::new(1, 0, 1, 3),
1283 label: "first".into(),
1284 },
1285 SpanLabel {
1286 span: SourceSpan::new(1, 8, 1, 11),
1287 label: "third".into(),
1288 },
1289 ];
1290 let out = format_multi_span("mismatch", &labels, source, "t.adm2", 1, &Renderer::plain());
1291 assert!(out.contains("mismatch"), "{out}");
1292 assert!(out.contains("first"), "{out}");
1293 assert!(out.contains("third"), "{out}");
1294 assert!(out.contains("| ^^^ --- third"), "{out}");
1295 assert!(out.contains("| first"), "{out}");
1296 assert!(!out.contains('\u{1b}'), "plain renderer has no ANSI: {out}");
1297 }
1298
1299 #[test]
1300 fn to_byte_range_matches_manual_slice() {
1301 let source = "line one\nline two\nline three";
1302 let span = SourceSpan::new(2, 5, 3, 4);
1303 let range = span.to_byte_range(source);
1304 assert_eq!(&source[range], "two\nline");
1305 }
1306}