ariadne/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
3
4mod source;
5mod display;
6mod draw;
7mod write;
8
9pub use crate::{
10    source::{Line, Source, Cache, FileCache, FnCache, sources},
11    draw::{Fmt, ColorGenerator},
12};
13pub use yansi::Color;
14
15#[cfg(any(feature = "concolor", doc))]
16pub use crate::draw::StdoutFmt;
17
18use crate::display::*;
19use std::{
20    ops::Range,
21    io::{self, Write},
22    hash::Hash,
23    cmp::{PartialEq, Eq},
24    fmt,
25};
26use unicode_width::UnicodeWidthChar;
27
28/// A trait implemented by spans within a character-based source.
29pub trait Span {
30    /// The identifier used to uniquely refer to a source. In most cases, this is the fully-qualified path of the file.
31    type SourceId: PartialEq + ToOwned + ?Sized;
32
33    /// Get the identifier of the source that this span refers to.
34    fn source(&self) -> &Self::SourceId;
35
36    /// Get the start offset of this span.
37    ///
38    /// Offsets are zero-indexed character offsets from the beginning of the source.
39    fn start(&self) -> usize;
40
41    /// Get the (exclusive) end offset of this span.
42    ///
43    /// The end offset should *always* be greater than or equal to the start offset as given by [`Span::start`].
44    ///
45    /// Offsets are zero-indexed character offsets from the beginning of the source.
46    fn end(&self) -> usize;
47
48    /// Get the length of this span (difference between the start of the span and the end of the span).
49    fn len(&self) -> usize { self.end().saturating_sub(self.start()) }
50
51    /// Determine whether the span contains the given offset.
52    fn contains(&self, offset: usize) -> bool { (self.start()..self.end()).contains(&offset) }
53}
54
55impl Span for Range<usize> {
56    type SourceId = ();
57
58    fn source(&self) -> &Self::SourceId { &() }
59    fn start(&self) -> usize { self.start }
60    fn end(&self) -> usize { self.end }
61}
62
63impl<Id: fmt::Debug + Hash + PartialEq + Eq + ToOwned> Span for (Id, Range<usize>) {
64    type SourceId = Id;
65
66    fn source(&self) -> &Self::SourceId { &self.0 }
67    fn start(&self) -> usize { self.1.start }
68    fn end(&self) -> usize { self.1.end }
69}
70
71/// A type that represents a labelled section of source code.
72#[derive(Clone, Debug, Hash, PartialEq, Eq)]
73pub struct Label<S = Range<usize>> {
74    span: S,
75    msg: Option<String>,
76    color: Option<Color>,
77    order: i32,
78    priority: i32,
79}
80
81impl<S> Label<S> {
82    /// Create a new [`Label`].
83    pub fn new(span: S) -> Self {
84        Self {
85            span,
86            msg: None,
87            color: None,
88            order: 0,
89            priority: 0,
90        }
91    }
92
93    /// Give this label a message.
94    pub fn with_message<M: ToString>(mut self, msg: M) -> Self {
95        self.msg = Some(msg.to_string());
96        self
97    }
98
99    /// Give this label a highlight colour.
100    pub fn with_color(mut self, color: Color) -> Self {
101        self.color = Some(color);
102        self
103    }
104
105    /// Specify the order of this label relative to other labels.
106    ///
107    /// Lower values correspond to this label having an earlier order.
108    ///
109    /// If unspecified, labels default to an order of `0`.
110    ///
111    /// When labels are displayed after a line the crate needs to decide which labels should be displayed first. By
112    /// Default, the orders labels based on where their associated line meets the text (see [`LabelAttach`]).
113    /// Additionally, multi-line labels are ordered before inline labels. You can use this function to override this
114    /// behaviour.
115    pub fn with_order(mut self, order: i32) -> Self {
116        self.order = order;
117        self
118    }
119
120    /// Specify the priority of this label relative to other labels.
121    ///
122    /// Higher values correspond to this label having a higher priority.
123    ///
124    /// If unspecified, labels default to a priority of `0`.
125    ///
126    /// Label spans can overlap. When this happens, the crate needs to decide which labels to prioritise for various
127    /// purposes such as highlighting. By default, spans with a smaller length get a higher priority. You can use this
128    /// function to override this behaviour.
129    pub fn with_priority(mut self, priority: i32) -> Self {
130        self.priority = priority;
131        self
132    }
133}
134
135/// A type representing a diagnostic that is ready to be written to output.
136pub struct Report<'a, S: Span = Range<usize>> {
137    kind: ReportKind<'a>,
138    code: Option<String>,
139    msg: Option<String>,
140    note: Option<String>,
141    help: Option<String>,
142    location: (<S::SourceId as ToOwned>::Owned, usize),
143    labels: Vec<Label<S>>,
144    config: Config,
145}
146
147impl<S: Span> Report<'_, S> {
148    /// Begin building a new [`Report`].
149    pub fn build<Id: Into<<S::SourceId as ToOwned>::Owned>>(kind: ReportKind, src_id: Id, offset: usize) -> ReportBuilder<S> {
150        ReportBuilder {
151            kind,
152            code: None,
153            msg: None,
154            note: None,
155            help: None,
156            location: (src_id.into(), offset),
157            labels: Vec::new(),
158            config: Config::default(),
159        }
160    }
161
162    /// Write this diagnostic out to `stderr`.
163    pub fn eprint<C: Cache<S::SourceId>>(&self, cache: C) -> io::Result<()> {
164        self.write(cache, io::stderr())
165    }
166
167    /// Write this diagnostic out to `stdout`.
168    ///
169    /// In most cases, [`Report::eprint`] is the
170    /// ['more correct'](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)) function to use.
171    pub fn print<C: Cache<S::SourceId>>(&self, cache: C) -> io::Result<()> {
172        self.write_for_stdout(cache, io::stdout())
173    }
174}
175
176impl<'a, S: Span> fmt::Debug for Report<'a, S> {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        f.debug_struct("Report")
179            .field("kind", &self.kind)
180            .field("code", &self.code)
181            .field("msg", &self.msg)
182            .field("note", &self.note)
183            .field("help", &self.help)
184            .field("config", &self.config)
185            .finish()
186    }
187}
188/// A type that defines the kind of report being produced.
189#[derive(Copy, Clone, Debug, PartialEq, Eq)]
190pub enum ReportKind<'a> {
191    /// The report is an error and indicates a critical problem that prevents the program performing the requested
192    /// action.
193    Error,
194    /// The report is a warning and indicates a likely problem, but not to the extent that the requested action cannot
195    /// be performed.
196    Warning,
197    /// The report is advice to the user about a potential anti-pattern of other benign issues.
198    Advice,
199    /// The report is of a kind not built into Ariadne.
200    Custom(&'a str, Color),
201}
202
203impl fmt::Display for ReportKind<'_> {
204    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
205        match self {
206            ReportKind::Error => write!(f, "Error"),
207            ReportKind::Warning => write!(f, "Warning"),
208            ReportKind::Advice => write!(f, "Advice"),
209            ReportKind::Custom(s, _) => write!(f, "{}", s),
210        }
211    }
212}
213
214/// A type used to build a [`Report`].
215pub struct ReportBuilder<'a, S: Span> {
216    kind: ReportKind<'a>,
217    code: Option<String>,
218    msg: Option<String>,
219    note: Option<String>,
220    help: Option<String>,
221    location: (<S::SourceId as ToOwned>::Owned, usize),
222    labels: Vec<Label<S>>,
223    config: Config,
224}
225
226impl<'a, S: Span> ReportBuilder<'a, S> {
227    /// Give this report a numerical code that may be used to more precisely look up the error in documentation.
228    pub fn with_code<C: fmt::Display>(mut self, code: C) -> Self {
229        self.code = Some(format!("{:02}", code));
230        self
231    }
232
233    /// Set the message of this report.
234    pub fn set_message<M: ToString>(&mut self, msg: M) {
235        self.msg = Some(msg.to_string());
236    }
237
238    /// Add a message to this report.
239    pub fn with_message<M: ToString>(mut self, msg: M) -> Self {
240        self.msg = Some(msg.to_string());
241        self
242    }
243
244    /// Set the note of this report.
245    pub fn set_note<N: ToString>(&mut self, note: N) {
246        self.note = Some(note.to_string());
247    }
248
249    /// Set the note of this report.
250    pub fn with_note<N: ToString>(mut self, note: N) -> Self {
251        self.set_note(note);
252        self
253    }
254
255    /// Set the help message of this report.
256    pub fn set_help<N: ToString>(&mut self, note: N) {
257        self.help = Some(note.to_string());
258    }
259
260    /// Set the help message of this report.
261    pub fn with_help<N: ToString>(mut self, note: N) -> Self {
262        self.set_help(note);
263        self
264    }
265
266    /// Add a label to the report.
267    pub fn add_label(&mut self, label: Label<S>) {
268        self.add_labels(std::iter::once(label));
269    }
270
271    /// Add multiple labels to the report.
272    pub fn add_labels<L: IntoIterator<Item = Label<S>>>(&mut self, labels: L) {
273        let config = &self.config; // This would not be necessary in Rust 2021 edition
274        self.labels.extend(labels.into_iter().map(|mut label| { label.color = config.filter_color(label.color); label }));
275    }
276
277    /// Add a label to the report.
278    pub fn with_label(mut self, label: Label<S>) -> Self {
279        self.add_label(label);
280        self
281    }
282
283    /// Add multiple labels to the report.
284    pub fn with_labels<L: IntoIterator<Item = Label<S>>>(mut self, labels: L) -> Self {
285        self.add_labels(labels);
286        self
287    }
288
289    /// Use the given [`Config`] to determine diagnostic attributes.
290    pub fn with_config(mut self, config: Config) -> Self {
291        self.config = config;
292        self
293    }
294
295    /// Finish building the [`Report`].
296    pub fn finish(self) -> Report<'a, S> {
297        Report {
298            kind: self.kind,
299            code: self.code,
300            msg: self.msg,
301            note: self.note,
302            help: self.help,
303            location: self.location,
304            labels: self.labels,
305            config: self.config,
306        }
307    }
308}
309
310impl<'a, S: Span> fmt::Debug for ReportBuilder<'a, S> {
311    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312        f.debug_struct("ReportBuilder")
313            .field("kind", &self.kind)
314            .field("code", &self.code)
315            .field("msg", &self.msg)
316            .field("note", &self.note)
317            .field("help", &self.help)
318            .field("config", &self.config)
319            .finish()
320    }
321}
322
323/// The attachment point of inline label arrows
324#[derive(Copy, Clone, Debug, PartialEq, Eq)]
325pub enum LabelAttach {
326    /// Arrows should attach to the start of the label span.
327    Start,
328    /// Arrows should attach to the middle of the label span (or as close to the middle as we can get).
329    Middle,
330    /// Arrows should attach to the end of the label span.
331    End,
332}
333
334/// Possible character sets to use when rendering diagnostics.
335#[derive(Copy, Clone, Debug, PartialEq, Eq)]
336pub enum CharSet {
337    /// Unicode characters (an attempt is made to use only commonly-supported characters).
338    Unicode,
339    /// ASCII-only characters.
340    Ascii,
341}
342
343/// A type used to configure a report
344#[derive(Copy, Clone, Debug, PartialEq, Eq)]
345pub struct Config {
346    cross_gap: bool,
347    label_attach: LabelAttach,
348    compact: bool,
349    underlines: bool,
350    multiline_arrows: bool,
351    color: bool,
352    tab_width: usize,
353    char_set: CharSet,
354}
355
356impl Config {
357    /// When label lines cross one-another, should there be a gap?
358    ///
359    /// The alternative to this is to insert crossing characters. However, these interact poorly with label colours.
360    ///
361    /// If unspecified, this defaults to [`false`].
362    pub fn with_cross_gap(mut self, cross_gap: bool) -> Self { self.cross_gap = cross_gap; self }
363    /// Where should inline labels attach to their spans?
364    ///
365    /// If unspecified, this defaults to [`LabelAttach::Middle`].
366    pub fn with_label_attach(mut self, label_attach: LabelAttach) -> Self { self.label_attach = label_attach; self }
367    /// Should the report remove gaps to minimise used space?
368    ///
369    /// If unspecified, this defaults to [`false`].
370    pub fn with_compact(mut self, compact: bool) -> Self { self.compact = compact; self }
371    /// Should underlines be used for label span where possible?
372    ///
373    /// If unspecified, this defaults to [`true`].
374    pub fn with_underlines(mut self, underlines: bool) -> Self { self.underlines = underlines; self }
375    /// Should arrows be used to point to the bounds of multi-line spans?
376    ///
377    /// If unspecified, this defaults to [`true`].
378    pub fn with_multiline_arrows(mut self, multiline_arrows: bool) -> Self { self.multiline_arrows = multiline_arrows; self }
379    /// Should colored output should be enabled?
380    ///
381    /// If unspecified, this defaults to [`true`].
382    pub fn with_color(mut self, color: bool) -> Self { self.color = color; self }
383    /// How many characters width should tab characters be?
384    ///
385    /// If unspecified, this defaults to `4`.
386    pub fn with_tab_width(mut self, tab_width: usize) -> Self { self.tab_width = tab_width; self }
387    /// What character set should be used to display dynamic elements such as boxes and arrows?
388    ///
389    /// If unspecified, this defaults to [`CharSet::Unicode`].
390    pub fn with_char_set(mut self, char_set: CharSet) -> Self { self.char_set = char_set; self }
391
392    fn error_color(&self) -> Option<Color> { Some(Color::Red).filter(|_| self.color) }
393    fn warning_color(&self) -> Option<Color> { Some(Color::Yellow).filter(|_| self.color) }
394    fn advice_color(&self) -> Option<Color> { Some(Color::Fixed(147)).filter(|_| self.color) }
395    fn margin_color(&self) -> Option<Color> { Some(Color::Fixed(246)).filter(|_| self.color) }
396    fn skipped_margin_color(&self) -> Option<Color> { Some(Color::Fixed(240)).filter(|_| self.color) }
397    fn unimportant_color(&self) -> Option<Color> { Some(Color::Fixed(249)).filter(|_| self.color) }
398    fn note_color(&self) -> Option<Color> { Some(Color::Fixed(115)).filter(|_| self.color) }
399    fn filter_color(&self, color: Option<Color>) -> Option<Color> { color.filter(|_| self.color) }
400
401    // Find the character that should be drawn and the number of times it should be drawn for each char
402    fn char_width(&self, c: char, col: usize) -> (char, usize) {
403        match c {
404            '\t' => {
405                // Find the column that the tab should end at
406                let tab_end = (col / self.tab_width + 1) * self.tab_width;
407                (' ',  tab_end - col)
408            },
409            c if c.is_whitespace() => (' ', 1),
410            _ => (c, c.width().unwrap_or(1)),
411        }
412    }
413}
414
415impl Default for Config {
416    fn default() -> Self {
417        Self {
418            cross_gap: true,
419            label_attach: LabelAttach::Middle,
420            compact: false,
421            underlines: true,
422            multiline_arrows: true,
423            color: true,
424            tab_width: 4,
425            char_set: CharSet::Unicode,
426        }
427    }
428}