ariadne/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
3
4mod display;
5mod draw;
6mod source;
7mod write;
8
9pub use crate::{
10    draw::{ColorGenerator, Fmt},
11    source::{sources, Cache, FileCache, FnCache, Line, Source},
12};
13pub use yansi::Color;
14
15#[cfg(any(feature = "concolor", doc))]
16pub use crate::draw::StdoutFmt;
17
18use crate::display::*;
19use std::{
20    cmp::{Eq, PartialEq},
21    fmt,
22    hash::Hash,
23    io::{self, Write},
24    ops::Range,
25    ops::RangeInclusive,
26};
27use unicode_width::UnicodeWidthChar;
28
29/// A trait implemented by spans within a character-based source.
30pub trait Span {
31    /// The identifier used to uniquely refer to a source. In most cases, this is the fully-qualified path of the file.
32    type SourceId: PartialEq + ToOwned + ?Sized;
33
34    /// Get the identifier of the source that this span refers to.
35    fn source(&self) -> &Self::SourceId;
36
37    /// Get the start offset of this span.
38    ///
39    /// Offsets are zero-indexed character offsets from the beginning of the source.
40    fn start(&self) -> usize;
41
42    /// Get the (exclusive) end offset of this span.
43    ///
44    /// The end offset should *always* be greater than or equal to the start offset as given by [`Span::start`].
45    ///
46    /// Offsets are zero-indexed character offsets from the beginning of the source.
47    fn end(&self) -> usize;
48
49    /// Get the length of this span (difference between the start of the span and the end of the span).
50    fn len(&self) -> usize {
51        self.end().saturating_sub(self.start())
52    }
53
54    /// Returns `true` if this span has length zero.
55    fn is_empty(&self) -> bool {
56        self.len() == 0
57    }
58
59    /// Determine whether the span contains the given offset.
60    fn contains(&self, offset: usize) -> bool {
61        (self.start()..self.end()).contains(&offset)
62    }
63}
64
65impl Span for Range<usize> {
66    type SourceId = ();
67
68    fn source(&self) -> &Self::SourceId {
69        &()
70    }
71    fn start(&self) -> usize {
72        self.start
73    }
74    fn end(&self) -> usize {
75        self.end
76    }
77}
78
79impl<Id: fmt::Debug + Hash + PartialEq + Eq + ToOwned> Span for (Id, Range<usize>) {
80    type SourceId = Id;
81
82    fn source(&self) -> &Self::SourceId {
83        &self.0
84    }
85    fn start(&self) -> usize {
86        self.1.start
87    }
88    fn end(&self) -> usize {
89        self.1.end
90    }
91}
92
93impl Span for RangeInclusive<usize> {
94    type SourceId = ();
95
96    fn source(&self) -> &Self::SourceId {
97        &()
98    }
99    fn start(&self) -> usize {
100        *self.start()
101    }
102    fn end(&self) -> usize {
103        *self.end() + 1
104    }
105}
106
107impl<Id: fmt::Debug + Hash + PartialEq + Eq + ToOwned> Span for (Id, RangeInclusive<usize>) {
108    type SourceId = Id;
109
110    fn source(&self) -> &Self::SourceId {
111        &self.0
112    }
113    fn start(&self) -> usize {
114        *self.1.start()
115    }
116    fn end(&self) -> usize {
117        *self.1.end() + 1
118    }
119}
120
121/// A type that represents the way a label should be displayed.
122#[derive(Clone, Debug, Hash, PartialEq, Eq)]
123struct LabelDisplay {
124    msg: Option<String>,
125    color: Option<Color>,
126    order: i32,
127    priority: i32,
128}
129
130/// A type that represents a labelled section of source code.
131#[derive(Clone, Debug, Hash, PartialEq, Eq)]
132pub struct Label<S = Range<usize>> {
133    span: S,
134    display_info: LabelDisplay,
135}
136
137impl<S: Span> Label<S> {
138    /// Create a new [`Label`].
139    /// If the span is specified as a `Range<usize>` the numbers have to be zero-indexed character offsets.
140    ///
141    /// # Panics
142    ///
143    /// Panics if the given span is backwards.
144    pub fn new(span: S) -> Self {
145        assert!(span.start() <= span.end(), "Label start is after its end");
146
147        Self {
148            span,
149            display_info: LabelDisplay {
150                msg: None,
151                color: None,
152                order: 0,
153                priority: 0,
154            },
155        }
156    }
157
158    /// Give this label a message.
159    pub fn with_message<M: ToString>(mut self, msg: M) -> Self {
160        self.display_info.msg = Some(msg.to_string());
161        self
162    }
163
164    /// Give this label a highlight colour.
165    pub fn with_color(mut self, color: Color) -> Self {
166        self.display_info.color = Some(color);
167        self
168    }
169
170    /// Specify the order of this label relative to other labels.
171    ///
172    /// Lower values correspond to this label having an earlier order. Labels with the same order will be arranged
173    /// in whatever order best suits their spans and layout constraints.
174    ///
175    /// If unspecified, labels default to an order of `0`.
176    ///
177    /// Label order is respected across files. If labels that appear earlier in a file have an order that requires
178    /// them to appear later, the resulting diagnostic may result in multiple instances of the same file being
179    /// displayed.
180    pub fn with_order(mut self, order: i32) -> Self {
181        self.display_info.order = order;
182        self
183    }
184
185    /// Specify the priority of this label relative to other labels.
186    ///
187    /// Higher values correspond to this label having a higher priority.
188    ///
189    /// If unspecified, labels default to a priority of `0`.
190    ///
191    /// Label spans can overlap. When this happens, the crate needs to decide which labels to prioritise for various
192    /// purposes such as highlighting. By default, spans with a smaller length get a higher priority. You can use this
193    /// function to override this behaviour.
194    pub fn with_priority(mut self, priority: i32) -> Self {
195        self.display_info.priority = priority;
196        self
197    }
198}
199
200/// A type representing a diagnostic that is ready to be written to output.
201pub struct Report<'a, S: Span = Range<usize>> {
202    kind: ReportKind<'a>,
203    code: Option<String>,
204    msg: Option<String>,
205    notes: Vec<String>,
206    help: Vec<String>,
207    span: S,
208    labels: Vec<Label<S>>,
209    config: Config,
210}
211
212impl<S: Span> Report<'_, S> {
213    /// Begin building a new [`Report`].
214    ///
215    /// The span is the primary location at which the error should be reported.
216    pub fn build(kind: ReportKind, span: S) -> ReportBuilder<S> {
217        ReportBuilder {
218            kind,
219            code: None,
220            msg: None,
221            notes: vec![],
222            help: vec![],
223            span,
224            labels: Vec::new(),
225            config: Config::default(),
226        }
227    }
228
229    /// Write this diagnostic out to `stderr`.
230    pub fn eprint<C: Cache<S::SourceId>>(&self, cache: C) -> io::Result<()> {
231        self.write(cache, io::stderr())
232    }
233
234    /// Write this diagnostic out to `stdout`.
235    ///
236    /// In most cases, [`Report::eprint`] is the
237    /// ['more correct'](https://en.wikipedia.org/wiki/Standard_streams#Standard_error_(stderr)) function to use.
238    pub fn print<C: Cache<S::SourceId>>(&self, cache: C) -> io::Result<()> {
239        self.write_for_stdout(cache, io::stdout())
240    }
241}
242
243impl<'a, S: Span> fmt::Debug for Report<'a, S> {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        f.debug_struct("Report")
246            .field("kind", &self.kind)
247            .field("code", &self.code)
248            .field("msg", &self.msg)
249            .field("notes", &self.notes)
250            .field("help", &self.help)
251            .field("config", &self.config)
252            .finish()
253    }
254}
255/// A type that defines the kind of report being produced.
256#[derive(Copy, Clone, Debug, PartialEq, Eq)]
257pub enum ReportKind<'a> {
258    /// The report is an error and indicates a critical problem that prevents the program performing the requested
259    /// action.
260    Error,
261    /// The report is a warning and indicates a likely problem, but not to the extent that the requested action cannot
262    /// be performed.
263    Warning,
264    /// The report is advice to the user about a potential anti-pattern of other benign issues.
265    Advice,
266    /// The report is of a kind not built into Ariadne.
267    Custom(&'a str, Color),
268}
269
270impl fmt::Display for ReportKind<'_> {
271    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
272        match self {
273            ReportKind::Error => write!(f, "Error"),
274            ReportKind::Warning => write!(f, "Warning"),
275            ReportKind::Advice => write!(f, "Advice"),
276            ReportKind::Custom(s, _) => write!(f, "{}", s),
277        }
278    }
279}
280
281/// A type used to build a [`Report`].
282pub struct ReportBuilder<'a, S: Span> {
283    kind: ReportKind<'a>,
284    code: Option<String>,
285    msg: Option<String>,
286    notes: Vec<String>,
287    help: Vec<String>,
288    span: S,
289    labels: Vec<Label<S>>,
290    config: Config,
291}
292
293impl<'a, S: Span> ReportBuilder<'a, S> {
294    /// Give this report a numerical code that may be used to more precisely look up the error in documentation.
295    pub fn with_code<C: fmt::Display>(mut self, code: C) -> Self {
296        self.code = Some(format!("{:02}", code));
297        self
298    }
299
300    /// Set the message of this report.
301    pub fn set_message<M: ToString>(&mut self, msg: M) {
302        self.msg = Some(msg.to_string());
303    }
304
305    /// Add a message to this report.
306    pub fn with_message<M: ToString>(mut self, msg: M) -> Self {
307        self.msg = Some(msg.to_string());
308        self
309    }
310
311    /// Set the note of this report.
312    pub fn set_note<N: ToString>(&mut self, note: N) {
313        self.notes = vec![note.to_string()];
314    }
315
316    /// Adds a note to this report.
317    pub fn add_note<N: ToString>(&mut self, note: N) {
318        self.notes.push(note.to_string());
319    }
320
321    /// Removes all notes in this report.
322    pub fn with_notes<N: IntoIterator<Item = impl ToString>>(&mut self, notes: N) {
323        for note in notes {
324            self.add_note(note)
325        }
326    }
327
328    /// Set the note of this report.
329    pub fn with_note<N: ToString>(mut self, note: N) -> Self {
330        self.add_note(note);
331        self
332    }
333
334    /// Set the help message of this report.
335    pub fn set_help<N: ToString>(&mut self, note: N) {
336        self.help = vec![note.to_string()];
337    }
338
339    /// Add a help message to this report.
340    pub fn add_help<N: ToString>(&mut self, note: N) {
341        self.help.push(note.to_string());
342    }
343
344    /// Set the help messages of this report.
345    pub fn with_helps<N: IntoIterator<Item = impl ToString>>(&mut self, helps: N) {
346        for help in helps {
347            self.add_help(help)
348        }
349    }
350
351    /// Set the help message of this report.
352    pub fn with_help<N: ToString>(mut self, note: N) -> Self {
353        self.add_help(note);
354        self
355    }
356
357    /// Add a label to the report.
358    pub fn add_label(&mut self, label: Label<S>) {
359        self.add_labels(std::iter::once(label));
360    }
361
362    /// Add multiple labels to the report.
363    pub fn add_labels<L: IntoIterator<Item = Label<S>>>(&mut self, labels: L) {
364        let config = &self.config; // This would not be necessary in Rust 2021 edition
365        self.labels.extend(labels.into_iter().map(|mut label| {
366            label.display_info.color = config.filter_color(label.display_info.color);
367            label
368        }));
369    }
370
371    /// Add a label to the report.
372    pub fn with_label(mut self, label: Label<S>) -> Self {
373        self.add_label(label);
374        self
375    }
376
377    /// Add multiple labels to the report.
378    pub fn with_labels<L: IntoIterator<Item = Label<S>>>(mut self, labels: L) -> Self {
379        self.add_labels(labels);
380        self
381    }
382
383    /// Use the given [`Config`] to determine diagnostic attributes.
384    pub fn with_config(mut self, config: Config) -> Self {
385        self.config = config;
386        self
387    }
388
389    /// Finish building the [`Report`].
390    pub fn finish(self) -> Report<'a, S> {
391        Report {
392            kind: self.kind,
393            code: self.code,
394            msg: self.msg,
395            notes: self.notes,
396            help: self.help,
397            span: self.span,
398            labels: self.labels,
399            config: self.config,
400        }
401    }
402}
403
404impl<'a, S: Span> fmt::Debug for ReportBuilder<'a, S> {
405    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406        f.debug_struct("ReportBuilder")
407            .field("kind", &self.kind)
408            .field("code", &self.code)
409            .field("msg", &self.msg)
410            .field("notes", &self.notes)
411            .field("help", &self.help)
412            .field("config", &self.config)
413            .finish()
414    }
415}
416
417/// The attachment point of inline label arrows
418#[derive(Copy, Clone, Debug, PartialEq, Eq)]
419pub enum LabelAttach {
420    /// Arrows should attach to the start of the label span.
421    Start,
422    /// Arrows should attach to the middle of the label span (or as close to the middle as we can get).
423    Middle,
424    /// Arrows should attach to the end of the label span.
425    End,
426}
427
428/// Possible character sets to use when rendering diagnostics.
429#[derive(Copy, Clone, Debug, PartialEq, Eq)]
430pub enum CharSet {
431    /// Unicode characters (an attempt is made to use only commonly-supported characters).
432    Unicode,
433    /// ASCII-only characters.
434    Ascii,
435}
436
437/// Possible character sets to use when rendering diagnostics.
438#[derive(Copy, Clone, Debug, PartialEq, Eq)]
439pub enum IndexType {
440    /// Byte spans. Always results in O(1) lookups
441    Byte,
442    /// Char based spans. May incur O(n) lookups
443    Char,
444}
445
446/// A type used to configure a report
447#[derive(Copy, Clone, Debug, PartialEq, Eq)]
448pub struct Config {
449    cross_gap: bool,
450    label_attach: LabelAttach,
451    compact: bool,
452    underlines: bool,
453    multiline_arrows: bool,
454    color: bool,
455    tab_width: usize,
456    char_set: CharSet,
457    index_type: IndexType,
458}
459
460impl Config {
461    /// When label lines cross one-another, should there be a gap?
462    ///
463    /// The alternative to this is to insert crossing characters. However, these interact poorly with label colours.
464    ///
465    /// If unspecified, this defaults to [`false`].
466    pub const fn with_cross_gap(mut self, cross_gap: bool) -> Self {
467        self.cross_gap = cross_gap;
468        self
469    }
470    /// Where should inline labels attach to their spans?
471    ///
472    /// If unspecified, this defaults to [`LabelAttach::Middle`].
473    pub const fn with_label_attach(mut self, label_attach: LabelAttach) -> Self {
474        self.label_attach = label_attach;
475        self
476    }
477    /// Should the report remove gaps to minimise used space?
478    ///
479    /// If unspecified, this defaults to [`false`].
480    pub const fn with_compact(mut self, compact: bool) -> Self {
481        self.compact = compact;
482        self
483    }
484    /// Should underlines be used for label span where possible?
485    ///
486    /// If unspecified, this defaults to [`true`].
487    pub const fn with_underlines(mut self, underlines: bool) -> Self {
488        self.underlines = underlines;
489        self
490    }
491    /// Should arrows be used to point to the bounds of multi-line spans?
492    ///
493    /// If unspecified, this defaults to [`true`].
494    pub const fn with_multiline_arrows(mut self, multiline_arrows: bool) -> Self {
495        self.multiline_arrows = multiline_arrows;
496        self
497    }
498    /// Should colored output should be enabled?
499    ///
500    /// If unspecified, this defaults to [`true`].
501    pub const fn with_color(mut self, color: bool) -> Self {
502        self.color = color;
503        self
504    }
505    /// How many characters width should tab characters be?
506    ///
507    /// If unspecified, this defaults to `4`.
508    pub const fn with_tab_width(mut self, tab_width: usize) -> Self {
509        self.tab_width = tab_width;
510        self
511    }
512    /// What character set should be used to display dynamic elements such as boxes and arrows?
513    ///
514    /// If unspecified, this defaults to [`CharSet::Unicode`].
515    pub const fn with_char_set(mut self, char_set: CharSet) -> Self {
516        self.char_set = char_set;
517        self
518    }
519    /// Should this report use byte spans instead of char spans?
520    ///
521    /// If unspecified, this defaults to 'false'
522    pub const fn with_index_type(mut self, index_type: IndexType) -> Self {
523        self.index_type = index_type;
524        self
525    }
526
527    fn error_color(&self) -> Option<Color> {
528        Some(Color::Red).filter(|_| self.color)
529    }
530    fn warning_color(&self) -> Option<Color> {
531        Some(Color::Yellow).filter(|_| self.color)
532    }
533    fn advice_color(&self) -> Option<Color> {
534        Some(Color::Fixed(147)).filter(|_| self.color)
535    }
536    fn margin_color(&self) -> Option<Color> {
537        Some(Color::Fixed(246)).filter(|_| self.color)
538    }
539    fn skipped_margin_color(&self) -> Option<Color> {
540        Some(Color::Fixed(240)).filter(|_| self.color)
541    }
542    fn unimportant_color(&self) -> Option<Color> {
543        Some(Color::Fixed(249)).filter(|_| self.color)
544    }
545    fn note_color(&self) -> Option<Color> {
546        Some(Color::Fixed(115)).filter(|_| self.color)
547    }
548    fn filter_color(&self, color: Option<Color>) -> Option<Color> {
549        color.filter(|_| self.color)
550    }
551
552    // Find the character that should be drawn and the number of times it should be drawn for each char
553    fn char_width(&self, c: char, col: usize) -> (char, usize) {
554        match c {
555            '\t' => {
556                // Find the column that the tab should end at
557                let tab_end = (col / self.tab_width + 1) * self.tab_width;
558                (' ', tab_end - col)
559            }
560            c if c.is_whitespace() => (' ', 1),
561            _ => (c, c.width().unwrap_or(1)),
562        }
563    }
564
565    /// Create a new, default config.
566    pub const fn new() -> Self {
567        Self {
568            cross_gap: true,
569            label_attach: LabelAttach::Middle,
570            compact: false,
571            underlines: true,
572            multiline_arrows: true,
573            color: true,
574            tab_width: 4,
575            char_set: CharSet::Unicode,
576            index_type: IndexType::Char,
577        }
578    }
579}
580
581impl Default for Config {
582    fn default() -> Self {
583        Self::new()
584    }
585}
586
587#[test]
588#[should_panic]
589#[allow(clippy::reversed_empty_ranges)]
590fn backwards_label_should_panic() {
591    Label::new(1..0);
592}