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