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        // TODO Use Vec::push_mut when stabilized
250        snippets.push(Snippet::with_code(code));
251        snippets.last_mut().unwrap()
252    }
253}
254
255/// Adds a span to the appropriate snippet in the given vector.
256///
257/// This is a utility function used in constructing a vector of snippets with
258/// annotated spans.
259///
260/// If a snippet for the given code already exists in the vector, this function
261/// adds the span to that snippet. Otherwise, it creates a new snippet with the
262/// given code and span, and appends it to the vector.
263pub fn add_span<'a>(code: &'a super::Code, span: Span<'a>, snippets: &mut Vec<Snippet<'a>>) {
264    snippet_for_code(snippets, code).spans.push(span);
265}
266
267#[test]
268fn test_add_span_with_matching_code() {
269    let code = Rc::new(super::Code {
270        value: std::cell::RefCell::new("echo hello".to_string()),
271        start_line_number: std::num::NonZero::new(1).unwrap(),
272        source: Rc::new(super::Source::CommandString),
273    });
274    let span = Span {
275        range: 5..10,
276        role: SpanRole::Primary {
277            label: "greeting".into(),
278        },
279    };
280    let mut snippets = vec![Snippet::with_code(&code)];
281
282    add_span(&code, span, &mut snippets);
283
284    assert_eq!(snippets.len(), 1);
285    assert_eq!(snippets[0].spans.len(), 1);
286    assert_eq!(snippets[0].spans[0].range, 5..10);
287    assert_eq!(
288        snippets[0].spans[0].role,
289        SpanRole::Primary {
290            label: "greeting".into()
291        }
292    );
293}
294
295#[test]
296fn test_add_span_without_matching_code() {
297    let code1 = Rc::new(super::Code {
298        value: std::cell::RefCell::new("echo hello".to_string()),
299        start_line_number: std::num::NonZero::new(1).unwrap(),
300        source: Rc::new(super::Source::CommandString),
301    });
302    let code2 = Rc::new(super::Code {
303        value: std::cell::RefCell::new("ls -l".to_string()),
304        start_line_number: std::num::NonZero::new(1).unwrap(),
305        source: Rc::new(super::Source::CommandString),
306    });
307    let span = Span {
308        range: 0..2,
309        role: SpanRole::Primary {
310            label: "list".into(),
311        },
312    };
313    let mut snippets = vec![Snippet::with_code(&code1)];
314
315    add_span(&code2, span, &mut snippets);
316
317    assert_eq!(snippets.len(), 2);
318    assert_eq!(snippets[0].code.value.borrow().as_str(), "echo hello");
319    assert_eq!(snippets[0].spans.len(), 0);
320    assert_eq!(snippets[1].code.value.borrow().as_str(), "ls -l");
321    assert_eq!(snippets[1].spans.len(), 1);
322    assert_eq!(snippets[1].spans[0].range, 0..2);
323    assert_eq!(
324        snippets[1].spans[0].role,
325        SpanRole::Primary {
326            label: "list".into()
327        }
328    );
329}
330
331impl super::Source {
332    /// Extends the given vector of snippets with spans annotating the context of this source.
333    ///
334    /// If `self` is a source that has a related location (e.g., the `original` field of
335    /// `CommandSubst`), this method adds one or more spans describing the location to the given
336    /// vector. If the `code` of the location is already present in the vector, it adds the span
337    /// to the existing snippet; otherwise, it creates a new snippet.
338    ///
339    /// If `self` does not have a related location, this method does nothing.
340    pub fn extend_with_context<'a>(&'a self, snippets: &mut Vec<Snippet<'a>>) {
341        use super::Source::*;
342        match self {
343            Unknown
344            | Stdin
345            | CommandString
346            | CommandFile { .. }
347            | VariableValue { .. }
348            | InitFile { .. }
349            | Other { .. } => (),
350
351            CommandSubst { original } => {
352                let range = original.byte_range();
353                let role = SpanRole::Supplementary {
354                    label: "command substitution appeared here".into(),
355                };
356                add_span(&original.code, Span { range, role }, snippets);
357            }
358
359            Arith { original } => {
360                let range = original.byte_range();
361                let role = SpanRole::Supplementary {
362                    label: "arithmetic expansion appeared here".into(),
363                };
364                add_span(&original.code, Span { range, role }, snippets);
365            }
366
367            Eval { original } => {
368                let range = original.byte_range();
369                let role = SpanRole::Supplementary {
370                    label: "command passed to the eval built-in here".into(),
371                };
372                add_span(&original.code, Span { range, role }, snippets);
373            }
374
375            DotScript { name, origin } => {
376                let range = origin.byte_range();
377                let role = SpanRole::Supplementary {
378                    label: format!("script `{name}` was sourced here").into(),
379                };
380                add_span(&origin.code, Span { range, role }, snippets);
381            }
382
383            Trap { origin, .. } => {
384                let range = origin.byte_range();
385                let role = SpanRole::Supplementary {
386                    label: "trap was set here".into(),
387                };
388                add_span(&origin.code, Span { range, role }, snippets);
389            }
390
391            Alias { original, alias } => {
392                // Where the alias was substituted
393                let range = original.byte_range();
394                let role = SpanRole::Supplementary {
395                    label: format!("alias `{}` was substituted here", alias.name).into(),
396                };
397                add_span(&original.code, Span { range, role }, snippets);
398                // Recurse into the source of the substituted code
399                original.code.source.extend_with_context(snippets);
400
401                // Where the alias was defined
402                let range = alias.origin.byte_range();
403                let role = SpanRole::Supplementary {
404                    label: format!("alias `{}` was defined here", alias.name).into(),
405                };
406                add_span(&alias.origin.code, Span { range, role }, snippets);
407                // Recurse into the source of the alias definition
408                alias.origin.code.source.extend_with_context(snippets);
409            }
410        }
411    }
412}
413
414mod annotate_snippets_support {
415    use super::*;
416
417    impl From<ReportType> for annotate_snippets::Level<'_> {
418        fn from(r#type: ReportType) -> Self {
419            use ReportType::*;
420            match r#type {
421                None => Self::INFO.no_name(),
422                Error => Self::ERROR,
423                Warning => Self::WARNING,
424            }
425        }
426    }
427
428    /// Converts `yash_env::source::pretty::Span` into
429    /// `annotate_snippets::Annotation`.
430    ///
431    /// This conversion is not provided as a public `From<&Span> for Annotation` implementation
432    /// because a future variant of `SpanRole` may map to
433    /// `annotate_snippets::Patch` instead of `annotate_snippets::Annotation`.
434    fn span_to_annotation<'a>(span: &'a Span<'a>) -> annotate_snippets::Annotation<'a> {
435        use annotate_snippets::AnnotationKind as AK;
436        let (kind, label) = match &span.role {
437            SpanRole::Primary { label } => (AK::Primary, label),
438            SpanRole::Supplementary { label } => (AK::Context, label),
439        };
440        kind.span(span.range.clone()).label(label)
441    }
442
443    // `From<&Snippet>` is not implemented for
444    // `annotate_snippets::Snippet<'_, annotate_snippets::Annotation<'_>>`
445    // because a future variant of `SpanRole` may map to
446    // `annotate_snippets::Patch` instead of `annotate_snippets::Annotation`.
447
448    /// Converts `yash_env::source::pretty::Snippet` into
449    /// `annotate_snippets::Snippet<'a, annotate_snippets::Annotation<'a>>`.
450    ///
451    /// This conversion is not provided as a public `From<&Snippet> for Snippet` implementation
452    /// because a future variant of `SpanRole` may map to
453    /// `annotate_snippets::Patch` instead of `annotate_snippets::Annotation`, which does not fit
454    /// into a single `annotate_snippets::Snippet<'a, annotate_snippets::Annotation<'a>>`.
455    fn snippet_to_annotation_snippet<'a>(
456        snippet: &'a Snippet<'a>,
457    ) -> annotate_snippets::Snippet<'a, annotate_snippets::Annotation<'a>> {
458        annotate_snippets::Snippet::source(snippet.code_string())
459            .line_start(
460                snippet
461                    .code
462                    .start_line_number
463                    .get()
464                    .try_into()
465                    .unwrap_or(usize::MAX),
466            )
467            .path(snippet.code.source.label())
468            .annotations(snippet.spans.iter().map(span_to_annotation))
469    }
470
471    /// Converts `yash_env::source::pretty::FootnoteType` into
472    /// `annotate_snippets::Level`.
473    impl From<FootnoteType> for annotate_snippets::Level<'_> {
474        fn from(r#type: FootnoteType) -> Self {
475            use FootnoteType::*;
476            match r#type {
477                None => Self::INFO.no_name(),
478                Note => Self::NOTE,
479                Suggestion => Self::HELP,
480            }
481        }
482    }
483
484    /// Converts `yash_env::source::pretty::Footnote` into
485    /// `annotate_snippets::Message`.
486    impl<'a> From<Footnote<'a>> for annotate_snippets::Message<'a> {
487        fn from(footer: Footnote<'a>) -> Self {
488            annotate_snippets::Level::from(footer.r#type).message(footer.label)
489        }
490    }
491
492    /// Converts `&yash_env::source::pretty::Footnote` into
493    /// `annotate_snippets::Message`.
494    impl<'a> From<&'a Footnote<'a>> for annotate_snippets::Message<'a> {
495        fn from(footer: &'a Footnote<'a>) -> Self {
496            annotate_snippets::Level::from(footer.r#type).message(&*footer.label)
497        }
498    }
499
500    /// Converts `yash_env::source::pretty::Report` into
501    /// `annotate_snippets::Group`.
502    impl<'a> From<&'a Report<'a>> for annotate_snippets::Group<'a> {
503        fn from(report: &'a Report<'a>) -> Self {
504            let title = annotate_snippets::Level::from(report.r#type).primary_title(&*report.title);
505            let title = if let Some(id) = &report.id {
506                title.id(&**id)
507            } else {
508                title
509            };
510
511            title
512                .elements(report.snippets.iter().map(snippet_to_annotation_snippet))
513                .elements(
514                    report
515                        .footnotes
516                        .iter()
517                        .map(annotate_snippets::Message::from),
518                )
519        }
520    }
521}