Skip to main content

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