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///
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}