diff --git a/Cargo.lock b/Cargo.lock index 38bbaec345..007f9bc974 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,6 +84,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "ariadne" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44055e597c674aef7cb903b2b9f6e4cba1277ed0d2d61dae7cd52d7ffa81f8e2" +dependencies = [ + "unicode-width 0.1.14", + "yansi", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -524,6 +534,7 @@ name = "just" version = "1.37.0" dependencies = [ "ansi_term", + "ariadne", "blake3", "camino", "chrono", diff --git a/Cargo.toml b/Cargo.toml index f10743b9af..941ceaca4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [".", "crates/*"] [dependencies] ansi_term = "0.12.0" +ariadne = "0.4.1" blake3 = { version = "1.5.0", features = ["rayon", "mmap"] } camino = "1.0.4" chrono = "0.4.38" diff --git a/justfile b/justfile index e975d506fb..759022e285 100755 --- a/justfile +++ b/justfile @@ -12,7 +12,7 @@ export JUST_LOG := log watch +args='test': cargo watch --clear --exec '{{ args }}' -[group: 'test'] +[group('test')] test: cargo test --all diff --git a/src/compile_error.rs b/src/compile_error.rs index 7fa2e0a3cb..7c17c1c7bd 100644 --- a/src/compile_error.rs +++ b/src/compile_error.rs @@ -19,6 +19,62 @@ impl<'src> CompileError<'src> { } } +pub(crate) fn render_compile_error(error: &CompileError, color: Color) { + use ariadne::{Config, Label, Report, ReportKind, Source}; + + let token = error.token; + let source = Source::from(token.src); + + let start = token.offset; + let end = token.offset + token.length; + + let path = format!("{}", token.path.display()); + let label = Label::new((&path, start..end)); + + let config = Config::default().with_color(color.stderr().active()); + let report = Report::build(ReportKind::Error, &path, start).with_config(config); + + let report = match &*error.kind { + CompileErrorKind::AttributeArgumentCountMismatch { + attribute, + found, + min, + max, + } => { + let label_msg = format!("Found {found} {}", Count("argument", *found)); + + let note = if min == max { + format!("`{attribute}` takes {min} {}", Count("argument", *min)) + } else { + format!("`{attribute}` takes between {min} and {max} arguments") + }; + + report + .with_message("Attribute argument count mismatch") + .with_label(label.with_message(label_msg)) + .with_note(note) + .finish() + } + CompileErrorKind::DuplicateAttribute { attribute, first } => { + let original_label = source + .line(*first) + .map(|line| Label::new((&path, line.span())).with_message("original")); + + let mut report = report.with_message(format!("Duplicate attribute `{attribute}`")); + if let Some(original) = original_label { + report = report.with_label(original); + } + report.with_label(label.with_message("duplicate")).finish() + } + _ => { + let message = format!("{error}"); + report.with_message(message).with_label(label).finish() + } + }; + + report.eprint((&path, source)).unwrap(); +} + fn capitalize(s: &str) -> String { let mut chars = s.chars(); match chars.next() { diff --git a/src/error.rs b/src/error.rs index a07c4efd62..7b91b2fe03 100644 --- a/src/error.rs +++ b/src/error.rs @@ -504,6 +504,13 @@ impl ColorDisplay for Error<'_> { } } +pub(crate) fn render_error(error: &Error, color: Color) { + match error { + Error::Compile { compile_error } => compile_error::render_compile_error(compile_error, color), + _ => eprintln!("{}", error.color_display(color.stderr())), + } +} + fn format_cmd(binary: &OsString, arguments: &Vec) -> String { iter::once(binary) .chain(arguments) diff --git a/src/run.rs b/src/run.rs index a07b73bbc9..c17ba50236 100644 --- a/src/run.rs +++ b/src/run.rs @@ -29,7 +29,7 @@ pub fn run(args: impl Iterator + Clone>) -> Result<() }) .map_err(|error| { if !verbosity.quiet() && error.print_message() { - eprintln!("{}", error.color_display(color.stderr())); + crate::error::render_error(&error, color); } error.code().unwrap_or(EXIT_FAILURE) }) diff --git a/tests/string.rs b/tests/string.rs index 12bcbac0f7..06a0d4a9a9 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -184,12 +184,12 @@ test! { args: ("a"), stdout: "", stderr: " - error: Unterminated string - ——▶ justfile:1:6 - │ - 1 │ a b= ': - │ ^ - ", +Error: Unterminated string + ╭─[justfile:1:6] + │ + 1 │ a b= ': +───╯ +", status: EXIT_FAILURE, } @@ -201,12 +201,12 @@ test! { args: ("a"), stdout: "", stderr: r#" - error: Unterminated string - ——▶ justfile:1:6 - │ - 1 │ a b= ": - │ ^ - "#, +Error: Unterminated string + ╭─[justfile:1:6] + │ + 1 │ a b= ": +───╯ +"#, status: EXIT_FAILURE, } @@ -234,11 +234,11 @@ test! { args: ("a"), stdout: "", stderr: " - error: Unterminated string - ——▶ justfile:1:6 - │ - 1 │ a b= ''': - │ ^^^ +Error: Unterminated string + ╭─[justfile:1:6] + │ + 1 │ a b= ''': +───╯ ", status: EXIT_FAILURE, } @@ -251,11 +251,11 @@ test! { args: ("a"), stdout: "", stderr: r#" - error: Unterminated string - ——▶ justfile:1:6 - │ - 1 │ a b= """: - │ ^^^ +Error: Unterminated string + ╭─[justfile:1:6] + │ + 1 │ a b= """: +───╯ "#, status: EXIT_FAILURE, } @@ -481,11 +481,11 @@ fn unicode_escape_non_hex() { .status(1) .stderr( r#" -error: expected hex digit [0-9A-Fa-f] but found `o` - ——▶ justfile:1:6 - │ -1 │ x := "\u{foo}" - │ ^^^^^^^^^ +Error: expected hex digit [0-9A-Fa-f] but found `o` + ╭─[justfile:1:6] + │ + 1 │ x := "\u{foo}" +───╯ "#, ) .run(); @@ -517,11 +517,11 @@ fn unicode_escape_too_long() { .status(1) .stderr( r#" -error: unicode escape sequence starting with `\u{FFFFFFF` longer than six hex digits - ——▶ justfile:1:6 - │ -1 │ x := "\u{FFFFFFFFFF}" - │ ^^^^^^^^^^^^^^^^ +Error: unicode escape sequence starting with `\u{FFFFFFF` longer than six hex digits + ╭─[justfile:1:6] + │ + 1 │ x := "\u{FFFFFFFFFF}" +───╯ "#, ) .run(); diff --git a/tests/subsequents.rs b/tests/subsequents.rs index 9ef4600067..c0a7ff9fe6 100644 --- a/tests/subsequents.rs +++ b/tests/subsequents.rs @@ -45,13 +45,13 @@ test! { justfile: " foo: && foo ", - stderr: " - error: Recipe `foo` depends on itself - ——▶ justfile:1:9 - │ - 1 │ foo: && foo - │ ^^^ - ", + stderr: +"Error: Recipe `foo` depends on itself + ╭─[justfile:1:9] + │ + 1 │ foo: && foo +───╯ +", status: EXIT_FAILURE, } diff --git a/tests/test.rs b/tests/test.rs index e02b5a85b6..9bf68d3add 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -228,7 +228,8 @@ impl Test { fn compare_string(name: &str, have: &str, want: &str) -> bool { let equal = have == want; if !equal { - eprintln!("Bad {name}: {}", StrComparison::new(&have, &want)); + //eprintln!("Bad {name}: {}", StrComparison::new(&have, &want)); + eprintln!("Bad {name}:\n{}||\n-------------\n{}||\n=========", &have, &want); } equal } diff --git a/tests/working_directory.rs b/tests/working_directory.rs index bfae635f5c..9580b33ae1 100644 --- a/tests/working_directory.rs +++ b/tests/working_directory.rs @@ -381,12 +381,12 @@ fn attribute_with_nocd_is_forbidden() { ) .stderr( " - error: Recipe `bar` has both `[no-cd]` and `[working-directory]` attributes - ——▶ justfile:3:1 - │ - 3 │ bar: - │ ^^^ - ", +Error: Recipe `bar` has both `[no-cd]` and `[working-directory]` attributes + ╭─[justfile:3:1] + │ + 3 │ bar: +───╯ +" ) .status(EXIT_FAILURE) .run();