From 4695028fa7f36d495f22c448d50a7b4f6801d230 Mon Sep 17 00:00:00 2001 From: Patrik Lindberg Date: Sun, 15 Dec 2024 22:16:48 +0100 Subject: [PATCH] Represent different kinds of reports using a trait instead of an enum, avoiding the extra lifetime to support custom report kinds. --- README.md | 2 +- examples/multifile.rs | 4 +- examples/multiline.rs | 4 +- examples/simple.rs | 6 +-- examples/stresstest.rs | 4 +- src/lib.rs | 95 +++++++++++++++++++++++++----------------- src/source.rs | 20 ++++----- src/write.rs | 61 +++++++++++++-------------- 8 files changed, 103 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index e5c332b..2dfbe28 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ fn main() { let b = colors.next(); let out = Color::Fixed(81); - Report::build(ReportKind::Error, ("sample.tao", 12..12)) + Report::build(ErrorKind, ("sample.tao", 12..12)) .with_code(3) .with_message(format!("Incompatible types")) .with_label( diff --git a/examples/multifile.rs b/examples/multifile.rs index 711c58d..afeb312 100644 --- a/examples/multifile.rs +++ b/examples/multifile.rs @@ -1,4 +1,4 @@ -use ariadne::{sources, ColorGenerator, Fmt, Label, Report, ReportKind}; +use ariadne::{sources, ColorGenerator, ErrorKind, Fmt, Label, Report}; fn main() { let mut colors = ColorGenerator::new(); @@ -8,7 +8,7 @@ fn main() { let b = colors.next(); let c = colors.next(); - Report::build(ReportKind::Error, ("b.tao", 10..14)) + Report::build(ErrorKind, ("b.tao", 10..14)) .with_code(3) .with_message("Cannot add types Nat and Str".to_string()) .with_label( diff --git a/examples/multiline.rs b/examples/multiline.rs index f5e94a1..0e779da 100644 --- a/examples/multiline.rs +++ b/examples/multiline.rs @@ -1,4 +1,4 @@ -use ariadne::{Color, ColorGenerator, Fmt, Label, Report, ReportKind, Source}; +use ariadne::{Color, ColorGenerator, ErrorKind, Fmt, Label, Report, Source}; fn main() { let mut colors = ColorGenerator::new(); @@ -9,7 +9,7 @@ fn main() { let out = Color::Fixed(81); let out2 = colors.next(); - Report::build(ReportKind::Error, ("sample.tao", 32..33)) + Report::build(ErrorKind, ("sample.tao", 32..33)) .with_code(3) .with_message("Incompatible types".to_string()) .with_label( diff --git a/examples/simple.rs b/examples/simple.rs index 2ed5ef0..55928d8 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,7 +1,7 @@ -use ariadne::{Color, Config, Label, Report, ReportKind, Source}; +use ariadne::{Color, Config, ErrorKind, Label, Report, Source}; fn main() { - Report::build(ReportKind::Error, 34..34) + Report::build(ErrorKind, 34..34) .with_message("Incompatible types") .with_label(Label::new(32..33).with_message("This is of type Nat")) .with_label(Label::new(42..45).with_message("This is of type Str")) @@ -11,7 +11,7 @@ fn main() { const SOURCE: &str = "a b c d e f"; // also supports labels with no messages to only emphasis on some areas - Report::build(ReportKind::Error, 2..3) + Report::build(ErrorKind, 2..3) .with_message("Incompatible types") .with_config(Config::default().with_compact(true)) .with_label(Label::new(0..1).with_color(Color::Red)) diff --git a/examples/stresstest.rs b/examples/stresstest.rs index 798e236..f31e44e 100644 --- a/examples/stresstest.rs +++ b/examples/stresstest.rs @@ -1,9 +1,9 @@ -use ariadne::{Color, ColorGenerator, Config, Label, Report, ReportKind, Source}; +use ariadne::{Color, ColorGenerator, Config, ErrorKind, Label, Report, Source}; fn main() { let mut colors = ColorGenerator::new(); - Report::build(ReportKind::Error, ("stresstest.tao", 13..13)) + Report::build(ErrorKind, ("stresstest.tao", 13..13)) .with_code(3) .with_message("Incompatible types".to_string()) .with_label( diff --git a/src/lib.rs b/src/lib.rs index ededa5f..35661e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,7 @@ pub use yansi::Color; pub use crate::draw::StdoutFmt; use crate::display::*; +use std::fmt::Debug; use std::{ cmp::{Eq, PartialEq}, fmt, @@ -198,8 +199,8 @@ impl Label { } /// A type representing a diagnostic that is ready to be written to output. -pub struct Report<'a, S: Span = Range> { - kind: ReportKind<'a>, +pub struct Report> { + kind: K, code: Option, msg: Option, notes: Vec, @@ -209,11 +210,11 @@ pub struct Report<'a, S: Span = Range> { config: Config, } -impl Report<'_, S> { +impl Report { /// Begin building a new [`Report`]. /// /// The span is the primary location at which the error should be reported. - pub fn build(kind: ReportKind, span: S) -> ReportBuilder { + pub fn build(kind: K, span: S) -> ReportBuilder { ReportBuilder { kind, code: None, @@ -240,7 +241,7 @@ impl Report<'_, S> { } } -impl<'a, S: Span> fmt::Debug for Report<'a, S> { +impl fmt::Debug for Report { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Report") .field("kind", &self.kind) @@ -252,35 +253,60 @@ impl<'a, S: Span> fmt::Debug for Report<'a, S> { .finish() } } + /// A type that defines the kind of report being produced. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum ReportKind<'a> { - /// The report is an error and indicates a critical problem that prevents the program performing the requested - /// action. - Error, - /// The report is a warning and indicates a likely problem, but not to the extent that the requested action cannot - /// be performed. - Warning, - /// The report is advice to the user about a potential anti-pattern of other benign issues. - Advice, - /// The report is of a kind not built into Ariadne. - Custom(&'a str, Color), +pub trait ReportKind { + /// The name of the report kind. This will be displayed in the output. + fn name(&self) -> &str; + + /// The color that should be used for reports of this kind. + fn color(&self) -> Color; } -impl fmt::Display for ReportKind<'_> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ReportKind::Error => write!(f, "Error"), - ReportKind::Warning => write!(f, "Warning"), - ReportKind::Advice => write!(f, "Advice"), - ReportKind::Custom(s, _) => write!(f, "{}", s), - } +/// The report is an error and indicates a critical problem that prevents the program performing the requested +/// action. +pub struct ErrorKind; + +impl ReportKind for ErrorKind { + fn name(&self) -> &str { + "Error" + } + + fn color(&self) -> Color { + Color::Red + } +} + +/// The report is a warning and indicates a likely problem, but not to the extent that the requested action cannot +/// be performed. +pub struct WarningKind; + +impl ReportKind for WarningKind { + fn name(&self) -> &str { + "Warning" + } + + fn color(&self) -> Color { + Color::Yellow + } +} + +/// The report is advice to the user about a potential anti-pattern of other benign issues. +pub struct AdviceKind; + +impl ReportKind for AdviceKind { + fn name(&self) -> &str { + "Advice" + } + + fn color(&self) -> Color { + Color::Fixed(147) } } /// A type used to build a [`Report`]. -pub struct ReportBuilder<'a, S: Span> { - kind: ReportKind<'a>, +pub struct ReportBuilder { + kind: K, code: Option, msg: Option, notes: Vec, @@ -290,7 +316,7 @@ pub struct ReportBuilder<'a, S: Span> { config: Config, } -impl<'a, S: Span> ReportBuilder<'a, S> { +impl ReportBuilder { /// Give this report a numerical code that may be used to more precisely look up the error in documentation. pub fn with_code(mut self, code: C) -> Self { self.code = Some(format!("{:02}", code)); @@ -375,7 +401,7 @@ impl<'a, S: Span> ReportBuilder<'a, S> { } /// Finish building the [`Report`]. - pub fn finish(self) -> Report<'a, S> { + pub fn finish(self) -> Report { Report { kind: self.kind, code: self.code, @@ -389,7 +415,7 @@ impl<'a, S: Span> ReportBuilder<'a, S> { } } -impl<'a, S: Span> fmt::Debug for ReportBuilder<'a, S> { +impl fmt::Debug for ReportBuilder { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("ReportBuilder") .field("kind", &self.kind) @@ -512,15 +538,6 @@ impl Config { self } - fn error_color(&self) -> Option { - Some(Color::Red).filter(|_| self.color) - } - fn warning_color(&self) -> Option { - Some(Color::Yellow).filter(|_| self.color) - } - fn advice_color(&self) -> Option { - Some(Color::Fixed(147)).filter(|_| self.color) - } fn margin_color(&self) -> Option { Some(Color::Fixed(246)).filter(|_| self.color) } diff --git a/src/source.rs b/src/source.rs index acae6a7..4e48629 100644 --- a/src/source.rs +++ b/src/source.rs @@ -1,11 +1,11 @@ use super::*; +use std::io::Error; use std::{ collections::{hash_map::Entry, HashMap}, fs, path::{Path, PathBuf}, }; -use std::io::Error; /// A trait implemented by [`Source`] caches. pub trait Cache { @@ -332,9 +332,7 @@ impl Cache for FileCache { Ok::<_, Error>(match self.files.entry(path.to_path_buf()) { // TODO: Don't allocate here Entry::Occupied(entry) => entry.into_mut(), - Entry::Vacant(entry) => entry.insert(Source::from( - fs::read_to_string(path)?, - )), + Entry::Vacant(entry) => entry.insert(Source::from(fs::read_to_string(path)?)), }) } fn display<'a>(&self, path: &'a Path) -> Option { @@ -403,14 +401,12 @@ where I: IntoIterator, S: AsRef, { - FnCache::new( - (move |id| Err(format!("Failed to fetch source '{}'", id))) as fn(&_) -> _, - ) - .with_sources( - iter.into_iter() - .map(|(id, s)| (id, Source::from(s))) - .collect(), - ) + FnCache::new((move |id| Err(format!("Failed to fetch source '{}'", id))) as fn(&_) -> _) + .with_sources( + iter.into_iter() + .map(|(id, s)| (id, Source::from(s))) + .collect(), + ) } #[cfg(test)] diff --git a/src/write.rs b/src/write.rs index ee7b1d7..fdc3888 100644 --- a/src/write.rs +++ b/src/write.rs @@ -40,7 +40,7 @@ struct SourceGroup<'a, S: Span> { labels: Vec>, } -impl Report<'_, S> { +impl Report { fn get_source_groups(&self, cache: &mut impl Cache) -> Vec> { let mut groups = Vec::new(); for label in self.labels.iter() { @@ -170,13 +170,8 @@ impl Report<'_, S> { // --- Header --- let code = self.code.as_ref().map(|c| format!("[{}] ", c)); - let id = format!("{}{}:", Show(code), self.kind); - let kind_color = match self.kind { - ReportKind::Error => self.config.error_color(), - ReportKind::Warning => self.config.warning_color(), - ReportKind::Advice => self.config.advice_color(), - ReportKind::Custom(_, color) => Some(color), - }; + let id = format!("{}{}:", Show(code), self.kind.name()); + let kind_color = self.config.color.then_some(self.kind.color()); writeln!(w, "{} {}", id.fg(kind_color, s), Show(self.msg.as_ref()))?; let groups = self.get_source_groups(&mut cache); @@ -932,9 +927,11 @@ mod tests { use insta::assert_snapshot; - use crate::{Cache, CharSet, Config, IndexType, Label, Report, ReportKind, Source, Span}; + use crate::{ + Cache, CharSet, Config, ErrorKind, IndexType, Label, Report, ReportKind, Source, Span, + }; - impl Report<'_, S> { + impl Report { fn write_to_string>(&self, cache: C) -> String { let mut vec = Vec::new(); self.write(cache, &mut vec).unwrap(); @@ -957,7 +954,7 @@ mod tests { #[test] fn one_message() { let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii()) .with_message("can't compare apples with oranges") .finish() @@ -972,7 +969,7 @@ mod tests { fn two_labels_without_messages() { let source = "apple == orange;"; let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii()) .with_message("can't compare apples with oranges") .with_label(Label::new(0..5)) @@ -994,7 +991,7 @@ mod tests { fn two_labels_with_messages() { let source = "apple == orange;"; let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii()) .with_message("can't compare apples with oranges") .with_label(Label::new(0..5).with_message("This is an apple")) @@ -1020,7 +1017,7 @@ mod tests { fn multi_byte_chars() { let source = "äpplë == örängë;"; let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii().with_index_type(IndexType::Char)) .with_message("can't compare äpplës with örängës") .with_label(Label::new(0..5).with_message("This is an äpplë")) @@ -1046,7 +1043,7 @@ mod tests { fn byte_label() { let source = "äpplë == örängë;"; let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii().with_index_type(IndexType::Byte)) .with_message("can't compare äpplës with örängës") .with_label(Label::new(0..7).with_message("This is an äpplë")) @@ -1072,7 +1069,7 @@ mod tests { fn byte_column() { let source = "äpplë == örängë;"; let msg = remove_trailing( - Report::build(ReportKind::Error, 11..11) + Report::build(ErrorKind, 11..11) .with_config(no_color_and_ascii().with_index_type(IndexType::Byte)) .with_message("can't compare äpplës with örängës") .with_label(Label::new(0..7).with_message("This is an äpplë")) @@ -1098,7 +1095,7 @@ mod tests { fn label_at_end_of_long_line() { let source = format!("{}orange", "apple == ".repeat(100)); let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii()) .with_message("can't compare apples with oranges") .with_label( @@ -1123,7 +1120,7 @@ mod tests { fn label_of_width_zero_at_end_of_line() { let source = "apple ==\n"; let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii().with_index_type(IndexType::Byte)) .with_message("unexpected end of file") .with_label(Label::new(9..9).with_message("Unexpected end of file")) @@ -1146,7 +1143,7 @@ mod tests { fn empty_input() { let source = ""; let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii()) .with_message("unexpected end of file") .with_label(Label::new(0..0).with_message("No more fruit!")) @@ -1169,7 +1166,7 @@ mod tests { fn empty_input_help() { let source = ""; let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii()) .with_message("unexpected end of file") .with_label(Label::new(0..0).with_message("No more fruit!")) @@ -1195,7 +1192,7 @@ mod tests { fn empty_input_note() { let source = ""; let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii()) .with_message("unexpected end of file") .with_label(Label::new(0..0).with_message("No more fruit!")) @@ -1221,7 +1218,7 @@ mod tests { fn empty_input_help_note() { let source = ""; let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii()) .with_message("unexpected end of file") .with_label(Label::new(0..0).with_message("No more fruit!")) @@ -1253,7 +1250,7 @@ mod tests { for i in 0..=source.len() { for j in i..=source.len() { let _ = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii().with_index_type(IndexType::Byte)) .with_message("Label") .with_label(Label::new(i..j).with_message("Label")) @@ -1268,7 +1265,7 @@ mod tests { fn multiline_label() { let source = "apple\n==\norange"; let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii()) .with_label(Label::new(0..source.len()).with_message("illegal comparison")) .finish() @@ -1292,7 +1289,7 @@ mod tests { fn partially_overlapping_labels() { let source = "https://example.com/"; let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii()) .with_label(Label::new(0..source.len()).with_message("URL")) .with_label(Label::new(0..source.find(':').unwrap()).with_message("scheme")) @@ -1317,7 +1314,7 @@ mod tests { fn multiple_labels_same_span() { let source = "apple == orange;"; let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii()) .with_message("can't compare apples with oranges") .with_label(Label::new(0..5).with_message("This is an apple")) @@ -1358,7 +1355,7 @@ mod tests { fn note() { let source = "apple == orange;"; let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii()) .with_message("can't compare apples with oranges") .with_label(Label::new(0..5).with_message("This is an apple")) @@ -1386,7 +1383,7 @@ mod tests { fn help() { let source = "apple == orange;"; let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii()) .with_message("can't compare apples with oranges") .with_label(Label::new(0..5).with_message("This is an apple")) @@ -1414,7 +1411,7 @@ mod tests { fn help_and_note() { let source = "apple == orange;"; let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii()) .with_message("can't compare apples with oranges") .with_label(Label::new(0..5).with_message("This is an apple")) @@ -1445,7 +1442,7 @@ mod tests { fn single_note_single_line() { let source = "apple == orange;"; let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii()) .with_message("can't compare apples with oranges") .with_label(Label::new(0..15).with_message("This is a strange comparison")) @@ -1470,7 +1467,7 @@ mod tests { fn multi_notes_single_lines() { let source = "apple == orange;"; let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii()) .with_message("can't compare apples with oranges") .with_label(Label::new(0..15).with_message("This is a strange comparison")) @@ -1498,7 +1495,7 @@ mod tests { fn multi_notes_multi_lines() { let source = "apple == orange;"; let msg = remove_trailing( - Report::build(ReportKind::Error, 0..0) + Report::build(ErrorKind, 0..0) .with_config(no_color_and_ascii()) .with_message("can't compare apples with oranges") .with_label(Label::new(0..15).with_message("This is a strange comparison"))