Skip to main content

yash_env/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 object that can be converted to a
22//! [`Report`], you can then convert it to `annotate_snippets::Group`, which
23//! can be formatted into a human-readable diagnostic message string. If you
24//! want to use another formatter instead of `annotate_snippets`, you can
25//! provide your own conversion from `Report` to the formatter's data
26//! structures.
27//!
28//! ## Printing an error
29//!
30//! This example shows how to format an
31//! [`StartSubshellError`](crate::semantics::command::StartSubshellError)
32//! instance into a human-readable string.
33//!
34//! ```
35//! # use yash_env::semantics::Field;
36//! # use yash_env::semantics::command::StartSubshellError;
37//! # use yash_env::source::pretty::Report;
38//! # use yash_env::system::Errno;
39//! let error = StartSubshellError {
40//!     utility: Field::dummy("foo"),
41//!     errno: Errno::EAGAIN,
42//! };
43//! let report = Report::from(&error);
44//! let group = annotate_snippets::Group::from(&report);
45//! eprintln!("{}", annotate_snippets::Renderer::plain().render(&[group]));
46//! ```
47//!
48//! You can also implement conversion from your custom error object to
49//! [`Report`], which then can be used in the same way to format a diagnostic
50//! message. To do this, implement `From<YourError>` or `From<&YourError>` for
51//! `Report`.
52
53use super::Location;
54use std::borrow::Cow;
55use std::cell::Ref;
56use std::ops::Range;
57#[cfg(test)]
58use std::rc::Rc;
59
60/// Type of [`Report`]
61#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
62#[non_exhaustive]
63pub enum ReportType {
64    #[default]
65    None,
66    Error,
67    Warning,
68}
69
70/// Type and label annotating a [`Span`]
71#[derive(Clone, Debug, Eq, PartialEq)]
72#[non_exhaustive]
73pub enum SpanRole<'a> {
74    /// Primary span, usually indicating the main cause of a problem
75    Primary { label: Cow<'a, str> },
76    /// Secondary span, usually indicating related information
77    Supplementary { label: Cow<'a, str> },
78    // Patch { replacement: Cow<'a, str> },
79}
80
81/// Part of source code [`Snippet`] annotated with additional information
82#[derive(Clone, Debug, Eq, PartialEq)]
83pub struct Span<'a> {
84    /// Range of bytes in the source code
85    pub range: Range<usize>,
86    /// Type and label of this span
87    pub role: SpanRole<'a>,
88}
89
90/// Fragment of source code with annotated spans highlighting specific regions
91///
92/// A snippet corresponds to a single source [`Code`](super::Code). It contains
93/// zero or more [`Span`]s that annotate specific parts of the code.
94///
95/// `Snippet` holds a [`Ref`] to the string held in `self.code.value`, which
96/// provides an access to the string without making a new borrow
97/// ([`code_string`](Self::code_string)). This allows creating another
98/// message builder such as `annotate_snippets::Snippet` without the need to
99/// retain a borrow of `self.code.value`.
100#[derive(Debug)]
101pub struct Snippet<'a> {
102    /// Source code to which the spans refer
103    pub code: &'a super::Code,
104    /// Reference to the string held in `self.code.value`
105    code_string: Ref<'a, str>,
106    /// Spans describing parts of the code
107    pub spans: Vec<Span<'a>>,
108}
109
110impl Snippet<'_> {
111    /// Creates a new snippet for the given code without any spans.
112    #[must_use]
113    pub fn with_code(code: &super::Code) -> Snippet<'_> {
114        Self::with_code_and_spans(code, Vec::new())
115    }
116
117    /// Creates a new snippet for the given code with the given spans.
118    #[must_use]
119    pub fn with_code_and_spans<'a>(code: &'a super::Code, spans: Vec<Span<'a>>) -> Snippet<'a> {
120        Snippet {
121            code,
122            code_string: Ref::map(code.value.borrow(), String::as_str),
123            spans,
124        }
125    }
126
127    /// Creates a vector containing a snippet with a primary span.
128    ///
129    /// This is a convenience function for creating a vector of snippets
130    /// containing a primary span created from the given location and label.
131    /// The returned vector can be used as the `snippets` field of a
132    /// [`Report`].
133    ///
134    /// This function calls
135    /// [`Source::extend_with_context`](super::Source::extend_with_context) for
136    /// `location.code.source`, thereby adding supplementary spans describing the
137    /// context of the source code. This means that the returned vector may
138    /// contain multiple snippets or spans if the source has a related location.
139    #[must_use]
140    pub fn with_primary_span<'a>(location: &'a Location, label: Cow<'a, str>) -> Vec<Snippet<'a>> {
141        let range = location.byte_range();
142        let role = SpanRole::Primary { label };
143        let spans = vec![Span { range, role }];
144        let mut snippets = vec![Snippet::with_code_and_spans(&location.code, spans)];
145        location.code.source.extend_with_context(&mut snippets);
146        snippets
147    }
148
149    /// Returns the string held in `self.code.value`.
150    ///
151    /// This method returns a reference to the string held in `self.code.value`.
152    /// `Snippet` internally holds a `Ref` to the string, which provides an
153    /// access to the string without making a new borrow.
154    #[inline(always)]
155    #[must_use]
156    pub fn code_string(&self) -> &str {
157        &self.code_string
158    }
159}
160
161impl Clone for Snippet<'_> {
162    fn clone(&self) -> Self {
163        Snippet {
164            code: self.code,
165            code_string: Ref::clone(&self.code_string),
166            spans: self.spans.clone(),
167        }
168    }
169}
170
171impl PartialEq<Snippet<'_>> for Snippet<'_> {
172    fn eq(&self, other: &Snippet<'_>) -> bool {
173        self.code == other.code && self.spans == other.spans
174    }
175}
176
177impl Eq for Snippet<'_> {}
178
179/// Type of [`Footnote`]
180#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
181#[non_exhaustive]
182pub enum FootnoteType {
183    /// No specific type
184    #[default]
185    None,
186    /// For footnotes that provide additional information
187    Note,
188    /// For footnotes that provide suggestions
189    Suggestion,
190}
191
192/// Message without associated source code
193#[derive(Clone, Debug, Default, Eq, PartialEq)]
194pub struct Footnote<'a> {
195    /// Type of this footnote
196    pub r#type: FootnoteType,
197    /// Text of this footnote
198    pub label: Cow<'a, str>,
199}
200
201/// Entire report containing multiple snippets
202///
203/// `Report` is an intermediate data structure for constructing a diagnostic
204/// message. It contains multiple [`Snippet`]s, each of which corresponds to a
205/// specific part of the source code being analyzed.
206/// See the [module documentation](self) for more details.
207#[derive(Clone, Debug, Default, Eq, PartialEq)]
208#[non_exhaustive]
209pub struct Report<'a> {
210    /// Type of this report
211    pub r#type: ReportType,
212    /// Optional identifier of this report (e.g., error code)
213    pub id: Option<Cow<'a, str>>,
214    /// Main caption of this report
215    pub title: Cow<'a, str>,
216    /// Source code fragments annotated with additional information
217    pub snippets: Vec<Snippet<'a>>,
218    /// Additional message without associated source code
219    pub footnotes: Vec<Footnote<'a>>,
220}
221
222impl Report<'_> {
223    /// Creates a new, empty report.
224    #[inline]
225    #[must_use]
226    pub fn new() -> Self {
227        Report::default()
228    }
229}
230
231/// Returns a mutable reference to the snippet for the given code, creating it
232/// if necessary.
233///
234/// This is a utility function used in constructing a vector of snippets.
235///
236/// If a snippet for the given code already exists in the vector, this function
237/// returns a mutable reference to that snippet. Otherwise, it creates a new
238/// snippet with the given code and appends it to the vector, returning a
239/// mutable reference to the newly created snippet.
240pub fn snippet_for_code<'a, 'b>(
241    snippets: &'b mut Vec<Snippet<'a>>,
242    code: &'a super::Code,
243) -> &'b mut Snippet<'a> {
244    // if let Some(snippet) = snippets.iter_mut().find(|s| std::ptr::eq(s.code, code)) {
245    //     snippet
246    if let Some(i) = snippets.iter().position(|s| std::ptr::eq(s.code, code)) {
247        &mut snippets[i]
248    } else {
249        snippets.push_mut(Snippet::with_code(code))
250    }
251}
252
253/// Adds a span to the appropriate snippet in the given vector.
254///
255/// This is a utility function used in constructing a vector of snippets with
256/// annotated spans.
257///
258/// If a snippet for the given code already exists in the vector, this function
259/// adds the span to that snippet. Otherwise, it creates a new snippet with the
260/// given code and span, and appends it to the vector.
261pub fn add_span<'a>(code: &'a super::Code, span: Span<'a>, snippets: &mut Vec<Snippet<'a>>) {
262    snippet_for_code(snippets, code).spans.push(span);
263}
264
265#[test]
266fn test_add_span_with_matching_code() {
267    let code = Rc::new(super::Code {
268        value: std::cell::RefCell::new("echo hello".to_string()),
269        start_line_number: std::num::NonZero::new(1).unwrap(),
270        source: Rc::new(super::Source::CommandString),
271    });
272    let span = Span {
273        range: 5..10,
274        role: SpanRole::Primary {
275            label: "greeting".into(),
276        },
277    };
278    let mut snippets = vec![Snippet::with_code(&code)];
279
280    add_span(&code, span, &mut snippets);
281
282    assert_eq!(snippets.len(), 1);
283    assert_eq!(snippets[0].spans.len(), 1);
284    assert_eq!(snippets[0].spans[0].range, 5..10);
285    assert_eq!(
286        snippets[0].spans[0].role,
287        SpanRole::Primary {
288            label: "greeting".into()
289        }
290    );
291}
292
293#[test]
294fn test_add_span_without_matching_code() {
295    let code1 = Rc::new(super::Code {
296        value: std::cell::RefCell::new("echo hello".to_string()),
297        start_line_number: std::num::NonZero::new(1).unwrap(),
298        source: Rc::new(super::Source::CommandString),
299    });
300    let code2 = Rc::new(super::Code {
301        value: std::cell::RefCell::new("ls -l".to_string()),
302        start_line_number: std::num::NonZero::new(1).unwrap(),
303        source: Rc::new(super::Source::CommandString),
304    });
305    let span = Span {
306        range: 0..2,
307        role: SpanRole::Primary {
308            label: "list".into(),
309        },
310    };
311    let mut snippets = vec![Snippet::with_code(&code1)];
312
313    add_span(&code2, span, &mut snippets);
314
315    assert_eq!(snippets.len(), 2);
316    assert_eq!(snippets[0].code.value.borrow().as_str(), "echo hello");
317    assert_eq!(snippets[0].spans.len(), 0);
318    assert_eq!(snippets[1].code.value.borrow().as_str(), "ls -l");
319    assert_eq!(snippets[1].spans.len(), 1);
320    assert_eq!(snippets[1].spans[0].range, 0..2);
321    assert_eq!(
322        snippets[1].spans[0].role,
323        SpanRole::Primary {
324            label: "list".into()
325        }
326    );
327}
328
329impl super::Source {
330    /// Extends the given vector of snippets with spans annotating the context of this source.
331    ///
332    /// If `self` is a source that has a related location (e.g., the `original` field of
333    /// `CommandSubst`), this method adds one or more spans describing the location to the given
334    /// vector. If the `code` of the location is already present in the vector, it adds the span
335    /// to the existing snippet; otherwise, it creates a new snippet.
336    ///
337    /// If `self` does not have a related location, this method does nothing.
338    pub fn extend_with_context<'a>(&'a self, snippets: &mut Vec<Snippet<'a>>) {
339        use super::Source::*;
340        match self {
341            Unknown
342            | Stdin
343            | CommandString
344            | CommandFile { .. }
345            | VariableValue { .. }
346            | InitFile { .. }
347            | Other { .. } => (),
348
349            CommandSubst { original } => {
350                let range = original.byte_range();
351                let role = SpanRole::Supplementary {
352                    label: "command substitution appeared here".into(),
353                };
354                add_span(&original.code, Span { range, role }, snippets);
355            }
356
357            Arith { original } => {
358                let range = original.byte_range();
359                let role = SpanRole::Supplementary {
360                    label: "arithmetic expansion appeared here".into(),
361                };
362                add_span(&original.code, Span { range, role }, snippets);
363            }
364
365            Eval { original } => {
366                let range = original.byte_range();
367                let role = SpanRole::Supplementary {
368                    label: "command passed to the eval built-in here".into(),
369                };
370                add_span(&original.code, Span { range, role }, snippets);
371            }
372
373            DotScript { name, origin } => {
374                let range = origin.byte_range();
375                let role = SpanRole::Supplementary {
376                    label: format!("script `{name}` was sourced here").into(),
377                };
378                add_span(&origin.code, Span { range, role }, snippets);
379            }
380
381            Trap { origin, .. } => {
382                let range = origin.byte_range();
383                let role = SpanRole::Supplementary {
384                    label: "trap was set here".into(),
385                };
386                add_span(&origin.code, Span { range, role }, snippets);
387            }
388
389            Alias { original, alias } => {
390                // Where the alias was substituted
391                let range = original.byte_range();
392                let role = SpanRole::Supplementary {
393                    label: format!("alias `{}` was substituted here", alias.name).into(),
394                };
395                add_span(&original.code, Span { range, role }, snippets);
396                // Recurse into the source of the substituted code
397                original.code.source.extend_with_context(snippets);
398
399                // Where the alias was defined
400                let range = alias.origin.byte_range();
401                let role = SpanRole::Supplementary {
402                    label: format!("alias `{}` was defined here", alias.name).into(),
403                };
404                add_span(&alias.origin.code, Span { range, role }, snippets);
405                // Recurse into the source of the alias definition
406                alias.origin.code.source.extend_with_context(snippets);
407            }
408        }
409    }
410}
411
412mod annotate_snippets_support {
413    use super::*;
414
415    impl From<ReportType> for annotate_snippets::Level<'_> {
416        fn from(r#type: ReportType) -> Self {
417            use ReportType::*;
418            match r#type {
419                None => Self::INFO.no_name(),
420                Error => Self::ERROR,
421                Warning => Self::WARNING,
422            }
423        }
424    }
425
426    /// Converts `yash_env::source::pretty::Span` into
427    /// `annotate_snippets::Annotation`.
428    ///
429    /// This conversion is not provided as a public `From<&Span> for Annotation` implementation
430    /// because a future variant of `SpanRole` may map to
431    /// `annotate_snippets::Patch` instead of `annotate_snippets::Annotation`.
432    fn span_to_annotation<'a>(span: &'a Span<'a>) -> annotate_snippets::Annotation<'a> {
433        use annotate_snippets::AnnotationKind as AK;
434        let (kind, label) = match &span.role {
435            SpanRole::Primary { label } => (AK::Primary, label),
436            SpanRole::Supplementary { label } => (AK::Context, label),
437        };
438        kind.span(span.range.clone()).label(label)
439    }
440
441    // `From<&Snippet>` is not implemented for
442    // `annotate_snippets::Snippet<'_, annotate_snippets::Annotation<'_>>`
443    // because a future variant of `SpanRole` may map to
444    // `annotate_snippets::Patch` instead of `annotate_snippets::Annotation`.
445
446    /// Converts `yash_env::source::pretty::Snippet` into
447    /// `annotate_snippets::Snippet<'a, annotate_snippets::Annotation<'a>>`.
448    ///
449    /// This conversion is not provided as a public `From<&Snippet> for Snippet` implementation
450    /// because a future variant of `SpanRole` may map to
451    /// `annotate_snippets::Patch` instead of `annotate_snippets::Annotation`, which does not fit
452    /// into a single `annotate_snippets::Snippet<'a, annotate_snippets::Annotation<'a>>`.
453    fn snippet_to_annotation_snippet<'a>(
454        snippet: &'a Snippet<'a>,
455    ) -> annotate_snippets::Snippet<'a, annotate_snippets::Annotation<'a>> {
456        annotate_snippets::Snippet::source(snippet.code_string())
457            .line_start(
458                snippet
459                    .code
460                    .start_line_number
461                    .get()
462                    .try_into()
463                    .unwrap_or(usize::MAX),
464            )
465            .path(snippet.code.source.label())
466            .annotations(snippet.spans.iter().map(span_to_annotation))
467    }
468
469    /// Converts `yash_env::source::pretty::FootnoteType` into
470    /// `annotate_snippets::Level`.
471    impl From<FootnoteType> for annotate_snippets::Level<'_> {
472        fn from(r#type: FootnoteType) -> Self {
473            use FootnoteType::*;
474            match r#type {
475                None => Self::INFO.no_name(),
476                Note => Self::NOTE,
477                Suggestion => Self::HELP,
478            }
479        }
480    }
481
482    /// Converts `yash_env::source::pretty::Footnote` into
483    /// `annotate_snippets::Message`.
484    impl<'a> From<Footnote<'a>> for annotate_snippets::Message<'a> {
485        fn from(footer: Footnote<'a>) -> Self {
486            annotate_snippets::Level::from(footer.r#type).message(footer.label)
487        }
488    }
489
490    /// Converts `&yash_env::source::pretty::Footnote` into
491    /// `annotate_snippets::Message`.
492    impl<'a> From<&'a Footnote<'a>> for annotate_snippets::Message<'a> {
493        fn from(footer: &'a Footnote<'a>) -> Self {
494            annotate_snippets::Level::from(footer.r#type).message(&*footer.label)
495        }
496    }
497
498    /// Converts `yash_env::source::pretty::Report` into
499    /// `annotate_snippets::Group`.
500    impl<'a> From<&'a Report<'a>> for annotate_snippets::Group<'a> {
501        fn from(report: &'a Report<'a>) -> Self {
502            let title = annotate_snippets::Level::from(report.r#type).primary_title(&*report.title);
503            let title = if let Some(id) = &report.id {
504                title.id(&**id)
505            } else {
506                title
507            };
508
509            title
510                .elements(report.snippets.iter().map(snippet_to_annotation_snippet))
511                .elements(
512                    report
513                        .footnotes
514                        .iter()
515                        .map(annotate_snippets::Message::from),
516                )
517        }
518    }
519}