yash_syntax/source/
pretty.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2021 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Pretty-printing diagnostic messages containing references to source code
18//!
19//! This module defines some data types for constructing intermediate data
20//! structures for printing diagnostic messages referencing source code
21//! fragments.  When you have an [`Error`](crate::parser::Error), you can
22//! convert it to a [`Report`]. Then, you can in turn convert it into
23//! `annotate_snippets::Snippet`, for example, and finally format a printable
24//! diagnostic message string.
25//!
26//! When the `yash_syntax` crate is built with the `annotate-snippets` feature
27//! enabled, it supports conversion from [`Report`] to
28//! [`Group`](annotate_snippets::Group). If you would like to use another
29//! formatter instead, you can provide your own conversion for yourself.
30//!
31//! ## Printing an error
32//!
33//! This example shows how to format an [`Error`](crate::parser::Error) instance
34//! into a human-readable string.
35//!
36//! ```
37//! # use yash_syntax::parser::{Error, ErrorCause, SyntaxError};
38//! # use yash_syntax::source::Location;
39//! # use yash_syntax::source::pretty::Report;
40//! let error = Error {
41//!     cause: ErrorCause::Syntax(SyntaxError::EmptyParam),
42//!     location: Location::dummy(""),
43//! };
44//! let report = Report::from(&error);
45//! // The lines below require the `annotate-snippets` feature.
46//! # #[cfg(feature = "annotate-snippets")]
47//! # {
48//! let group = annotate_snippets::Group::from(&report);
49//! eprintln!("{}", annotate_snippets::Renderer::plain().render(&[group]));
50//! # }
51//! ```
52//!
53//! You can also implement conversion from your custom error object to a
54//! [`Report`], which then can be used in the same way to format a diagnostic
55//! message. To do this, implement `From<YourError>` or `From<&YourError>` for
56//! `Report`.
57//!
58//! ## Deprecated items
59//!
60//! Before [`Report`] was introduced, this module provided [`Message`] as the
61//! main data structure for constructing a diagnostic message. `Message` could
62//! be constructed directly or through the [`MessageBase`] trait, which
63//! provided a blanket implementation of `From<&T> for Message` for
64//! implementors of the trait. These items are now deprecated in favor of
65//! `Report`, but are available for backward compatibility for now. Please
66//! migrate to `Report` before they are removed in a future release.
67
68use super::Location;
69use std::borrow::Cow;
70use std::cell::Ref;
71use std::ops::{Deref, Range};
72use std::rc::Rc;
73
74/// Type of [`Report`]
75#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
76#[non_exhaustive]
77pub enum ReportType {
78    #[default]
79    None,
80    Error,
81    Warning,
82}
83
84/// Type and label annotating a [`Span`]
85#[derive(Clone, Debug, Eq, PartialEq)]
86#[non_exhaustive]
87pub enum SpanRole<'a> {
88    /// Primary span, usually indicating the main cause of a problem
89    Primary { label: Cow<'a, str> },
90    /// Secondary span, usually indicating related information
91    Supplementary { label: Cow<'a, str> },
92    // Patch { replacement: Cow<'a, str> },
93}
94
95/// Part of source code [`Snippet`] annotated with additional information
96#[derive(Clone, Debug, Eq, PartialEq)]
97pub struct Span<'a> {
98    /// Range of bytes in the source code
99    pub range: Range<usize>,
100    /// Type and label of this span
101    pub role: SpanRole<'a>,
102}
103
104/// Fragment of source code with annotated spans highlighting specific regions
105///
106/// A snippet corresponds to a single source [`Code`](super::Code). It contains
107/// zero or more [`Span`]s that annotate specific parts of the code.
108///
109/// `Snippet` holds a [`Ref`] to the string held in `self.code.value`, which
110/// provides an access to the string without making a new borrow
111/// ([`code_string`](Self::code_string)). This allows creating another
112/// message builder such as `annotate_snippets::Snippet` without the need to
113/// retain a borrow of `self.code.value`.
114#[derive(Debug)]
115pub struct Snippet<'a> {
116    /// Source code to which the spans refer
117    pub code: &'a super::Code,
118    /// Reference to the string held in `self.code.value`
119    code_string: Ref<'a, str>,
120    /// Spans describing parts of the code
121    pub spans: Vec<Span<'a>>,
122}
123
124impl Snippet<'_> {
125    /// Creates a new snippet for the given code without any spans.
126    #[must_use]
127    pub fn with_code(code: &super::Code) -> Snippet<'_> {
128        Self::with_code_and_spans(code, Vec::new())
129    }
130
131    /// Creates a new snippet for the given code with the given spans.
132    #[must_use]
133    pub fn with_code_and_spans<'a>(code: &'a super::Code, spans: Vec<Span<'a>>) -> Snippet<'a> {
134        Snippet {
135            code,
136            code_string: Ref::map(code.value.borrow(), String::as_str),
137            spans,
138        }
139    }
140
141    /// Creates a vector containing a snippet with a primary span.
142    ///
143    /// This is a convenience function for creating a vector of snippets
144    /// containing a primary span created from the given location and label.
145    /// The returned vector can be used as the `snippets` field of a
146    /// [`Report`].
147    ///
148    /// This function calls
149    /// [`Source::extend_with_context`](super::Source::extend_with_context) for
150    /// `location.code.source`, thereby adding supplementary spans describing the
151    /// context of the source code. This means that the returned vector may
152    /// contain multiple snippets or spans if the source has a related location.
153    #[must_use]
154    pub fn with_primary_span<'a>(location: &'a Location, label: Cow<'a, str>) -> Vec<Snippet<'a>> {
155        let range = location.byte_range();
156        let role = SpanRole::Primary { label };
157        let spans = vec![Span { range, role }];
158        let mut snippets = vec![Snippet::with_code_and_spans(&location.code, spans)];
159        location.code.source.extend_with_context(&mut snippets);
160        snippets
161    }
162
163    /// Returns the string held in `self.code.value`.
164    ///
165    /// This method returns a reference to the string held in `self.code.value`.
166    /// `Snippet` internally holds a `Ref` to the string, which provides an
167    /// access to the string without making a new borrow.
168    #[inline(always)]
169    #[must_use]
170    pub fn code_string(&self) -> &str {
171        &self.code_string
172    }
173}
174
175impl Clone for Snippet<'_> {
176    fn clone(&self) -> Self {
177        Snippet {
178            code: self.code,
179            code_string: Ref::clone(&self.code_string),
180            spans: self.spans.clone(),
181        }
182    }
183}
184
185impl PartialEq<Snippet<'_>> for Snippet<'_> {
186    fn eq(&self, other: &Snippet<'_>) -> bool {
187        self.code == other.code && self.spans == other.spans
188    }
189}
190
191impl Eq for Snippet<'_> {}
192
193/// Type of [`Footnote`]
194#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
195#[non_exhaustive]
196pub enum FootnoteType {
197    /// No specific type
198    #[default]
199    None,
200    /// For footnotes that provide additional information
201    Note,
202    /// For footnotes that provide suggestions
203    Suggestion,
204}
205
206/// Message without associated source code
207#[derive(Clone, Debug, Default, Eq, PartialEq)]
208pub struct Footnote<'a> {
209    /// Type of this footnote
210    pub r#type: FootnoteType,
211    /// Text of this footnote
212    pub label: Cow<'a, str>,
213}
214
215/// Entire report containing multiple snippets
216///
217/// `Report` is an intermediate data structure for constructing a diagnostic
218/// message. It contains multiple [`Snippet`]s, each of which corresponds to a
219/// specific part of the source code being analyzed.
220/// See the [module documentation](self) for more details.
221#[derive(Clone, Debug, Default, Eq, PartialEq)]
222#[non_exhaustive]
223pub struct Report<'a> {
224    /// Type of this report
225    pub r#type: ReportType,
226    /// Optional identifier of this report (e.g., error code)
227    pub id: Option<Cow<'a, str>>,
228    /// Main caption of this report
229    pub title: Cow<'a, str>,
230    /// Source code fragments annotated with additional information
231    pub snippets: Vec<Snippet<'a>>,
232    /// Additional message without associated source code
233    pub footnotes: Vec<Footnote<'a>>,
234}
235
236impl Report<'_> {
237    /// Creates a new, empty report.
238    #[inline]
239    #[must_use]
240    pub fn new() -> Self {
241        Report::default()
242    }
243}
244
245/// Type of annotation.
246#[deprecated(note = "Use `ReportType` or `FootnoteType` instead", since = "0.16.0")]
247#[derive(Clone, Copy, Debug, Eq, PartialEq)]
248pub enum AnnotationType {
249    Error,
250    Warning,
251    Info,
252    Note,
253    Help,
254}
255
256/// Source code fragment annotated with a label
257///
258/// Annotations are part of an entire [`Message`].
259#[deprecated(note = "Use `Snippet` and `Span` instead", since = "0.16.0")]
260#[derive(Clone)]
261pub struct Annotation<'a> {
262    /// Type of annotation
263    #[allow(deprecated)]
264    pub r#type: AnnotationType,
265    /// String that describes the annotated part of the source code
266    pub label: Cow<'a, str>,
267    /// Position of the annotated fragment in the source code
268    pub location: &'a Location,
269    /// Annotated code string
270    ///
271    /// This value provides an access to the string held in
272    /// `self.location.code.value`, which can only be accessed by a `Ref`.
273    pub code: Rc<dyn Deref<Target = str> + 'a>,
274}
275
276#[allow(deprecated)]
277impl std::fmt::Debug for Annotation<'_> {
278    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279        f.debug_struct("Annotation")
280            .field("type", &self.r#type)
281            .field("label", &self.label)
282            .field("location", &self.location)
283            .field("code", &&**self.code)
284            .finish()
285    }
286}
287
288#[allow(deprecated)]
289impl<'a> Annotation<'a> {
290    /// Creates a new annotation.
291    ///
292    /// This function makes a borrow of `location.code.value` and stores it in
293    /// `self.code`. If it has been mutually borrowed, this function panics.
294    pub fn new(r#type: AnnotationType, label: Cow<'a, str>, location: &'a Location) -> Self {
295        Annotation {
296            r#type,
297            label,
298            location,
299            code: Rc::new(Ref::map(location.code.value.borrow(), String::as_str)),
300        }
301    }
302}
303
304/// Additional message without associated source code
305#[deprecated(note = "Use `Footnote` instead", since = "0.16.0")]
306#[derive(Clone, Debug)]
307pub struct Footer<'a> {
308    /// Type of this footer
309    #[allow(deprecated)]
310    pub r#type: AnnotationType,
311    /// Text of this footer
312    pub label: Cow<'a, str>,
313}
314
315/// Entire diagnostic message
316#[allow(deprecated)]
317#[deprecated(note = "Use `Report` instead", since = "0.16.0")]
318#[derive(Clone, Debug)]
319pub struct Message<'a> {
320    /// Type of this message
321    pub r#type: AnnotationType,
322    /// String that communicates the most important information in this message
323    pub title: Cow<'a, str>,
324    /// References to source code fragments annotated with additional information
325    pub annotations: Vec<Annotation<'a>>,
326    /// Additional text without associated source code
327    pub footers: Vec<Footer<'a>>,
328}
329
330/// Returns a mutable reference to the snippet for the given code, creating it
331/// if necessary.
332///
333/// This is a utility function used in constructing a vector of snippets.
334///
335/// If a snippet for the given code already exists in the vector, this function
336/// returns a mutable reference to that snippet. Otherwise, it creates a new
337/// snippet with the given code and appends it to the vector, returning a
338/// mutable reference to the newly created snippet.
339pub fn snippet_for_code<'a, 'b>(
340    snippets: &'b mut Vec<Snippet<'a>>,
341    code: &'a super::Code,
342) -> &'b mut Snippet<'a> {
343    // if let Some(snippet) = snippets.iter_mut().find(|s| std::ptr::eq(s.code, code)) {
344    //     snippet
345    if let Some(i) = snippets.iter().position(|s| std::ptr::eq(s.code, code)) {
346        &mut snippets[i]
347    } else {
348        // TODO Use Vec::push_mut when stabilized
349        snippets.push(Snippet::with_code(code));
350        snippets.last_mut().unwrap()
351    }
352}
353
354/// Adds a span to the appropriate snippet in the given vector.
355///
356/// This is a utility function used in constructing a vector of snippets with
357/// annotated spans.
358///
359/// If a snippet for the given code already exists in the vector, this function
360/// adds the span to that snippet. Otherwise, it creates a new snippet with the
361/// given code and span, and appends it to the vector.
362pub fn add_span<'a>(code: &'a super::Code, span: Span<'a>, snippets: &mut Vec<Snippet<'a>>) {
363    snippet_for_code(snippets, code).spans.push(span);
364}
365
366#[test]
367fn test_add_span_with_matching_code() {
368    let code = Rc::new(super::Code {
369        value: std::cell::RefCell::new("echo hello".to_string()),
370        start_line_number: std::num::NonZero::new(1).unwrap(),
371        source: Rc::new(super::Source::CommandString),
372    });
373    let span = Span {
374        range: 5..10,
375        role: SpanRole::Primary {
376            label: "greeting".into(),
377        },
378    };
379    let mut snippets = vec![Snippet::with_code(&code)];
380
381    add_span(&code, span, &mut snippets);
382
383    assert_eq!(snippets.len(), 1);
384    assert_eq!(snippets[0].spans.len(), 1);
385    assert_eq!(snippets[0].spans[0].range, 5..10);
386    assert_eq!(
387        snippets[0].spans[0].role,
388        SpanRole::Primary {
389            label: "greeting".into()
390        }
391    );
392}
393
394#[test]
395fn test_add_span_without_matching_code() {
396    let code1 = Rc::new(super::Code {
397        value: std::cell::RefCell::new("echo hello".to_string()),
398        start_line_number: std::num::NonZero::new(1).unwrap(),
399        source: Rc::new(super::Source::CommandString),
400    });
401    let code2 = Rc::new(super::Code {
402        value: std::cell::RefCell::new("ls -l".to_string()),
403        start_line_number: std::num::NonZero::new(1).unwrap(),
404        source: Rc::new(super::Source::CommandString),
405    });
406    let span = Span {
407        range: 0..2,
408        role: SpanRole::Primary {
409            label: "list".into(),
410        },
411    };
412    let mut snippets = vec![Snippet::with_code(&code1)];
413
414    add_span(&code2, span, &mut snippets);
415
416    assert_eq!(snippets.len(), 2);
417    assert_eq!(snippets[0].code.value.borrow().as_str(), "echo hello");
418    assert_eq!(snippets[0].spans.len(), 0);
419    assert_eq!(snippets[1].code.value.borrow().as_str(), "ls -l");
420    assert_eq!(snippets[1].spans.len(), 1);
421    assert_eq!(snippets[1].spans[0].range, 0..2);
422    assert_eq!(
423        snippets[1].spans[0].role,
424        SpanRole::Primary {
425            label: "list".into()
426        }
427    );
428}
429
430impl super::Source {
431    /// Extends the given vector of snippets with spans annotating the context of this source.
432    ///
433    /// If `self` is a source that has a related location (e.g., the `original` field of
434    /// `CommandSubst`), this method adds one or more spans describing the location to the given
435    /// vector. If the `code` of the location is already present in the vector, it adds the span
436    /// to the existing snippet; otherwise, it creates a new snippet.
437    ///
438    /// If `self` does not have a related location, this method does nothing.
439    pub fn extend_with_context<'a>(&'a self, snippets: &mut Vec<Snippet<'a>>) {
440        use super::Source::*;
441        match self {
442            Unknown
443            | Stdin
444            | CommandString
445            | CommandFile { .. }
446            | VariableValue { .. }
447            | InitFile { .. }
448            | Other { .. } => (),
449
450            CommandSubst { original } => {
451                let range = original.byte_range();
452                let role = SpanRole::Supplementary {
453                    label: "command substitution appeared here".into(),
454                };
455                add_span(&original.code, Span { range, role }, snippets);
456            }
457
458            Arith { original } => {
459                let range = original.byte_range();
460                let role = SpanRole::Supplementary {
461                    label: "arithmetic expansion appeared here".into(),
462                };
463                add_span(&original.code, Span { range, role }, snippets);
464            }
465
466            Eval { original } => {
467                let range = original.byte_range();
468                let role = SpanRole::Supplementary {
469                    label: "command passed to the eval built-in here".into(),
470                };
471                add_span(&original.code, Span { range, role }, snippets);
472            }
473
474            DotScript { name, origin } => {
475                let range = origin.byte_range();
476                let role = SpanRole::Supplementary {
477                    label: format!("script `{name}` was sourced here").into(),
478                };
479                add_span(&origin.code, Span { range, role }, snippets);
480            }
481
482            Trap { origin, .. } => {
483                let range = origin.byte_range();
484                let role = SpanRole::Supplementary {
485                    label: "trap was set here".into(),
486                };
487                add_span(&origin.code, Span { range, role }, snippets);
488            }
489
490            Alias { original, alias } => {
491                // Where the alias was substituted
492                let range = original.byte_range();
493                let role = SpanRole::Supplementary {
494                    label: format!("alias `{}` was substituted here", alias.name).into(),
495                };
496                add_span(&original.code, Span { range, role }, snippets);
497                // Recurse into the source of the substituted code
498                original.code.source.extend_with_context(snippets);
499
500                // Where the alias was defined
501                let range = alias.origin.byte_range();
502                let role = SpanRole::Supplementary {
503                    label: format!("alias `{}` was defined here", alias.name).into(),
504                };
505                add_span(&alias.origin.code, Span { range, role }, snippets);
506                // Recurse into the source of the alias definition
507                alias.origin.code.source.extend_with_context(snippets);
508            }
509        }
510    }
511
512    /// Appends complementary annotations describing this source.
513    #[allow(deprecated)]
514    #[deprecated(note = "Use `extend_with_context` instead", since = "0.16.0")]
515    pub fn complement_annotations<'a, 's: 'a, T: Extend<Annotation<'a>>>(&'s self, result: &mut T) {
516        use super::Source::*;
517        match self {
518            Unknown
519            | Stdin
520            | CommandString
521            | CommandFile { .. }
522            | VariableValue { .. }
523            | InitFile { .. }
524            | Other { .. } => (),
525
526            CommandSubst { original } => {
527                // TODO Use Extend::extend_one
528                result.extend(std::iter::once(Annotation::new(
529                    AnnotationType::Info,
530                    "command substitution appeared here".into(),
531                    original,
532                )));
533            }
534            Arith { original } => {
535                // TODO Use Extend::extend_one
536                result.extend(std::iter::once(Annotation::new(
537                    AnnotationType::Info,
538                    "arithmetic expansion appeared here".into(),
539                    original,
540                )));
541            }
542            Eval { original } => {
543                // TODO Use Extend::extend_one
544                result.extend(std::iter::once(Annotation::new(
545                    AnnotationType::Info,
546                    "command passed to the eval built-in here".into(),
547                    original,
548                )));
549            }
550            DotScript { name, origin } => {
551                // TODO Use Extend::extend_one
552                result.extend(std::iter::once(Annotation::new(
553                    AnnotationType::Info,
554                    format!("script `{name}` was sourced here",).into(),
555                    origin,
556                )));
557            }
558            Trap { origin, .. } => {
559                // TODO Use Extend::extend_one
560                result.extend(std::iter::once(Annotation::new(
561                    AnnotationType::Info,
562                    "trap was set here".into(),
563                    origin,
564                )));
565            }
566            Alias { original, alias } => {
567                // TODO Use Extend::extend_one
568                result.extend(std::iter::once(Annotation::new(
569                    AnnotationType::Info,
570                    format!("alias `{}` was substituted here", alias.name).into(),
571                    original,
572                )));
573                original.code.source.complement_annotations(result);
574                result.extend(std::iter::once(Annotation::new(
575                    AnnotationType::Info,
576                    format!("alias `{}` was defined here", alias.name).into(),
577                    &alias.origin,
578                )));
579                alias.origin.code.source.complement_annotations(result);
580            }
581        }
582    }
583}
584
585/// Helper for constructing a [`Message`]
586///
587/// Thanks to the blanket implementation `impl<'a, T: MessageBase> From<&'a T>
588/// for Message<'a>`, implementors of this trait can be converted to a message
589/// for free.
590#[allow(deprecated)]
591#[deprecated(note = "Use `Report` instead", since = "0.16.0")]
592pub trait MessageBase {
593    /// Returns the type of the entire message.
594    ///
595    /// The default implementation returns `AnnotationType::Error`.
596    fn message_type(&self) -> AnnotationType {
597        AnnotationType::Error
598    }
599
600    // TODO message tag
601
602    /// Returns the main caption of the message.
603    fn message_title(&self) -> Cow<'_, str>;
604
605    /// Returns an annotation to be the first in the message.
606    fn main_annotation(&self) -> Annotation<'_>;
607
608    /// Adds additional annotations to the given container.
609    ///
610    /// The default implementation does nothing.
611    fn additional_annotations<'a, T: Extend<Annotation<'a>>>(&'a self, results: &mut T) {
612        let _ = results;
613    }
614
615    /// Returns footers that are included in the message.
616    fn footers(&self) -> Vec<Footer<'_>> {
617        Vec::new()
618    }
619}
620
621/// Constructs a message based on the message base.
622#[allow(deprecated)]
623impl<'a, T: MessageBase> From<&'a T> for Message<'a> {
624    fn from(base: &'a T) -> Self {
625        let main_annotation = base.main_annotation();
626        let main_source = &main_annotation.location.code.source;
627        let mut annotations = vec![main_annotation];
628
629        main_source.complement_annotations(&mut annotations);
630        base.additional_annotations(&mut annotations);
631
632        Message {
633            r#type: base.message_type(),
634            title: base.message_title(),
635            annotations,
636            footers: base.footers(),
637        }
638    }
639}
640
641#[cfg(feature = "annotate-snippets")]
642mod annotate_snippets_support {
643    use super::*;
644
645    impl From<ReportType> for annotate_snippets::Level<'_> {
646        fn from(r#type: ReportType) -> Self {
647            use ReportType::*;
648            match r#type {
649                None => Self::INFO.no_name(),
650                Error => Self::ERROR,
651                Warning => Self::WARNING,
652            }
653        }
654    }
655
656    /// Converts `yash_syntax::source::pretty::Span` into
657    /// `annotate_snippets::Annotation`.
658    ///
659    /// This conversion is not provided as a public `From<&Span> for Annotation` implementation
660    /// because a future variant of `SpanRole` may map to
661    /// `annotate_snippets::Patch` instead of `annotate_snippets::Annotation`.
662    fn span_to_annotation<'a>(span: &'a Span<'a>) -> annotate_snippets::Annotation<'a> {
663        use annotate_snippets::AnnotationKind as AK;
664        let (kind, label) = match &span.role {
665            SpanRole::Primary { label } => (AK::Primary, label),
666            SpanRole::Supplementary { label } => (AK::Context, label),
667        };
668        kind.span(span.range.clone()).label(label)
669    }
670
671    // `From<&Snippet>` is not implemented for
672    // `annotate_snippets::Snippet<'_, annotate_snippets::Annotation<'_>>`
673    // because a future variant of `SpanRole` may map to
674    // `annotate_snippets::Patch` instead of `annotate_snippets::Annotation`.
675
676    /// Converts `yash_syntax::source::pretty::Snippet` into
677    /// `annotate_snippets::Snippet<'a, annotate_snippets::Annotation<'a>>`.
678    ///
679    /// This conversion is not provided as a public `From<&Snippet> for Snippet` implementation
680    /// because a future variant of `SpanRole` may map to
681    /// `annotate_snippets::Patch` instead of `annotate_snippets::Annotation`, which does not fit
682    /// into a single `annotate_snippets::Snippet<'a, annotate_snippets::Annotation<'a>>`.
683    fn snippet_to_annotation_snippet<'a>(
684        snippet: &'a Snippet<'a>,
685    ) -> annotate_snippets::Snippet<'a, annotate_snippets::Annotation<'a>> {
686        annotate_snippets::Snippet::source(snippet.code_string())
687            .line_start(
688                snippet
689                    .code
690                    .start_line_number
691                    .get()
692                    .try_into()
693                    .unwrap_or(usize::MAX),
694            )
695            .path(snippet.code.source.label())
696            .annotations(snippet.spans.iter().map(span_to_annotation))
697    }
698
699    /// Converts `yash_syntax::source::pretty::FootnoteType` into
700    /// `annotate_snippets::Level`.
701    ///
702    /// This implementation is only available when the `yash_syntax` crate is
703    /// built with the `annotate-snippets` feature enabled.
704    impl From<FootnoteType> for annotate_snippets::Level<'_> {
705        fn from(r#type: FootnoteType) -> Self {
706            use FootnoteType::*;
707            match r#type {
708                None => Self::INFO.no_name(),
709                Note => Self::NOTE,
710                Suggestion => Self::HELP,
711            }
712        }
713    }
714
715    /// Converts `yash_syntax::source::pretty::Footnote` into
716    /// `annotate_snippets::Message`.
717    ///
718    /// This implementation is only available when the `yash_syntax` crate is
719    /// built with the `annotate-snippets` feature enabled.
720    impl<'a> From<Footnote<'a>> for annotate_snippets::Message<'a> {
721        fn from(footer: Footnote<'a>) -> Self {
722            annotate_snippets::Level::from(footer.r#type).message(footer.label)
723        }
724    }
725
726    /// Converts `&yash_syntax::source::pretty::Footnote` into
727    /// `annotate_snippets::Message`.
728    ///
729    /// This implementation is only available when the `yash_syntax` crate is
730    /// built with the `annotate-snippets` feature enabled.
731    impl<'a> From<&'a Footnote<'a>> for annotate_snippets::Message<'a> {
732        fn from(footer: &'a Footnote<'a>) -> Self {
733            annotate_snippets::Level::from(footer.r#type).message(&*footer.label)
734        }
735    }
736
737    /// Converts `yash_syntax::source::pretty::Report` into
738    /// `annotate_snippets::Group`.
739    ///
740    /// This implementation is only available when the `yash_syntax` crate is
741    /// built with the `annotate-snippets` feature enabled.
742    impl<'a> From<&'a Report<'a>> for annotate_snippets::Group<'a> {
743        fn from(report: &'a Report<'a>) -> Self {
744            let title = annotate_snippets::Level::from(report.r#type).primary_title(&*report.title);
745            let title = if let Some(id) = &report.id {
746                title.id(&**id)
747            } else {
748                title
749            };
750
751            title
752                .elements(report.snippets.iter().map(snippet_to_annotation_snippet))
753                .elements(
754                    report
755                        .footnotes
756                        .iter()
757                        .map(annotate_snippets::Message::from),
758                )
759        }
760    }
761
762    /// Converts `yash_syntax::source::pretty::AnnotationType` into
763    /// `annotate_snippets::Level`.
764    ///
765    /// This implementation is only available when the `yash_syntax` crate is
766    /// built with the `annotate-snippets` feature enabled.
767    #[allow(deprecated)]
768    impl<'a> From<AnnotationType> for annotate_snippets::Level<'a> {
769        fn from(r#type: AnnotationType) -> Self {
770            use AnnotationType::*;
771            match r#type {
772                Error => Self::ERROR,
773                Warning => Self::WARNING,
774                Info => Self::INFO,
775                Note => Self::NOTE,
776                Help => Self::HELP,
777            }
778        }
779    }
780
781    /// Converts `yash_syntax::source::pretty::AnnotationType` into
782    /// `annotate_snippets::AnnotationKind`.
783    ///
784    /// This implementation is only available when the `yash_syntax` crate is
785    /// built with the `annotate-snippets` feature enabled.
786    #[allow(deprecated)]
787    impl From<AnnotationType> for annotate_snippets::AnnotationKind {
788        fn from(r#type: AnnotationType) -> Self {
789            use AnnotationType::*;
790            match r#type {
791                Error | Warning => Self::Primary,
792                Info | Note | Help => Self::Context,
793            }
794        }
795    }
796
797    /// Converts `yash_syntax::source::pretty::Message` into
798    /// `annotate_snippets::Group`.
799    ///
800    /// This implementation is only available when the `yash_syntax` crate is
801    /// built with the `annotate-snippets` feature enabled.
802    #[allow(deprecated)]
803    impl<'a> From<&'a Message<'a>> for annotate_snippets::Group<'a> {
804        fn from(message: &'a Message<'a>) -> Self {
805            let mut snippets: Vec<(
806                &super::super::Code,
807                annotate_snippets::Snippet<'a, annotate_snippets::Annotation<'a>>,
808                Vec<annotate_snippets::Annotation>,
809            )> = Vec::new();
810            // We basically convert each annotation into a snippet, but want to merge annotations
811            // with the same code into a single snippet. For this, we first collect all annotations
812            // into a temporary vector, and then merge annotations with the same code into a single
813            // snippet.
814            for annotation in &message.annotations {
815                let range = annotation.location.byte_range();
816                let as_annotation = annotate_snippets::AnnotationKind::from(annotation.r#type)
817                    .span(range)
818                    .label(&annotation.label);
819                let code = &*annotation.location.code;
820                if let Some((_, _, annotations)) =
821                    snippets.iter_mut().find(|&&mut (c, _, _)| c == code)
822                {
823                    annotations.push(as_annotation);
824                } else {
825                    let line_start = code
826                        .start_line_number
827                        .get()
828                        .try_into()
829                        .unwrap_or(usize::MAX);
830                    let snippet = annotate_snippets::Snippet::source(&**annotation.code)
831                        .line_start(line_start)
832                        .path(code.source.label());
833                    snippets.push((code, snippet, vec![as_annotation]));
834                }
835            }
836
837            annotate_snippets::Level::from(message.r#type)
838                .primary_title(&*message.title)
839                .elements(
840                    snippets
841                        .into_iter()
842                        .map(|(_, snippet, annotations)| snippet.annotations(annotations)),
843                )
844                .elements(message.footers.iter().map(|footer| {
845                    annotate_snippets::Level::from(footer.r#type).message(&*footer.label)
846                }))
847        }
848    }
849}