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}