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
29pub trait Span {
31 type SourceId: PartialEq + ToOwned + ?Sized;
33
34 fn source(&self) -> &Self::SourceId;
36
37 fn start(&self) -> usize;
41
42 fn end(&self) -> usize;
48
49 fn len(&self) -> usize {
51 self.end().saturating_sub(self.start())
52 }
53
54 fn is_empty(&self) -> bool {
56 self.len() == 0
57 }
58
59 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#[derive(Clone, Debug, Hash, PartialEq, Eq)]
123pub struct LabelDisplay {
124 msg: Option<String>,
125 color: Option<Color>,
126 order: i32,
127 priority: i32,
128}
129
130impl LabelDisplay {
131 pub fn msg(&self) -> Option<&str> {
133 self.msg.as_deref()
134 }
135}
136
137#[derive(Clone, Debug, Hash, PartialEq, Eq)]
139pub struct Label<S = Range<usize>> {
140 span: S,
141 display_info: LabelDisplay,
142}
143
144impl<S: Span> Label<S> {
145 pub fn new(span: S) -> Self {
152 assert!(span.start() <= span.end(), "Label start is after its end");
153
154 Self {
155 span,
156 display_info: LabelDisplay {
157 msg: None,
158 color: None,
159 order: 0,
160 priority: 0,
161 },
162 }
163 }
164
165 pub fn with_message<M: ToString>(mut self, msg: M) -> Self {
167 self.display_info.msg = Some(msg.to_string());
168 self
169 }
170
171 pub fn with_color(mut self, color: Color) -> Self {
173 self.display_info.color = Some(color);
174 self
175 }
176
177 pub fn with_order(mut self, order: i32) -> Self {
188 self.display_info.order = order;
189 self
190 }
191
192 pub fn with_priority(mut self, priority: i32) -> Self {
202 self.display_info.priority = priority;
203 self
204 }
205
206 pub fn span(&self) -> &S {
208 &self.span
209 }
210
211 pub fn display_info(&self) -> &LabelDisplay {
213 &self.display_info
214 }
215}
216
217pub struct Report<'a, S: Span = Range<usize>> {
219 kind: ReportKind<'a>,
220 code: Option<String>,
221 msg: Option<String>,
222 notes: Vec<String>,
223 help: Vec<String>,
224 span: S,
225 labels: Vec<Label<S>>,
226 config: Config,
227}
228
229impl<S: Span> Report<'_, S> {
230 pub fn build(kind: ReportKind, span: S) -> ReportBuilder<S> {
234 ReportBuilder {
235 kind,
236 code: None,
237 msg: None,
238 notes: vec![],
239 help: vec![],
240 span,
241 labels: Vec::new(),
242 config: Config::default(),
243 }
244 }
245
246 pub fn eprint<C: Cache<S::SourceId>>(&self, cache: C) -> io::Result<()> {
248 self.write(cache, io::stderr())
249 }
250
251 pub fn print<C: Cache<S::SourceId>>(&self, cache: C) -> io::Result<()> {
256 self.write_for_stdout(cache, io::stdout())
257 }
258}
259
260impl<S: Span> fmt::Debug for Report<'_, S> {
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262 f.debug_struct("Report")
263 .field("kind", &self.kind)
264 .field("code", &self.code)
265 .field("msg", &self.msg)
266 .field("notes", &self.notes)
267 .field("help", &self.help)
268 .field("config", &self.config)
269 .finish()
270 }
271}
272#[derive(Copy, Clone, Debug, PartialEq, Eq)]
274pub enum ReportKind<'a> {
275 Error,
278 Warning,
281 Advice,
283 Custom(&'a str, Color),
285}
286
287impl fmt::Display for ReportKind<'_> {
288 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
289 match self {
290 ReportKind::Error => write!(f, "Error"),
291 ReportKind::Warning => write!(f, "Warning"),
292 ReportKind::Advice => write!(f, "Advice"),
293 ReportKind::Custom(s, _) => write!(f, "{}", s),
294 }
295 }
296}
297
298pub struct ReportBuilder<'a, S: Span> {
300 kind: ReportKind<'a>,
301 code: Option<String>,
302 msg: Option<String>,
303 notes: Vec<String>,
304 help: Vec<String>,
305 span: S,
306 labels: Vec<Label<S>>,
307 config: Config,
308}
309
310impl<'a, S: Span> ReportBuilder<'a, S> {
311 pub fn with_code<C: fmt::Display>(mut self, code: C) -> Self {
313 self.code = Some(format!("{:02}", code));
314 self
315 }
316
317 pub fn set_message<M: ToString>(&mut self, msg: M) {
319 self.msg = Some(msg.to_string());
320 }
321
322 pub fn with_message<M: ToString>(mut self, msg: M) -> Self {
324 self.msg = Some(msg.to_string());
325 self
326 }
327
328 pub fn set_note<N: ToString>(&mut self, note: N) {
330 self.notes = vec![note.to_string()];
331 }
332
333 pub fn add_note<N: ToString>(&mut self, note: N) {
335 self.notes.push(note.to_string());
336 }
337
338 pub fn with_notes<N: IntoIterator<Item = impl ToString>>(&mut self, notes: N) {
340 for note in notes {
341 self.add_note(note)
342 }
343 }
344
345 pub fn with_note<N: ToString>(mut self, note: N) -> Self {
347 self.add_note(note);
348 self
349 }
350
351 pub fn set_help<N: ToString>(&mut self, note: N) {
353 self.help = vec![note.to_string()];
354 }
355
356 pub fn add_help<N: ToString>(&mut self, note: N) {
358 self.help.push(note.to_string());
359 }
360
361 pub fn with_helps<N: IntoIterator<Item = impl ToString>>(&mut self, helps: N) {
363 for help in helps {
364 self.add_help(help)
365 }
366 }
367
368 pub fn with_help<N: ToString>(mut self, note: N) -> Self {
370 self.add_help(note);
371 self
372 }
373
374 pub fn add_label(&mut self, label: Label<S>) {
376 self.add_labels(std::iter::once(label));
377 }
378
379 pub fn add_labels<L: IntoIterator<Item = Label<S>>>(&mut self, labels: L) {
381 let config = &self.config; self.labels.extend(labels.into_iter().map(|mut label| {
383 label.display_info.color = config.filter_color(label.display_info.color);
384 label
385 }));
386 }
387
388 pub fn with_label(mut self, label: Label<S>) -> Self {
390 self.add_label(label);
391 self
392 }
393
394 pub fn with_labels<L: IntoIterator<Item = Label<S>>>(mut self, labels: L) -> Self {
396 self.add_labels(labels);
397 self
398 }
399
400 pub fn with_config(mut self, config: Config) -> Self {
402 self.config = config;
403 self
404 }
405
406 pub fn finish(self) -> Report<'a, S> {
408 Report {
409 kind: self.kind,
410 code: self.code,
411 msg: self.msg,
412 notes: self.notes,
413 help: self.help,
414 span: self.span,
415 labels: self.labels,
416 config: self.config,
417 }
418 }
419}
420
421impl<S: Span> fmt::Debug for ReportBuilder<'_, S> {
422 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423 f.debug_struct("ReportBuilder")
424 .field("kind", &self.kind)
425 .field("code", &self.code)
426 .field("msg", &self.msg)
427 .field("notes", &self.notes)
428 .field("help", &self.help)
429 .field("config", &self.config)
430 .finish()
431 }
432}
433
434#[derive(Copy, Clone, Debug, PartialEq, Eq)]
436pub enum LabelAttach {
437 Start,
439 Middle,
441 End,
443}
444
445#[derive(Copy, Clone, Debug, PartialEq, Eq)]
447pub enum CharSet {
448 Unicode,
450 Ascii,
452}
453
454#[derive(Copy, Clone, Debug, PartialEq, Eq)]
456pub enum IndexType {
457 Byte,
459 Char,
461}
462
463#[derive(Copy, Clone, Debug, PartialEq, Eq)]
465pub struct Config {
466 cross_gap: bool,
467 label_attach: LabelAttach,
468 compact: bool,
469 underlines: bool,
470 multiline_arrows: bool,
471 color: bool,
472 tab_width: usize,
473 char_set: CharSet,
474 index_type: IndexType,
475 severity_prefix: bool,
476}
477
478impl Config {
479 pub const fn with_cross_gap(mut self, cross_gap: bool) -> Self {
485 self.cross_gap = cross_gap;
486 self
487 }
488 pub const fn with_label_attach(mut self, label_attach: LabelAttach) -> Self {
492 self.label_attach = label_attach;
493 self
494 }
495 pub const fn with_compact(mut self, compact: bool) -> Self {
499 self.compact = compact;
500 self
501 }
502 pub const fn with_underlines(mut self, underlines: bool) -> Self {
506 self.underlines = underlines;
507 self
508 }
509 pub const fn with_multiline_arrows(mut self, multiline_arrows: bool) -> Self {
513 self.multiline_arrows = multiline_arrows;
514 self
515 }
516 pub const fn with_color(mut self, color: bool) -> Self {
520 self.color = color;
521 self
522 }
523 pub const fn with_tab_width(mut self, tab_width: usize) -> Self {
527 self.tab_width = tab_width;
528 self
529 }
530 pub const fn with_char_set(mut self, char_set: CharSet) -> Self {
534 self.char_set = char_set;
535 self
536 }
537 pub const fn with_index_type(mut self, index_type: IndexType) -> Self {
541 self.index_type = index_type;
542 self
543 }
544 pub const fn with_severity_prefix(mut self, severity_prefix: bool) -> Self {
548 self.severity_prefix = severity_prefix;
549 self
550 }
551
552 fn error_color(&self) -> Option<Color> {
553 Some(Color::Red).filter(|_| self.color)
554 }
555 fn warning_color(&self) -> Option<Color> {
556 Some(Color::Yellow).filter(|_| self.color)
557 }
558 fn advice_color(&self) -> Option<Color> {
559 Some(Color::Fixed(147)).filter(|_| self.color)
560 }
561 fn margin_color(&self) -> Option<Color> {
562 Some(Color::Fixed(246)).filter(|_| self.color)
563 }
564 fn skipped_margin_color(&self) -> Option<Color> {
565 Some(Color::Fixed(240)).filter(|_| self.color)
566 }
567 fn unimportant_color(&self) -> Option<Color> {
568 Some(Color::Fixed(249)).filter(|_| self.color)
569 }
570 fn note_color(&self) -> Option<Color> {
571 Some(Color::Fixed(115)).filter(|_| self.color)
572 }
573 fn filter_color(&self, color: Option<Color>) -> Option<Color> {
574 color.filter(|_| self.color)
575 }
576
577 fn char_width(&self, c: char, col: usize) -> (char, usize) {
579 match c {
580 '\t' => {
581 let tab_end = (col / self.tab_width + 1) * self.tab_width;
583 (' ', tab_end - col)
584 }
585 c if c.is_whitespace() => (' ', 1),
586 _ => (c, c.width().unwrap_or(1)),
587 }
588 }
589
590 pub const fn new() -> Self {
592 Self {
593 cross_gap: true,
594 label_attach: LabelAttach::Middle,
595 compact: false,
596 underlines: true,
597 multiline_arrows: true,
598 color: true,
599 tab_width: 4,
600 char_set: CharSet::Unicode,
601 index_type: IndexType::Char,
602 severity_prefix: true,
603 }
604 }
605}
606
607impl Default for Config {
608 fn default() -> Self {
609 Self::new()
610 }
611}
612
613#[test]
614#[should_panic]
615#[allow(clippy::reversed_empty_ranges)]
616fn backwards_label_should_panic() {
617 Label::new(1..0);
618}