syntax_error/
lib.rs

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