From e735bd475f53b62e30a3e4a041e21462db63ac47 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Thu, 17 Oct 2024 20:16:38 -0500 Subject: [PATCH] add rendered and json error messages in try/catch (#14082) # Description This PR adds a couple more options for dealing with try/catch errors. It adds a `json` version of the error and a `rendered` version of the error. It also respects the error_style configuration point. ![image](https://github.com/user-attachments/assets/32574f07-f511-40c0-8b57-de5f6f13a9c4) # User-Facing Changes # Tests + Formatting # After Submitting --- crates/nu-cmd-lang/src/core_commands/try_.rs | 6 ++++- crates/nu-command/tests/commands/try_.rs | 17 ++++++++++++++ crates/nu-engine/src/eval_ir.rs | 17 ++++++++++++-- crates/nu-protocol/Cargo.toml | 1 + .../nu-protocol/src/errors/labeled_error.rs | 17 ++++++++++++++ crates/nu-protocol/src/errors/shell_error.rs | 22 +++++++++++++++++-- 6 files changed, 75 insertions(+), 5 deletions(-) diff --git a/crates/nu-cmd-lang/src/core_commands/try_.rs b/crates/nu-cmd-lang/src/core_commands/try_.rs index ccf0d41f3b..c7242550a7 100644 --- a/crates/nu-cmd-lang/src/core_commands/try_.rs +++ b/crates/nu-cmd-lang/src/core_commands/try_.rs @@ -107,7 +107,11 @@ fn run_catch( if let Some(catch) = catch { stack.set_last_error(&error); - let error = error.into_value(span); + let fancy_errors = match engine_state.get_config().error_style { + nu_protocol::ErrorStyle::Fancy => true, + nu_protocol::ErrorStyle::Plain => false, + }; + let error = error.into_value(span, fancy_errors); let block = engine_state.get_block(catch.block_id); // Put the error value in the positional closure var if let Some(var) = block.signature.get_positional(0) { diff --git a/crates/nu-command/tests/commands/try_.rs b/crates/nu-command/tests/commands/try_.rs index 487fd034b5..7e6952d2b9 100644 --- a/crates/nu-command/tests/commands/try_.rs +++ b/crates/nu-command/tests/commands/try_.rs @@ -126,3 +126,20 @@ fn prints_only_if_last_pipeline() { let actual = nu!("try { ['should not print'] | every 1 }; 'last value'"); assert_eq!(actual.out, "last value"); } + +#[test] +fn get_error_columns() { + let actual = nu!(" try { non_existent_command } catch {|err| $err} | columns | to json -r"); + assert_eq!( + actual.out, + "[\"msg\",\"debug\",\"raw\",\"rendered\",\"json\"]" + ); +} + +#[test] +fn get_json_error() { + let actual = nu!("try { non_existent_command } catch {|err| $err} | get json | from json | update labels.span {{start: 0 end: 0}} | to json -r"); + assert_eq!( + actual.out, "{\"msg\":\"External command failed\",\"labels\":[{\"text\":\"Command `non_existent_command` not found\",\"span\":{\"start\":0,\"end\":0}}],\"code\":\"nu::shell::external_command\",\"url\":null,\"help\":\"`non_existent_command` is neither a Nushell built-in or a known external command\",\"inner\":[]}" + ); +} diff --git a/crates/nu-engine/src/eval_ir.rs b/crates/nu-engine/src/eval_ir.rs index 233b01c33f..8d809f182e 100644 --- a/crates/nu-engine/src/eval_ir.rs +++ b/crates/nu-engine/src/eval_ir.rs @@ -220,8 +220,17 @@ fn eval_ir_block_impl( } Err(err) => { if let Some(error_handler) = ctx.stack.error_handlers.pop(ctx.error_handler_base) { + let fancy_errors = match ctx.engine_state.get_config().error_style { + nu_protocol::ErrorStyle::Fancy => true, + nu_protocol::ErrorStyle::Plain => false, + }; // If an error handler is set, branch there - prepare_error_handler(ctx, error_handler, Some(err.into_spanned(*span))); + prepare_error_handler( + ctx, + error_handler, + Some(err.into_spanned(*span)), + fancy_errors, + ); pc = error_handler.handler_index; } else { // If not, exit the block with the error @@ -246,6 +255,7 @@ fn prepare_error_handler( ctx: &mut EvalContext<'_>, error_handler: ErrorHandler, error: Option>, + fancy_errors: bool, ) { if let Some(reg_id) = error_handler.error_register { if let Some(error) = error { @@ -254,7 +264,10 @@ fn prepare_error_handler( // Create the error value and put it in the register ctx.put_reg( reg_id, - error.item.into_value(error.span).into_pipeline_data(), + error + .item + .into_value(error.span, fancy_errors) + .into_pipeline_data(), ); } else { // Set the register to empty diff --git a/crates/nu-protocol/Cargo.toml b/crates/nu-protocol/Cargo.toml index 5556ce4755..e5deba5fa2 100644 --- a/crates/nu-protocol/Cargo.toml +++ b/crates/nu-protocol/Cargo.toml @@ -35,6 +35,7 @@ miette = { workspace = true, features = ["fancy-no-backtrace"] } num-format = { workspace = true } rmp-serde = { workspace = true, optional = true } serde = { workspace = true } +serde_json = { workspace = true } thiserror = "1.0" typetag = "0.2" os_pipe = { workspace = true, features = ["io_safety"] } diff --git a/crates/nu-protocol/src/errors/labeled_error.rs b/crates/nu-protocol/src/errors/labeled_error.rs index c76a6c78dd..a207178415 100644 --- a/crates/nu-protocol/src/errors/labeled_error.rs +++ b/crates/nu-protocol/src/errors/labeled_error.rs @@ -139,6 +139,23 @@ impl LabeledError { self } + pub fn render_error_to_string(diag: impl miette::Diagnostic, fancy_errors: bool) -> String { + let theme = if fancy_errors { + miette::GraphicalTheme::unicode() + } else { + miette::GraphicalTheme::none() + }; + + let mut out = String::new(); + miette::GraphicalReportHandler::new() + .with_width(80) + .with_theme(theme) + .render_report(&mut out, &diag) + .unwrap_or_default(); + + out + } + /// Create a [`LabeledError`] from a type that implements [`miette::Diagnostic`]. /// /// # Example diff --git a/crates/nu-protocol/src/errors/shell_error.rs b/crates/nu-protocol/src/errors/shell_error.rs index 0f609061d6..49ece8323a 100644 --- a/crates/nu-protocol/src/errors/shell_error.rs +++ b/crates/nu-protocol/src/errors/shell_error.rs @@ -1474,13 +1474,16 @@ impl ShellError { self.external_exit_code().map(|e| e.item).unwrap_or(1) } - pub fn into_value(self, span: Span) -> Value { + pub fn into_value(self, span: Span, fancy_errors: bool) -> Value { let exit_code = self.external_exit_code(); let mut record = record! { "msg" => Value::string(self.to_string(), span), "debug" => Value::string(format!("{self:?}"), span), - "raw" => Value::error(self, span), + "raw" => Value::error(self.clone(), span), + // "labeled_error" => Value::string(LabeledError::from_diagnostic_and_render(self.clone()), span), + "rendered" => Value::string(ShellError::render_error_to_string(self.clone(), fancy_errors), span), + "json" => Value::string(serde_json::to_string(&self).expect("Could not serialize error"), span), }; if let Some(code) = exit_code { @@ -1499,6 +1502,21 @@ impl ShellError { span, ) } + pub fn render_error_to_string(diag: impl miette::Diagnostic, fancy_errors: bool) -> String { + let theme = if fancy_errors { + miette::GraphicalTheme::unicode() + } else { + miette::GraphicalTheme::none() + }; + let mut out = String::new(); + miette::GraphicalReportHandler::new() + .with_width(80) + .with_theme(theme) + .render_report(&mut out, &diag) + .unwrap_or_default(); + + out + } } impl From for ShellError {