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}