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}