simple errors for lang-dev
| -rw-r--r-- | .github/example.png | bin | 14014 -> 20132 bytes | |||
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Cargo.lock | 16 | ||||
| -rw-r--r-- | Cargo.toml | 6 | ||||
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | src/config.rs | 81 | ||||
| -rw-r--r-- | src/lib.rs | 146 |
7 files changed, 200 insertions, 62 deletions
diff --git a/.github/example.png b/.github/example.png Binary files differindex 0b48e46..9f574e3 100644 --- a/.github/example.png +++ b/.github/example.png @@ -1 +1,2 @@ /target +Cargo.lock diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 9eb39a4..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,16 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "lerr" -version = "0.1.2" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" @@ -1,6 +1,6 @@ [package] name = "lerr" -version = "0.1.2" +version = "0.1.3" edition = "2021" license = "MIT" readme = "README.md" @@ -12,4 +12,8 @@ keywords = ["error", "lang-dev", "diagnostics"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +comat = "0.1.0" unicode-width = "0.1.10" + +[dev-dependencies] +anstream = { version = "0.5.0", default-features = false } @@ -10,15 +10,15 @@ heres the code for the sample above. feel free to add coloring with your favorite coloring crate, or just use raw ansi sequences. ```rust +use comat::cformat as cmt; use lerr::Error; let mut e = Error::new("Strin::nouveau().i_like_tests(3.14158)"); -e.label((0..5, "you probably meant String")) - .label((7..16, "use new()")) - .label((17..18, "caps: I")) - .label((30..37, "your π is bad")); +e.message(cmt!(r#"{bold_red}error{reset}: unknown function {bold_red}String::new(){reset}"#)) + .label((0..5, cmt!("you probably meant {black}String{reset}"))) + .label((7..16, cmt!("use {green}new(){reset}"))) + .label((17..18, cmt!("caps: {bold_cyan}I{reset}"))) + .label((30..37, cmt!("your {bold_yellow}π{reset} is bad"))); eprintln!("{e}"); -// dont mind this -assert_eq!(e.to_string(), "\n\u{1b}[1;34;30m0 | \u{1b}[0mStrin::nouveau().i_like_tests(3.14158)\n\u{1b}[1;34;30m ¦ \u{1b}[0m\u{1b}[1;34;31m──┬──\u{1b}[0m \u{1b}[1;34;31m────┬────\u{1b}[0m \u{1b}[1;34;31m^\u{1b}[0m caps: I \u{1b}[1;34;31m^^^^^^^\u{1b}[0m your π is bad\n\u{1b}[1;34;30m ¦ \u{1b}[0m \u{1b}[1;34;31m│\u{1b}[0m \u{1b}[1;34;31m╰\u{1b}[0m use new()\n\u{1b}[1;34;30m ¦ \u{1b}[0m \u{1b}[1;34;31m╰\u{1b}[0m you probably meant String\n"); ``` Please note that multiline labels are not yet supported. diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..dd7ffdb --- /dev/null +++ b/src/config.rs @@ -0,0 +1,81 @@ +//! allows configuration of how the error will appear. +/// characters used in printing the error. +#[derive(Debug, Clone, Copy)] +pub struct Charset { + /// the line on the left + pub column_line: char, + /// the line on the left when theres a label there + pub column_broken_line: char, + /// the character shown below the error span for inline labels + /// ```text + /// 0 | problem + /// ^^^^^^^ issue + /// ^^^^^^^ these ones + /// ``` + pub spanning: char, + /// the character shown about the span, when the error is moved to a next line + /// ```text + /// 0 | problem + /// ───┬─── + /// ^^^ ^^^ these ones + /// ``` + pub spanning_out: char, + /// the character shown in the middle of the span, when the error is moved to a next line, in the middle + /// ```text + /// 0 | problem + /// ───┬─── + /// ^ this one + /// ``` + pub spanning_mid: char, + /// the character used to extend the label to yet another line + /// ```text + /// 0 | problem + /// ───┬─── + /// │ < this one + /// ``` + pub out_extension: char, + /// the character used to end the label for a moved label + /// ```text + /// 0 | problem + /// ───┬─── + /// ╰ issue + /// ^ this one + /// ``` + pub out_end: char, + /// the character used for a note + /// ```text + /// 0 | problem + /// > btw i must say you use the same text in the example alot + /// ^ this one + /// ``` + pub note: char, +} + +impl Charset { + /// Produces a (pretty) unicode charset. + pub const fn unicode() -> Self { + Self { + column_line: '|', + column_broken_line: '¦', + spanning: '^', + spanning_out: '─', + spanning_mid: '┬', + out_extension: '│', // not a pipe btw + out_end: '╰', + note: '>', + } + } + /// Produces a (ugly) ascii charset. + pub const fn ascii() -> Self { + Self { + column_line: '|', + column_broken_line: ':', + spanning: '^', + spanning_out: '-', + spanning_mid: '.', + out_extension: '|', + out_end: '\\', + note: '>', + } + } +} @@ -1,18 +1,19 @@ #![doc = include_str!("../README.md")] +#![forbid(unsafe_code)] #![warn( - clippy::multiple_unsafe_ops_per_block, clippy::missing_const_for_fn, clippy::redundant_pub_crate, - clippy::missing_safety_doc, clippy::imprecise_flops, - unsafe_op_in_unsafe_fn, clippy::dbg_macro, missing_docs )] -use std::ops::Range; - +use comat::{cwrite, cwriteln}; +use config::Charset; +use std::{fmt::Write, ops::Range}; use unicode_width::UnicodeWidthStr; +pub mod config; + /// Span of bytes in the source pub type Span = Range<usize>; /// Label around a [`Span`] @@ -57,15 +58,18 @@ impl<'s> Source<'s> { /// The error builder that this crate is all about #[derive(Debug)] +#[non_exhaustive] pub struct Error<'s> { + /// The message + pub message: String, /// Source text pub source: Source<'s>, /// Labels we hold pub labels: Vec<Label>, /// Notes pub notes: Vec<Note>, - /// The message - pub message: String, + /// The config + pub charset: Charset, } impl<'s> Error<'s> { @@ -77,9 +81,16 @@ impl<'s> Error<'s> { source: Source(source), notes: vec![], message: String::new(), + charset: Charset::unicode(), } } + /// Sets the charset + pub fn charset(&mut self, charset: Charset) -> &mut Self { + self.charset = charset; + self + } + /// Add a message to this error pub fn message(&mut self, message: impl ToString) -> &mut Self { self.message = message.to_string(); @@ -89,9 +100,7 @@ impl<'s> Error<'s> { /// Add a label to this error pub fn label(&mut self, label: impl Into<Label>) -> &mut Self { let l = label.into(); - if self.source.0.len() < l.span.end { - panic!("label must be in bounds"); - } + assert!(self.source.0.len() >= l.span.end, "label must be in bounds"); self.labels.push(l); self } @@ -103,17 +112,24 @@ impl<'s> Error<'s> { }); self } + + #[cfg(test)] + fn monochrome(&self) -> String { + anstream::adapter::strip_str(&self.to_string()).to_string() + } } macro_rules! wrpeat { - ($to:ident, $n:expr, $fmt:literal $(, $arg:expr)* $(,)?) => { - for _ in 0..$n { write!($to, $fmt $(, $arg)*)? } + ($to:ident, $n:expr, $fmt:expr) => { + for _ in 0..$n { + write!($to, "{}", $fmt)? + } }; } impl<'s> std::fmt::Display for Error<'s> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!(f, "{}", self.message)?; + cwriteln!(f, "{:reset}", self.message)?; let lines = self.source.0.lines().count(); let width = lines.ilog10() as usize + 1; let space = " "; @@ -150,8 +166,16 @@ impl<'s> std::fmt::Display for Error<'s> { if found.is_empty() { continue; } - writeln!(f, "\x1b[1;34;30m{line:width$} | \x1b[0m{code}")?; - write!(f, "\x1b[1;34;30m{space:width$} ¦ \x1b[0m")?; + cwriteln!( + f, + "{bold_black}{line:width$} {} {reset}{code}", + self.charset.column_line + )?; + cwrite!( + f, + "{space:width$} {:bold_black} {reset}", + self.charset.column_broken_line + )?; // sort by width found.sort_unstable_by(|(a, ..), (b, ..)| match a.span.start.cmp(&b.span.start) { @@ -176,36 +200,40 @@ impl<'s> std::fmt::Display for Error<'s> { { let p = about.saturating_sub(1); let middle = (p + 1) / 2; - write!(f, "\x1b[1;34;31m")?; - wrpeat!(f, middle, "─"); - write!(f, "┬")?; - wrpeat!(f, p - middle, "─"); - write!(f, "\x1b[0m")?; + cwrite!(f, "{bold_red}")?; + wrpeat!(f, middle, self.charset.spanning_out); + f.write_char(self.charset.spanning_mid)?; + wrpeat!(f, p - middle, self.charset.spanning_out); + cwrite!(f, "{reset}")?; middles.push((l, middle, msglen)); position += about; continue; } - write!(f, "\x1b[1;34;31m")?; - wrpeat!(f, about, "^"); + cwrite!(f, "{bold_red}")?; + wrpeat!(f, about, self.charset.spanning); position += about; - write!(f, "\x1b[0m ")?; - position += 1; - write!(f, "{}", l.message)?; - position += msglen; + cwrite!(f, " {:reset}", l.message)?; + position += 1 + msglen; } writeln!(f)?; - extras(self, middles, line_span, f, width)?; + extras(self, middles, line_span, f, width, self.charset)?; fn extras( e: &Error, mut unfinished: Vec<(&Label, usize, usize)>, line_span: Span, f: &mut std::fmt::Formatter<'_>, width: usize, + charset: Charset, ) -> std::fmt::Result { if unfinished.is_empty() { return Ok(()); } - write!(f, "\x1b[1;34;30m{:width$} ¦ \x1b[0m", " ")?; + cwrite!( + f, + "{:width$} {:bold_black} ", + " ", + charset.column_broken_line + )?; let mut position = 0; let mut i = 0; while i < unfinished.len() { @@ -225,26 +253,26 @@ impl<'s> std::fmt::Display for Error<'s> { .any(|(b, ..)| l.span.start + connection + msglen + 2 > b.span.start) { // if it will, leave it for the next line (this is a recursive fn) - write!(f, "\x1b[1;34;31m│\x1b[0m ")?; + cwrite!(f, "{:bold_red} ", charset.out_extension)?; position += 2; i += 1; continue; } - write!(f, "\x1b[1;34;31m╰\x1b[0m ")?; + cwrite!(f, "{:bold_red} ", charset.out_end)?; position += 2; - write!(f, "{}", l.message)?; + cwrite!(f, "{:reset}", l.message)?; position += msglen; unfinished.remove(i); } writeln!(f)?; - extras(e, unfinished, line_span, f, width) + extras(e, unfinished, line_span, f, width, charset) } found.clear(); } for note in &self.notes { - writeln!(f, "{space:width$} \x1b[1;34;30m>\x1b[0m {}", note.message)?; + cwriteln!(f, "{space:width$} {bold_black}>{reset} {}", note.message)?; } Ok(()) } @@ -253,19 +281,59 @@ impl<'s> std::fmt::Display for Error<'s> { #[test] fn display() { let out = Error::new("void fn x(void) -> four {\nwierd};") + .message("attempted to use string as type") .label((19..23, "what is 'four'?")) - .note("\x1b[1;34;32mhelp\x1b[0m: change it to 4") - .note("\x1b[1;34;34mnote\x1b[0m: maybe python would be better for you") - .to_string(); + .note("help: change it to 4") + .note("note: maybe python would be better for you") + .charset(Charset::ascii()) + .monochrome(); println!("{out}"); - assert_eq!(out, "\n\u{1b}[1;34;30m0 | \u{1b}[0mvoid fn x(void) -> four {\n\u{1b}[1;34;30m ¦ \u{1b}[0m \u{1b}[1;34;31m^^^^\u{1b}[0m what is 'four'?\n \u{1b}[1;34;30m>\u{1b}[0m \u{1b}[1;34;32mhelp\u{1b}[0m: change it to 4\n \u{1b}[1;34;30m>\u{1b}[0m \u{1b}[1;34;34mnote\u{1b}[0m: maybe python would be better for you\n"); + assert_eq!( + out, + r"attempted to use string as type +0 | void fn x(void) -> four { + : ^^^^ what is 'four'? + > help: change it to 4 + > note: maybe python would be better for you +" + ); } #[test] fn inline() { let out = Error::new("im out of this worl") + .message("such spelling") .label((15..19, "forgot d")) .label((0..2, r#"forgot '"#)) - .to_string(); + .charset(Charset::ascii()) + .monochrome(); println!("{out}"); - assert_eq!(out, "\n\u{1b}[1;34;30m0 | \u{1b}[0mim out of this worl\n\u{1b}[1;34;30m ¦ \u{1b}[0m\u{1b}[1;34;31m^^\u{1b}[0m forgot ' \u{1b}[1;34;31m^^^^\u{1b}[0m forgot d\n"); + assert_eq!( + out, + r"such spelling +0 | im out of this worl + : ^^ forgot ' ^^^^ forgot d +" + ); +} + +#[test] +fn outline() { + let e = Error::new("Strin::nouveau().i_like_tests(3.14158)") + .message("unknown method String::new") + .label((0..5, "you probably meant String")) + .label((7..16, "use new()")) + .label((17..18, "caps: I")) + .label((30..37, "your π is bad")) + .charset(Charset::ascii()) + .monochrome(); + println!("{e}"); + assert_eq!( + e, + r"unknown method String::new +0 | Strin::nouveau().i_like_tests(3.14158) + : --.-- ----.---- ^ caps: I ^^^^^^^ your π is bad + : | \ use new() + : \ you probably meant String +" + ); } |