diff --git a/crates/nu-engine/src/eval_ir.rs b/crates/nu-engine/src/eval_ir.rs index ecdebbac51..25b4cc1921 100644 --- a/crates/nu-engine/src/eval_ir.rs +++ b/crates/nu-engine/src/eval_ir.rs @@ -187,6 +187,7 @@ fn eval_ir_block_impl( // Program counter, starts at zero. let mut pc = 0; + let need_backtrace = ctx.engine_state.get_env_var("NU_BACKTRACE").is_some(); while pc < ir_block.instructions.len() { let instruction = &ir_block.instructions[pc]; @@ -195,7 +196,7 @@ fn eval_ir_block_impl( D::enter_instruction(ctx.engine_state, ir_block, pc, ctx.registers); - let result = eval_instruction::(ctx, instruction, span, ast); + let result = eval_instruction::(ctx, instruction, span, ast, need_backtrace); D::leave_instruction( ctx.engine_state, @@ -228,8 +229,10 @@ fn eval_ir_block_impl( // If an error handler is set, branch there prepare_error_handler(ctx, error_handler, Some(err.into_spanned(*span))); pc = error_handler.handler_index; + } else if need_backtrace { + let err = ShellError::into_chainned(err, *span); + return Err(err); } else { - // If not, exit the block with the error return Err(err); } } @@ -285,6 +288,7 @@ fn eval_instruction( instruction: &Instruction, span: &Span, ast: &Option, + need_backtrace: bool, ) -> Result { use self::InstructionResult::*; @@ -548,7 +552,14 @@ fn eval_instruction( } Instruction::Call { decl_id, src_dst } => { let input = ctx.take_reg(*src_dst); - let result = eval_call::(ctx, *decl_id, *span, input)?; + let mut result = eval_call::(ctx, *decl_id, *span, input)?; + if need_backtrace { + match &mut result { + PipelineData::ByteStream(s, ..) => s.push_caller_span(*span), + PipelineData::ListStream(s, ..) => s.push_caller_span(*span), + _ => (), + }; + } ctx.put_reg(*src_dst, result); Ok(Continue) } @@ -1457,14 +1468,40 @@ fn drain(ctx: &mut EvalContext<'_>, data: PipelineData) -> Result { let span = stream.span(); - if let Err(err) = stream.drain() { + let callback_spans = stream.get_caller_spans().clone(); + if let Err(mut err) = stream.drain() { ctx.stack.set_last_error(&err); - return Err(err); + if callback_spans.is_empty() { + return Err(err); + } else { + for s in callback_spans { + err = ShellError::EvalBlockWithInput { + span: s, + sources: vec![err], + } + } + return Err(err); + } } else { ctx.stack.set_last_exit_code(0, span); } } - PipelineData::ListStream(stream, ..) => stream.drain()?, + PipelineData::ListStream(stream, ..) => { + let callback_spans = stream.get_caller_spans().clone(); + if let Err(mut err) = stream.drain() { + if callback_spans.is_empty() { + return Err(err); + } else { + for s in callback_spans { + err = ShellError::EvalBlockWithInput { + span: s, + sources: vec![err], + } + } + return Err(err); + } + } + } PipelineData::Value(..) | PipelineData::Empty => {} } Ok(Continue) diff --git a/crates/nu-protocol/src/errors/chained_error.rs b/crates/nu-protocol/src/errors/chained_error.rs new file mode 100644 index 0000000000..ad45c44157 --- /dev/null +++ b/crates/nu-protocol/src/errors/chained_error.rs @@ -0,0 +1,120 @@ +use super::shell_error::ShellError; +use crate::Span; +use miette::{LabeledSpan, Severity, SourceCode}; +use thiserror::Error; + +/// An error struct that contains source errors. +/// +/// However, it's a bit special; if the error is constructed for the first time using +/// [`ChainedError::new`], it will behave the same as the single source error. +/// +/// If it's constructed nestedly using [`ChainedError::new_chained`], it will treat all underlying errors as related. +/// +/// For a usage example, please check [`ShellError::into_chainned`]. +#[derive(Debug, Clone, PartialEq, Error)] +pub struct ChainedError { + first: bool, + pub(crate) sources: Vec, + span: Span, +} + +impl std::fmt::Display for ChainedError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.first { + write!(f, "{}", self.sources[0]) + } else { + write!(f, "oops") + } + } +} + +impl ChainedError { + pub fn new(source: ShellError, span: Span) -> Self { + Self { + first: true, + sources: vec![source], + span, + } + } + + pub fn new_chained(sources: Self, span: Span) -> Self { + Self { + first: false, + sources: vec![ShellError::ChainedError(sources)], + span, + } + } +} + +impl miette::Diagnostic for ChainedError { + fn related<'a>(&'a self) -> Option + 'a>> { + if self.first { + self.sources[0].related() + } else { + Some(Box::new(self.sources.iter().map(|s| s as _))) + } + } + + fn code<'a>(&'a self) -> Option> { + if self.first { + self.sources[0].code() + } else { + Some(Box::new("chained_error")) + } + } + + fn severity(&self) -> Option { + if self.first { + self.sources[0].severity() + } else { + None + } + } + + fn help<'a>(&'a self) -> Option> { + if self.first { + self.sources[0].help() + } else { + None + } + } + + fn url<'a>(&'a self) -> Option> { + if self.first { + self.sources[0].url() + } else { + None + } + } + + fn labels<'a>(&'a self) -> Option + 'a>> { + if self.first { + self.sources[0].labels() + } else { + Some(Box::new( + vec![LabeledSpan::new_with_span( + Some("error happened when running this".to_string()), + self.span, + )] + .into_iter(), + )) + } + } + + // Finally, we redirect the source_code method to our own source. + fn source_code(&self) -> Option<&dyn SourceCode> { + if self.first { + self.sources[0].source_code() + } else { + None + } + } + + fn diagnostic_source(&self) -> Option<&dyn miette::Diagnostic> { + if self.first { + self.sources[0].diagnostic_source() + } else { + None + } + } +} diff --git a/crates/nu-protocol/src/errors/cli_error.rs b/crates/nu-protocol/src/errors/cli_error.rs index 7439d1df79..a53fec0600 100644 --- a/crates/nu-protocol/src/errors/cli_error.rs +++ b/crates/nu-protocol/src/errors/cli_error.rs @@ -50,6 +50,10 @@ pub fn report_compile_error(working_set: &StateWorkingSet, error: &CompileError) fn report_error(working_set: &StateWorkingSet, error: &dyn miette::Diagnostic) { eprintln!("Error: {:?}", CliError(error, working_set)); + let have_no_backtrace = working_set.get_env_var("NU_BACKTRACE").is_none(); + if have_no_backtrace { + eprintln!("set the `NU_BACKTRACE=1` environment variable to display a backtrace.") + } // reset vt processing, aka ansi because illbehaved externals can break it #[cfg(windows)] { diff --git a/crates/nu-protocol/src/errors/mod.rs b/crates/nu-protocol/src/errors/mod.rs index 5f385a21ee..9c729dc8f4 100644 --- a/crates/nu-protocol/src/errors/mod.rs +++ b/crates/nu-protocol/src/errors/mod.rs @@ -1,3 +1,4 @@ +mod chained_error; pub mod cli_error; mod compile_error; mod config_error; diff --git a/crates/nu-protocol/src/errors/shell_error/mod.rs b/crates/nu-protocol/src/errors/shell_error/mod.rs index 6f651836ca..c7be1dfec5 100644 --- a/crates/nu-protocol/src/errors/shell_error/mod.rs +++ b/crates/nu-protocol/src/errors/shell_error/mod.rs @@ -1,3 +1,4 @@ +use super::chained_error::ChainedError; use crate::{ ast::Operator, engine::StateWorkingSet, format_shell_error, record, ConfigError, LabeledError, ParseError, Span, Spanned, Type, Value, @@ -1325,6 +1326,10 @@ On Windows, this would be %USERPROFILE%\AppData\Roaming"# #[label = "while running this code"] span: Option, }, + + #[error(transparent)] + #[diagnostic(transparent)] + ChainedError(ChainedError), } impl ShellError { @@ -1373,6 +1378,16 @@ impl ShellError { span, ) } + + /// Convert self error to a [`ShellError::ChainedError`] variant. + pub fn into_chainned(self, span: Span) -> Self { + match self { + ShellError::ChainedError(inner) => { + ShellError::ChainedError(ChainedError::new_chained(inner, span)) + } + other => ShellError::ChainedError(ChainedError::new(other, span)), + } + } } impl From> for ShellError { diff --git a/crates/nu-protocol/src/pipeline/byte_stream.rs b/crates/nu-protocol/src/pipeline/byte_stream.rs index adefbdf9f0..2a7c79e128 100644 --- a/crates/nu-protocol/src/pipeline/byte_stream.rs +++ b/crates/nu-protocol/src/pipeline/byte_stream.rs @@ -193,6 +193,7 @@ pub struct ByteStream { signals: Signals, type_: ByteStreamType, known_size: Option, + caller_spans: Vec, } impl ByteStream { @@ -209,9 +210,22 @@ impl ByteStream { signals, type_, known_size: None, + caller_spans: vec![], } } + /// Push a caller [`Span`] to the bytestream, it's useful to construct a backtrace. + pub fn push_caller_span(&mut self, span: Span) { + if span != self.span { + self.caller_spans.push(span) + } + } + + /// Get all caller [`Span`], it's useful to construct a backtrace. + pub fn get_caller_spans(&self) -> &Vec { + &self.caller_spans + } + /// Create a [`ByteStream`] from an arbitrary reader. The type must be provided. pub fn read( reader: impl Read + Send + 'static, diff --git a/crates/nu-protocol/src/pipeline/list_stream.rs b/crates/nu-protocol/src/pipeline/list_stream.rs index 55ae4bfee0..f300a443d6 100644 --- a/crates/nu-protocol/src/pipeline/list_stream.rs +++ b/crates/nu-protocol/src/pipeline/list_stream.rs @@ -15,6 +15,7 @@ pub type ValueIterator = Box + Send + 'static>; pub struct ListStream { stream: ValueIterator, span: Span, + caller_spans: Vec, } impl ListStream { @@ -27,6 +28,7 @@ impl ListStream { Self { stream: Box::new(InterruptIter::new(iter, signals)), span, + caller_spans: vec![], } } @@ -35,6 +37,18 @@ impl ListStream { self.span } + /// Push a caller [`Span`] to the bytestream, it's useful to construct a backtrace. + pub fn push_caller_span(&mut self, span: Span) { + if span != self.span { + self.caller_spans.push(span) + } + } + + /// Get all caller [`Span`], it's useful to construct a backtrace. + pub fn get_caller_spans(&self) -> &Vec { + &self.caller_spans + } + /// Changes the [`Span`] associated with this [`ListStream`]. pub fn with_span(mut self, span: Span) -> Self { self.span = span; @@ -94,6 +108,7 @@ impl ListStream { Self { stream: Box::new(f(self.stream)), span: self.span, + caller_spans: self.caller_spans, } } diff --git a/crates/nu-protocol/tests/test_config.rs b/crates/nu-protocol/tests/test_config.rs index 49ad1cdec6..18e79635cf 100644 --- a/crates/nu-protocol/tests/test_config.rs +++ b/crates/nu-protocol/tests/test_config.rs @@ -60,7 +60,7 @@ fn fancy_default_errors() { assert_eq!( actual.err, - "Error: \u{1b}[31m×\u{1b}[0m oh no!\n ╭─[\u{1b}[36;1;4mline2:1:13\u{1b}[0m]\n \u{1b}[2m1\u{1b}[0m │ force_error \"My error\"\n · \u{1b}[35;1m ─────┬────\u{1b}[0m\n · \u{1b}[35;1m╰── \u{1b}[35;1mhere's the error\u{1b}[0m\u{1b}[0m\n ╰────\n\n" + "Error: \u{1b}[31m×\u{1b}[0m oh no!\n ╭─[\u{1b}[36;1;4mline2:1:13\u{1b}[0m]\n \u{1b}[2m1\u{1b}[0m │ force_error \"My error\"\n · \u{1b}[35;1m ─────┬────\u{1b}[0m\n · \u{1b}[35;1m╰── \u{1b}[35;1mhere's the error\u{1b}[0m\u{1b}[0m\n ╰────\n\nset the `NU_BACKTRACE=1` environment variable to display a backtrace.\n" ); } @@ -92,6 +92,7 @@ snippet line 1: force_error "my error" label at line 1, columns 13 to 22: here's the error +set the `NU_BACKTRACE=1` environment variable to display a backtrace. "#, ); } diff --git a/tests/shell/pipeline/commands/external.rs b/tests/shell/pipeline/commands/external.rs index 7268133ab0..e5c6915565 100644 --- a/tests/shell/pipeline/commands/external.rs +++ b/tests/shell/pipeline/commands/external.rs @@ -686,3 +686,34 @@ fn arg_dont_run_subcommand_if_surrounded_with_quote() { let actual = nu!("nu --testbin cococo '(echo aa)'"); assert_eq!(actual.out, "(echo aa)"); } + +#[test] +fn external_error_with_backtrace() { + Playground::setup("external error with backtrace", |dirs, sandbox| { + sandbox.with_files(&[FileWithContent("tmp_env.nu", "$env.NU_BACKTRACE = 1")]); + + let actual = nu!( + env_config: "tmp_env.nu", + cwd: dirs.test(), + r#"def a [x] { if $x == 3 { nu --testbin --fail }};def b [] {a 1; a 3; a 2}; b"#); + let chained_error_cnt: Vec<&str> = actual + .err + .matches("diagnostic code: chained_error") + .collect(); + assert_eq!(chained_error_cnt.len(), 1); + assert!(actual.err.contains("non_zero_exit_code")); + let eval_with_input_cnt: Vec<&str> = actual.err.matches("eval_block_with_input").collect(); + assert_eq!(eval_with_input_cnt.len(), 1); + + let actual = nu!( + env_config: "tmp_env.nu", + cwd: dirs.test(), + r#"nu --testbin --fail"#); + let chained_error_cnt: Vec<&str> = actual + .err + .matches("diagnostic code: chained_error") + .collect(); + // run error make directly, show no backtrace is available + assert_eq!(chained_error_cnt.len(), 0); + }); +} diff --git a/tests/shell/pipeline/commands/internal.rs b/tests/shell/pipeline/commands/internal.rs index 833dcaab8d..aa90fbf681 100644 --- a/tests/shell/pipeline/commands/internal.rs +++ b/tests/shell/pipeline/commands/internal.rs @@ -1,4 +1,4 @@ -use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::fs::Stub::{FileWithContent, FileWithContentToBeTrimmed}; use nu_test_support::playground::Playground; use nu_test_support::{nu, pipeline}; use pretty_assertions::assert_eq; @@ -1135,3 +1135,83 @@ fn error_on_out_greater_pipe() { .err .contains("Redirecting stdout to a pipe is the same as normal piping")) } + +#[test] +fn error_with_backtrace() { + Playground::setup("error with backtrace", |dirs, sandbox| { + sandbox.with_files(&[FileWithContent("tmp_env.nu", "$env.NU_BACKTRACE = 1")]); + + let actual = nu!( + env_config: "tmp_env.nu", + cwd: dirs.test(), + r#"def a [x] { if $x == 3 { error make {msg: 'a custom error'}}};a 3"#); + let chained_error_cnt: Vec<&str> = actual + .err + .matches("diagnostic code: chained_error") + .collect(); + // run `a 3`, and it raises error, so there should be 1. + assert_eq!(chained_error_cnt.len(), 1); + assert!(actual.err.contains("a custom error")); + + let actual = nu!( + env_config: "tmp_env.nu", + cwd: dirs.test(), + r#"def a [x] { if $x == 3 { error make {msg: 'a custom error'}}};def b [] { a 1; a 3; a 2 };b"#); + + let chained_error_cnt: Vec<&str> = actual + .err + .matches("diagnostic code: chained_error") + .collect(); + // run `b`, it runs `a 3`, and it raises error, so there should be 2. + assert_eq!(chained_error_cnt.len(), 2); + assert!(actual.err.contains("a custom error")); + + let actual = nu!( + env_config: "tmp_env.nu", + cwd: dirs.test(), + r#"error make {msg: 'a custom err'}"#); + let chained_error_cnt: Vec<&str> = actual + .err + .matches("diagnostic code: chained_error") + .collect(); + // run error make directly, show no backtrace is available + assert_eq!(chained_error_cnt.len(), 0); + }); +} + +#[test] +fn liststream_error_with_backtrace() { + Playground::setup("liststream error with backtrace", |dirs, sandbox| { + sandbox.with_files(&[FileWithContent("tmp_env.nu", "$env.NU_BACKTRACE = 1")]); + + let actual = nu!( + env_config: "tmp_env.nu", + cwd: dirs.test(), + r#"def a [x] { if $x == 3 { [1] | each {error make {'msg': 'a custom error'}}}};a 3"#); + assert!(actual.err.contains("a custom error")); + + let actual = nu!( + env_config: "tmp_env.nu", + cwd: dirs.test(), + r#"def a [x] { if $x == 3 { [1] | each {error make {'msg': 'a custom error'}}}};def b [] { a 1; a 3; a 2 };b"#); + let chained_error_cnt: Vec<&str> = actual + .err + .matches("diagnostic code: chained_error") + .collect(); + assert_eq!(chained_error_cnt.len(), 1); + assert!(actual.err.contains("a custom error")); + let eval_with_input_cnt: Vec<&str> = actual.err.matches("eval_block_with_input").collect(); + assert_eq!(eval_with_input_cnt.len(), 2); + + let actual = nu!( + env_config: "tmp_env.nu", + cwd: dirs.test(), + r#"[1] | each { error make {msg: 'a custom err'} }"#); + let chained_error_cnt: Vec<&str> = actual + .err + .matches("diagnostic code: chained_error") + .collect(); + // run error make directly, show no backtrace is available + assert_eq!(chained_error_cnt.len(), 0); + }); +}