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 [`Message`]. 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 `Message` to `Snippet`. If you would
28//! like to use another formatter instead, you can provide your own conversion
29//! 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::Message;
40//! let error = Error {
41//!     cause: ErrorCause::Syntax(SyntaxError::EmptyParam),
42//!     location: Location::dummy(""),
43//! };
44//! let message = Message::from(&error);
45//! // The lines below require the `annotate-snippets` feature.
46//! # #[cfg(feature = "annotate-snippets")]
47//! # {
48//! let message = annotate_snippets::Message::from(&message);
49//! eprint!("{}", annotate_snippets::Renderer::plain().render(message));
50//! # }
51//! ```
52//!
53//! You can also implement conversion from your custom error object to a
54//! [`Message`], which then can be used in the same way to format a diagnostic
55//! message. To do this, you can either directly implement `From<YourError>` for
56//! `Message`, or implement [`MessageBase`] for `YourError` thereby deriving
57//! `From<&YourError>` for `Message`.
58
59use super::Location;
60use std::borrow::Cow;
61use std::cell::Ref;
62use std::ops::Deref;
63use std::rc::Rc;
64
65/// Type of annotation.
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
67pub enum AnnotationType {
68    Error,
69    Warning,
70    Info,
71    Note,
72    Help,
73}
74
75/// Source code fragment annotated with a label
76///
77/// Annotations are part of an entire [`Message`].
78#[derive(Clone)]
79pub struct Annotation<'a> {
80    /// Type of annotation
81    pub r#type: AnnotationType,
82    /// String that describes the annotated part of the source code
83    pub label: Cow<'a, str>,
84    /// Position of the annotated fragment in the source code
85    pub location: &'a Location,
86    /// Annotated code string
87    ///
88    /// This value provides an access to the string held in
89    /// `self.location.code.value`, which can only be accessed by a `Ref`.
90    pub code: Rc<dyn Deref<Target = str> + 'a>,
91}
92
93impl std::fmt::Debug for Annotation<'_> {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        f.debug_struct("Annotation")
96            .field("type", &self.r#type)
97            .field("label", &self.label)
98            .field("location", &self.location)
99            .field("code", &&**self.code)
100            .finish()
101    }
102}
103
104impl<'a> Annotation<'a> {
105    /// Creates a new annotation.
106    ///
107    /// This function makes a borrow of `location.code.value` and stores it in
108    /// `self.code`. If it has been mutually borrowed, this function panics.
109    pub fn new(r#type: AnnotationType, label: Cow<'a, str>, location: &'a Location) -> Self {
110        Annotation {
111            r#type,
112            label,
113            location,
114            code: Rc::new(Ref::map(location.code.value.borrow(), String::as_str)),
115        }
116    }
117}
118
119/// Additional text without associated source code
120#[derive(Clone, Debug)]
121pub struct Footer<'a> {
122    /// Type of this footer
123    pub r#type: AnnotationType,
124    /// Text of this footer
125    pub label: Cow<'a, str>,
126}
127
128/// Entire diagnostic message
129#[derive(Clone, Debug)]
130pub struct Message<'a> {
131    /// Type of this message
132    pub r#type: AnnotationType,
133    /// String that communicates the most important information in this message
134    pub title: Cow<'a, str>,
135    /// References to source code fragments annotated with additional information
136    pub annotations: Vec<Annotation<'a>>,
137    /// Additional text without associated source code
138    pub footers: Vec<Footer<'a>>,
139}
140
141impl super::Source {
142    /// Appends complementary annotations describing this source.
143    pub fn complement_annotations<'a, 's: 'a, T: Extend<Annotation<'a>>>(&'s self, result: &mut T) {
144        use super::Source::*;
145        match self {
146            Unknown
147            | Stdin
148            | CommandString
149            | CommandFile { .. }
150            | VariableValue { .. }
151            | InitFile { .. }
152            | Other { .. } => (),
153
154            CommandSubst { original } => {
155                // TODO Use Extend::extend_one
156                result.extend(std::iter::once(Annotation::new(
157                    AnnotationType::Info,
158                    "command substitution appeared here".into(),
159                    original,
160                )));
161            }
162            Arith { original } => {
163                // TODO Use Extend::extend_one
164                result.extend(std::iter::once(Annotation::new(
165                    AnnotationType::Info,
166                    "arithmetic expansion appeared here".into(),
167                    original,
168                )));
169            }
170            Eval { original } => {
171                // TODO Use Extend::extend_one
172                result.extend(std::iter::once(Annotation::new(
173                    AnnotationType::Info,
174                    "command passed to the eval built-in here".into(),
175                    original,
176                )));
177            }
178            DotScript { name, origin } => {
179                // TODO Use Extend::extend_one
180                result.extend(std::iter::once(Annotation::new(
181                    AnnotationType::Info,
182                    format!("script `{name}` was sourced here",).into(),
183                    origin,
184                )));
185            }
186            Trap { origin, .. } => {
187                // TODO Use Extend::extend_one
188                result.extend(std::iter::once(Annotation::new(
189                    AnnotationType::Info,
190                    "trap was set here".into(),
191                    origin,
192                )));
193            }
194            Alias { original, alias } => {
195                // TODO Use Extend::extend_one
196                result.extend(std::iter::once(Annotation::new(
197                    AnnotationType::Info,
198                    format!("alias `{}` was substituted here", alias.name).into(),
199                    original,
200                )));
201                original.code.source.complement_annotations(result);
202                result.extend(std::iter::once(Annotation::new(
203                    AnnotationType::Info,
204                    format!("alias `{}` was defined here", alias.name).into(),
205                    &alias.origin,
206                )));
207                alias.origin.code.source.complement_annotations(result);
208            }
209        }
210    }
211}
212
213/// Helper for constructing a [`Message`]
214///
215/// Thanks to the blanket implementation `impl<'a, T: MessageBase> From<&'a T>
216/// for Message<'a>`, implementors of this trait can be converted to a message
217/// for free.
218pub trait MessageBase {
219    /// Returns the type of the entire message.
220    ///
221    /// The default implementation returns `AnnotationType::Error`.
222    fn message_type(&self) -> AnnotationType {
223        AnnotationType::Error
224    }
225
226    // TODO message tag
227
228    /// Returns the main caption of the message.
229    fn message_title(&self) -> Cow<str>;
230
231    /// Returns an annotation to be the first in the message.
232    fn main_annotation(&self) -> Annotation<'_>;
233
234    /// Adds additional annotations to the given container.
235    ///
236    /// The default implementation does nothing.
237    fn additional_annotations<'a, T: Extend<Annotation<'a>>>(&'a self, results: &mut T) {
238        let _ = results;
239    }
240
241    /// Returns footers that are included in the message.
242    fn footers(&self) -> Vec<Footer> {
243        Vec::new()
244    }
245}
246
247/// Constructs a message based on the message base.
248impl<'a, T: MessageBase> From<&'a T> for Message<'a> {
249    fn from(base: &'a T) -> Self {
250        let main_annotation = base.main_annotation();
251        let main_source = &main_annotation.location.code.source;
252        let mut annotations = vec![main_annotation];
253
254        main_source.complement_annotations(&mut annotations);
255        base.additional_annotations(&mut annotations);
256
257        Message {
258            r#type: base.message_type(),
259            title: base.message_title(),
260            annotations,
261            footers: base.footers(),
262        }
263    }
264}
265
266#[cfg(feature = "annotate-snippets")]
267mod annotate_snippets_support {
268    use super::*;
269
270    /// Converts `yash_syntax::source::pretty::AnnotationType` into
271    /// `annotate_snippets::Level`.
272    ///
273    /// This implementation is only available when the `yash_syntax` crate is
274    /// built with the `annotate-snippets` feature enabled.
275    impl From<AnnotationType> for annotate_snippets::Level {
276        fn from(r#type: AnnotationType) -> Self {
277            use AnnotationType::*;
278            match r#type {
279                Error => Self::Error,
280                Warning => Self::Warning,
281                Info => Self::Info,
282                Note => Self::Note,
283                Help => Self::Help,
284            }
285        }
286    }
287
288    /// Converts `yash_syntax::source::pretty::Message` into
289    /// `annotate_snippets::Message`.
290    ///
291    /// This implementation is only available when the `yash_syntax` crate is
292    /// built with the `annotate-snippets` feature enabled.
293    impl<'a> From<&'a Message<'a>> for annotate_snippets::Message<'a> {
294        fn from(message: &'a Message<'a>) -> Self {
295            let mut snippets: Vec<(
296                &super::super::Code,
297                annotate_snippets::Snippet,
298                Vec<annotate_snippets::Annotation>,
299            )> = Vec::new();
300            // We basically convert each annotation into a snippet, but want to merge annotations
301            // with the same code into a single snippet. For this, we first collect all annotations
302            // into a temporary vector, and then merge annotations with the same code into a single
303            // snippet.
304            for annotation in &message.annotations {
305                let range = annotation.location.range.clone();
306                let level = annotate_snippets::Level::from(annotation.r#type);
307                let as_annotation = level.span(range).label(&annotation.label);
308                let code = &*annotation.location.code;
309                if let Some((_, _, annotations)) =
310                    snippets.iter_mut().find(|&&mut (c, _, _)| c == code)
311                {
312                    annotations.push(as_annotation);
313                } else {
314                    let line_start = code
315                        .start_line_number
316                        .get()
317                        .try_into()
318                        .unwrap_or(usize::MAX);
319                    let snippet = annotate_snippets::Snippet::source(&annotation.code)
320                        .line_start(line_start)
321                        .origin(code.source.label())
322                        .fold(true);
323                    snippets.push((code, snippet, vec![as_annotation]));
324                }
325            }
326
327            annotate_snippets::Level::from(message.r#type)
328                .title(&message.title)
329                .snippets(
330                    snippets
331                        .into_iter()
332                        .map(|(_, snippet, annotations)| snippet.annotations(annotations)),
333                )
334                .footers(message.footers.iter().map(|footer| {
335                    let level = annotate_snippets::Level::from(footer.r#type);
336                    level.title(&footer.label)
337                }))
338        }
339    }
340}